/* * ModeShape (http://www.modeshape.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.modeshape.jcr.index.local; import java.io.File; import java.nio.file.Paths; import javax.jcr.RepositoryException; import org.mapdb.DB; import org.mapdb.DBMaker; import org.modeshape.common.collection.Problems; import org.modeshape.jcr.ExecutionContext; import org.modeshape.jcr.JcrI18n; import org.modeshape.jcr.NodeTypes; import org.modeshape.jcr.NodeTypes.Supplier; import org.modeshape.jcr.api.index.IndexDefinition; import org.modeshape.jcr.api.query.qom.ChildCount; import org.modeshape.jcr.api.query.qom.QueryObjectModelConstants; import org.modeshape.jcr.cache.change.ChangeSetAdapter.NodeTypePredicate; import org.modeshape.jcr.query.QueryContext; import org.modeshape.jcr.query.model.Comparison; import org.modeshape.jcr.query.model.FullTextSearch; import org.modeshape.jcr.spi.index.IndexCostCalculator; import org.modeshape.jcr.spi.index.provider.IndexProvider; import org.modeshape.jcr.spi.index.provider.IndexUsage; import org.modeshape.jcr.spi.index.provider.ManagedIndexBuilder; /** * An {@link IndexProvider} implementation that maintains indexes on the local file system using MapDB. * <p> * This provider is instantiated with: * <ul> * <li>an {@code directory} attribute, or</li> * <li>an {@code path} attribute <i>and</i> an {@code relativeTo} attribute</li> * </ul> * * @author Randall Hauch (rhauch@redhat.com) */ public class LocalIndexProvider extends IndexProvider { private static final String DB_FILENAME = "local-indexes.db"; /** * The directory in which the indexes are to be stored. This needs to be set, or the {@link #path} and {@link #relativeTo} * need to be set. */ private String directory; /** * The path in which the indexes are to be stored, relative to {@link #relativeTo}. Both of these need to be set, or the * {@link #directory} needs to be set. */ private String path; /** * The directory relative to which the {@link #path} specifies where the indexes are to be stored. Both of these need to be * set, or the {@link #directory} needs to be set. */ private String relativeTo; private DB db; private IndexUpdater indexUpdater; /** * A bunch of MapDB specific options which can be used to further tweak this provider */ private boolean cacheLRUEnable = false; private boolean mmapFileEnable = false; private boolean commitFileSyncDisable = false; private boolean transactionDisable = false; private boolean asyncWrite = false; private Integer cacheSize; public LocalIndexProvider() { } /** * Get the absolute or relative path to the directory where this provider should store the indexes. * * @return the path to the directory */ public String getDirectory() { return directory; } @Override protected void doInitialize() throws RepositoryException { if (directory == null && relativeTo != null && path != null) { // Try to set the directory using relativeTo and path ... try { File rel = new File(relativeTo); File dir = Paths.get(rel.toURI()).resolve(path).toFile(); directory = dir.getAbsolutePath(); } catch (RuntimeException e) { throw new RepositoryException(e); } } if (directory == null) { throw new RepositoryException(JcrI18n.localIndexProviderMustHaveDirectory.text(getRepositoryName())); } logger().debug("Initializing the local index provider '{0}' in repository '{1}' at: {2}", getName(), getRepositoryName(), directory); // Find the directory and make sure it exists and we have read and write permission ... File dir = new File(directory); if (!dir.exists()) { // Try to make it ... logger().debug("Attempting to create directory for local indexes in repository '{1}' at: {0}", dir.getAbsolutePath(), getRepositoryName()); if (dir.mkdirs()) { logger().debug("Created directory for local indexes in repository '{1}' at: {0}", dir.getAbsolutePath(), getRepositoryName()); } else { logger().debug("Unable to create directory for local indexes in repository '{1}' at: {0}", dir.getAbsolutePath(), getRepositoryName()); } } if (!dir.canRead()) { throw new RepositoryException(JcrI18n.localIndexProviderDirectoryMustBeReadable.text(dir, getRepositoryName())); } if (!dir.canWrite()) { throw new RepositoryException(JcrI18n.localIndexProviderDirectoryMustBeWritable.text(dir, getRepositoryName())); } // Find the file for the indexes ... File file = new File(dir, DB_FILENAME); if (logger().isDebugEnabled()) { String action = file.exists() ? "Opening" : "Creating"; logger().debug("{0} the local index provider database for repository '{1}' at: {2}", action, getRepositoryName(), file.getAbsolutePath()); } // Get the database ... DBMaker<?> dbMaker = DBMaker.newFileDB(file); if (this.cacheSize != null) { dbMaker.cacheSize(cacheSize); logger().debug("MapDB cache size set to {0} for index provider {1}", cacheSize, getName()); } if (this.cacheLRUEnable) { dbMaker.cacheLRUEnable(); logger().debug("MapDB cacheLRU enabled for index provider {0}", getName()); } if (this.mmapFileEnable) { dbMaker.mmapFileEnableIfSupported(); logger().debug("MapDB mmapFiles enabled for index provider {0}", getName()); } if (this.commitFileSyncDisable) { dbMaker.commitFileSyncDisable(); logger().debug("MapDB commitFileSync enabled for index provider {0}", getName()); } if (this.transactionDisable) { dbMaker.transactionDisable(); logger().debug("MapDB transactions disabled for index provider {0}", getName()); } if (this.asyncWrite) { dbMaker.asyncWriteEnable(); logger().debug("MapDB async writes enabled for index provider {0}", getName()); } // we always want to have the close via the shutdown hook; it should be idempotent dbMaker.closeOnJvmShutdown(); this.db = dbMaker.make(); this.indexUpdater = new IndexUpdater(db); logger().trace("Found the index files {0} in index database for repository '{1}' at: {2}", db.getCatalog(), getRepositoryName(), file.getAbsolutePath()); } @Override protected void postShutdown() { logger().debug("Shutting down the local index provider '{0}' in repository '{1}'", getName(), getRepositoryName()); if (db != null && !db.isClosed()) { try { db.commit(); db.close(); } finally { db = null; } } } @Override public Long getLatestIndexUpdateTime() { return indexUpdater.latestIndexUpdateTime(); } @Override public void validateProposedIndex( ExecutionContext context, IndexDefinition defn, NodeTypes.Supplier nodeTypeSupplier, Problems problems ) { // first perform some custom validations LocalIndexBuilder.validate(defn, problems); } @Override protected ManagedIndexBuilder getIndexBuilder( IndexDefinition defn, String workspaceName, Supplier nodeTypesSupplier, NodeTypePredicate matcher ) { return LocalIndexBuilder.create(context(), defn, nodeTypesSupplier, workspaceName, matcher, db); } @Override protected IndexUsage evaluateUsage( QueryContext context, IndexCostCalculator calculator, IndexDefinition defn ) { return new IndexUsage(context, calculator, defn) { @Override protected boolean applies( FullTextSearch search ) { // We don't support full text search criteria ... return false; } @Override protected boolean indexAppliesTo( Comparison constraint ) { if (QueryObjectModelConstants.JCR_OPERATOR_LIKE.equals(constraint.getOperator())) { // Our indexes don't handle LIKE operations ... return false; } return super.indexAppliesTo(constraint); } @Override protected boolean applies( ChildCount operand ) { // this index can't handle this return false; } }; } }