/* * 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.lucene; import java.io.IOException; import java.math.BigDecimal; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.jcr.Binary; import javax.jcr.RepositoryException; import javax.jcr.query.qom.Constraint; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.LegacyDoubleField; import org.apache.lucene.document.LegacyIntField; import org.apache.lucene.document.LegacyLongField; import org.apache.lucene.document.StringField; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexWriter; import org.modeshape.common.annotation.Immutable; import org.modeshape.common.annotation.ThreadSafe; import org.modeshape.common.logging.Logger; import org.modeshape.common.util.CheckArg; import org.modeshape.jcr.ExecutionContext; import org.modeshape.jcr.api.value.DateTime; import org.modeshape.jcr.index.lucene.query.LuceneQueryFactory; import org.modeshape.jcr.spi.index.IndexConstraints; import org.modeshape.jcr.spi.index.provider.ProvidedIndex; import org.modeshape.jcr.value.PropertyType; import org.modeshape.jcr.value.StringFactory; /** * Bases class for indexes stored in Lucene * * @author Horia Chiorean (hchiorea@redhat.com) * @since 4.5 */ @ThreadSafe @Immutable @SuppressWarnings("deprecation") public abstract class LuceneIndex implements ProvidedIndex<Object> { protected final Logger logger = Logger.getLogger(getClass()); protected final String name; protected final ExecutionContext context; protected final IndexWriter writer; protected final Map<String, PropertyType> propertyTypesByName; protected final LuceneConfig config; protected final StringFactory stringFactory; protected final Searcher searcher; protected LuceneIndex( String name, String workspaceName, LuceneConfig config, Map<String, PropertyType> propertyTypesByName, ExecutionContext context ) { assert !propertyTypesByName.isEmpty(); this.propertyTypesByName = propertyTypesByName; this.name = name; this.context = context; this.stringFactory = context.getValueFactories().getStringFactory(); this.config = config; this.writer = config.newWriter(workspaceName, name); this.searcher = new Searcher(config, writer, name); } @Override public void add( String nodeKey, String propertyName, Object value ) { add(nodeKey, propertyName, new Object[]{value}); } @Override public void remove( String nodeKey ) { CheckArg.isNotNull(nodeKey, "nodeKey"); try { // mark the nodekey as removed writer.deleteDocuments(FieldUtil.idTerm(nodeKey)); } catch (IOException e) { throw new LuceneIndexException(e); } } @Override public void remove( String nodeKey, String propertyName, Object value ) { // we don't really care about the value... remove(nodeKey, propertyName); } @Override public void remove( String nodeKey, String propertyName, Object[] values ) { // we don't really care about the values... remove(nodeKey, propertyName); } @Override public String getName() { return name; } @Override public long estimateCardinality( List<Constraint> andedConstraints, Map<String, Object> variables ) { try { return searcher.estimateCardinality(andedConstraints, queryFactory(variables)); } catch (IOException e) { throw new LuceneIndexException(e); } } @Override public long estimateTotalCount() { return writer.numDocs(); } @Override public Results filter(IndexConstraints constraints, long cardinalityEstimate) { return searcher.filter(constraints, queryFactory(constraints.getVariables()), cardinalityEstimate); } @Override public boolean requiresReindexing() { try { return !DirectoryReader.indexExists(writer.getDirectory()); } catch (IOException e) { logger.debug(e, "cannot determine if lucene index exists..."); return false; } } public void commit() { if (!writer.hasUncommittedChanges()) { return; } try { Map<String, String> oldData = writer.getCommitData(); Map<String, String> newData = new HashMap<>(oldData); preCommit(newData); writer.setCommitData(newData); writer.commit(); postCommit(); } catch (IOException e) { throw new LuceneIndexException("Cannot commit index writer", e); } } protected void postCommit() { //nothing by default } protected void preCommit(Map<String, String> commitData) { commitData.put(LuceneConfig.LAST_SUCCESSFUL_COMMIT_TIME, String.valueOf(System.currentTimeMillis())); } public void shutdown( boolean destroyed ) { if (destroyed) { clearAllData(); } try { searcher.close(); writer.close(); } catch (IOException e) { throw new LuceneIndexException("Cannot shutdown lucene index", e); } } public void clearAllData() { try { writer.deleteAll(); writer.commit(); } catch (IOException e) { throw new LuceneIndexException("Cannot remove all documents from the index"); } } protected void addProperty( String nodeKey, Document document, String property, Object... values ) { if (values != null && values.length > 0) { // add the new fields for the given values to the document List<Field> fields = valuesToFields(property, values); for (Field field : fields) { document.add(field); } } // always add the ID (which in the case of an update is removed first) document.add(FieldUtil.idField(nodeKey)); } protected abstract void remove(final String nodeKey, final String propertyName); protected abstract LuceneQueryFactory queryFactory( Map<String, Object> variables ); protected List<Field> valuesToFields( String propertyName, Object... values ) { assert values.length > 0; PropertyType type = propertyTypesByName.get(propertyName); assert type != null; List<Field> fields = new ArrayList<>(); for (Object value : values) { switch (type) { case NAME: case PATH: case REFERENCE: case SIMPLEREFERENCE: case WEAKREFERENCE: case URI: case STRING: { addStringField(propertyName, stringFactory.create(value), fields); break; } case BOOLEAN: { addBooleanField(propertyName, (Boolean)value, fields); break; } case BINARY: { // don't cast to anything, because depending on the index type this may be a string or a binary addBinaryField(propertyName, value, fields); break; } case DATE: { addDateField(propertyName, ((DateTime)value), fields); break; } case DECIMAL: { addDecimalField(propertyName,(BigDecimal) value, fields); break; } case DOUBLE: { addDoubleField(propertyName, (Double) value, fields); break; } case LONG: { addLongField(propertyName, (Long) value, fields); break; } default: throw new LuceneIndexException("Unsupported property type: " + type); } } return fields; } protected void addStringField( String propertyName, String value, List<Field> fields ) { fields.add(new StringField(propertyName, value, Field.Store.YES)); fields.add(new LegacyLongField(FieldUtil.lengthField(propertyName), value.length(), Field.Store.YES)); } protected void addBooleanField( String propertyName, Boolean value, List<Field> fields ) { String valueString = stringFactory.create(value); int intValue = value ? 1 : 0; fields.add(new LegacyIntField(propertyName, intValue, Field.Store.YES)); // add the length fields.add(new LegacyLongField(FieldUtil.lengthField(propertyName), valueString.length(), Field.Store.YES)); } protected void addDateField( String propertyName, DateTime value, List<Field> fields ) { // dates are stored as millis fields.add(new LegacyLongField(propertyName, value.getMilliseconds(), Field.Store.YES)); // add the length String valueString = stringFactory.create(value); fields.add(new LegacyLongField(FieldUtil.lengthField(propertyName), valueString.length(), Field.Store.YES)); } protected void addBinaryField( String propertyName, Object value, List<Field> fields ) { // only the length is indexed for binary values by default.... try { Binary binary = (Binary) value; fields.add(new LegacyLongField(FieldUtil.lengthField(propertyName), binary.getSize(), Field.Store.YES)); } catch (RepositoryException e) { throw new LuceneIndexException(e); } } protected void addDecimalField( String propertyName, BigDecimal value, List<Field> fields ) { // big decimals are stored as string, because Lucene doesn't support these natively... String stringValue = FieldUtil.decimalToString(value); fields.add(new StringField(propertyName, stringValue, Field.Store.YES)); // add the length using the JCR string format fields.add(new LegacyLongField(FieldUtil.lengthField(propertyName), stringFactory.create(value).length(), Field.Store.YES)); } protected void addDoubleField( String propertyName, Double value, List<Field> fields ) { fields.add(new LegacyDoubleField(propertyName, value, Field.Store.YES)); // add the length String valueString = stringFactory.create(value); fields.add(new LegacyLongField(FieldUtil.lengthField(propertyName), valueString.length(), Field.Store.YES)); } protected void addLongField( String propertyName, Long value, List<Field> fields ) { fields.add(new LegacyLongField(propertyName, value.longValue(), Field.Store.YES)); // add the length String valueString = stringFactory.create(value); fields.add(new LegacyLongField(FieldUtil.lengthField(propertyName), valueString.length(), Field.Store.YES)); } }