Package org.exoplatform.services.jcr.impl.core.query.lucene

Source Code of org.exoplatform.services.jcr.impl.core.query.lucene.SearchIndex

/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements.  See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.exoplatform.services.jcr.impl.core.query.lucene;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.jcr.RepositoryException;
import javax.jcr.query.InvalidQueryException;

import org.apache.commons.collections.iterators.AbstractIteratorDecorator;
import org.exoplatform.services.log.Log;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.MultiReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TermDocs;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;

import org.exoplatform.container.configuration.ConfigurationManager;
import org.exoplatform.services.document.DocumentReaderService;
import org.exoplatform.services.jcr.config.QueryHandlerEntry;
import org.exoplatform.services.jcr.config.QueryHandlerEntryWrapper;
import org.exoplatform.services.jcr.config.RepositoryConfigurationException;
import org.exoplatform.services.jcr.dataflow.ItemDataConsumer;
import org.exoplatform.services.jcr.datamodel.InternalQName;
import org.exoplatform.services.jcr.datamodel.ItemData;
import org.exoplatform.services.jcr.datamodel.NodeData;
import org.exoplatform.services.jcr.impl.Constants;
import org.exoplatform.services.jcr.impl.core.LocationFactory;
import org.exoplatform.services.jcr.impl.core.SessionDataManager;
import org.exoplatform.services.jcr.impl.core.SessionImpl;
import org.exoplatform.services.jcr.impl.core.query.DefaultQueryNodeFactory;
import org.exoplatform.services.jcr.impl.core.query.ErrorLog;
import org.exoplatform.services.jcr.impl.core.query.ExecutableQuery;
import org.exoplatform.services.jcr.impl.core.query.QueryHandler;
import org.exoplatform.services.jcr.impl.core.query.QueryHandlerContext;
import org.exoplatform.services.log.ExoLogger;

/**
* Implements a {@link org.apache.jackrabbit.core.query.QueryHandler} using
* Lucene.
*/
public class SearchIndex implements QueryHandler
{

   private static final DefaultQueryNodeFactory DEFAULT_QUERY_NODE_FACTORY = new DefaultQueryNodeFactory();

   /** The logger instance for this class */
   private static final Log log = ExoLogger.getLogger(SearchIndex.class);

   /**
    * Name of the file to persist search internal namespace mappings.
    */
   private static final String NS_MAPPING_FILE = "ns_mappings.properties";

   /**
    * Default name of the error log file
    */
   private static final String ERROR_LOG = "error.log";

   /**
    * Indicates if this <code>SearchIndex</code> is closed and cannot be used
    * anymore.
    */
   private boolean closed = false;

   private QueryHandlerContext context;

   /**
    * Text extractor for extracting text content of binary properties.
    */
   private DocumentReaderService extractor;

   /**
    * The actual index
    */
   private MultiIndex index;

   /**
    * Indicates the index format version which is relevant to a <b>query</b>.
    * This value may be different from what
    * {@link MultiIndex#getIndexFormatVersion()} returns because queries may be
    * executed on two physical indexes with different formats. Index format
    * versions are considered backward compatible. That is, the lower version of
    * the two physical indexes is used for querying.
    */
   private IndexFormatVersion indexFormatVersion;

   /**
    * The indexing configuration.
    */
   private IndexingConfiguration indexingConfig;

   /**
    * The name and path resolver used internally.
    */
   private LocationFactory npResolver;

   /**
    * The namespace mappings used internally.
    */
   private NamespaceMappings nsMappings;

   private final QueryHandlerEntryWrapper queryHandlerConfig;

   /**
    * The spell checker for this query handler or <code>null</code> if none is
    * configured.
    */
   private SpellChecker spellChecker;

   /**
    * The currently set synonym provider.
    */
   private SynonymProvider synProvider;

   private File indexDirectory;

   /**
    * The ErrorLog of this <code>MultiIndex</code>. All changes that must be in
    * index but interrupted by IOException are here.
    */
   private ErrorLog errorLog;

   private final ConfigurationManager cfm;

   public SearchIndex(QueryHandlerEntry queryHandlerConfig, ConfigurationManager cfm)
   {
      this.queryHandlerConfig = new QueryHandlerEntryWrapper(queryHandlerConfig);
      this.cfm = cfm;
   }

   /**
    * Adds the <code>node</code> to the search index.
    *
    * @param node the node to add.
    * @throws RepositoryException if an error occurs while indexing the node.
    * @throws IOException if an error occurs while adding the node to the index.
    */
   public void addNode(NodeData node) throws RepositoryException, IOException
   {
      throw new UnsupportedOperationException("addNode");
   }

   /**
    * Creates an excerpt provider for the given <code>query</code>.
    *
    * @param query the query.
    * @return an excerpt provider for the given <code>query</code>.
    * @throws IOException if the provider cannot be created.
    */
   public ExcerptProvider createExcerptProvider(Query query) throws IOException
   {
      ExcerptProvider ep = queryHandlerConfig.createExcerptProvider(query);
      ep.init(query, this);
      return ep;
   }

   /**
    * Creates a new query by specifying the query statement itself and the
    * language in which the query is stated. If the query statement is
    * syntactically invalid, given the language specified, an
    * InvalidQueryException is thrown. <code>language</code> must specify a query
    * language string from among those returned by
    * QueryManager.getSupportedQueryLanguages(); if it is not then an
    * <code>InvalidQueryException</code> is thrown.
    *
    * @param session the session of the current user creating the query object.
    * @param itemMgr the item manager of the current user.
    * @param statement the query statement.
    * @param language the syntax of the query statement.
    * @throws InvalidQueryException if statement is invalid or language is
    *           unsupported.
    * @return A <code>Query</code> object.
    */
   public ExecutableQuery createExecutableQuery(SessionImpl session, SessionDataManager itemMgr, String statement,
      String language) throws InvalidQueryException
   {
      QueryImpl query =
         new QueryImpl(session, itemMgr, this, getContext().getPropertyTypeRegistry(), statement, language,
            getQueryNodeFactory());
      query.setRespectDocumentOrder(queryHandlerConfig.getDocumentOrder());
      return query;
   }

   public org.exoplatform.services.jcr.impl.core.query.AbstractQueryImpl createQueryInstance()
      throws RepositoryException
   {
      try
      {
         Object obj = Class.forName(queryHandlerConfig.getQueryClass()).newInstance();
         if (obj instanceof org.exoplatform.services.jcr.impl.core.query.AbstractQueryImpl)
         {
            return (org.exoplatform.services.jcr.impl.core.query.AbstractQueryImpl)obj;
         }
         throw new IllegalArgumentException(queryHandlerConfig.getQueryClass() + " is not of type "
            + AbstractQueryImpl.class.getName());

      }
      catch (Throwable t)
      {
         throw new RepositoryException("Unable to create query: " + t.toString());
      }
   }

   /**
    * Removes the node with <code>uuid</code> from the search index.
    *
    * @param id the id of the node to remove from the index.
    * @throws IOException if an error occurs while removing the node from the
    *           index.
    */
   public void deleteNode(String id) throws IOException
   {
      throw new UnsupportedOperationException("deleteNode");
   }

   public QueryHits executeQuery(Query query, boolean needsSystemTree, InternalQName[] orderProps, boolean[] orderSpecs)
      throws IOException
   {
      checkOpen();
      SortField[] sortFields = createSortFields(orderProps, orderSpecs);

      IndexReader reader = getIndexReader(needsSystemTree);
      IndexSearcher searcher = new IndexSearcher(reader);
      Hits hits;
      if (sortFields.length > 0)
      {
         hits = searcher.search(query, new Sort(sortFields));
      }
      else
      {
         hits = searcher.search(query);
      }
      return new QueryHits(hits, reader);
   }

   /**
    * Returns the context for this query handler.
    *
    * @return the <code>QueryHandlerContext</code> instance for this
    *         <code>QueryHandler</code>.
    */
   public QueryHandlerContext getContext()
   {
      return context;
   }

   /**
    * Returns the index format version that this search index is able to support
    * when a query is executed on this index.
    *
    * @return the index format version for this search index.
    */
   public IndexFormatVersion getIndexFormatVersion()
   {
      if (indexFormatVersion == null)
      {
         if (getContext().getParentHandler() instanceof SearchIndex)
         {
            SearchIndex parent = (SearchIndex)getContext().getParentHandler();
            if (parent.getIndexFormatVersion().getVersion() < index.getIndexFormatVersion().getVersion())
            {
               indexFormatVersion = parent.getIndexFormatVersion();
            }
            else
            {
               indexFormatVersion = index.getIndexFormatVersion();
            }
         }
         else
         {
            indexFormatVersion = index.getIndexFormatVersion();
         }
      }
      return indexFormatVersion;
   }

   /**
    * @return the indexing configuration or <code>null</code> if there is none.
    */
   public IndexingConfiguration getIndexingConfig()
   {
      return indexingConfig;
   }

   /**
    * Returns an index reader for this search index. The caller of this method is
    * responsible for closing the index reader when he is finished using it.
    *
    * @return an index reader for this search index.
    * @throws IOException the index reader cannot be obtained.
    */
   public IndexReader getIndexReader() throws IOException
   {
      return getIndexReader(true);
   }

   // --------------------------< properties >----------------------------------

   /**
    * Returns an index reader for this search index. The caller of this method is
    * responsible for closing the index reader when he is finished using it.
    *
    * @param includeSystemIndex if <code>true</code> the index reader will cover
    *          the complete workspace. If <code>false</code> the returned index
    *          reader will not contains any nodes under /jcr:system.
    * @return an index reader for this search index.
    * @throws IOException the index reader cannot be obtained.
    */
   public IndexReader getIndexReader(boolean includeSystemIndex) throws IOException
   {
      QueryHandler parentHandler = getContext().getParentHandler();
      CachingMultiIndexReader parentReader = null;
      if (parentHandler instanceof SearchIndex && includeSystemIndex)
      {
         parentReader = ((SearchIndex)parentHandler).index.getIndexReader();
      }

      CachingMultiIndexReader reader = index.getIndexReader();
      if (parentReader != null)
      {
         CachingMultiIndexReader[] readers = {reader, parentReader};
         return new CombinedIndexReader(readers);
      }
      return reader;

   }

   /**
    * Returns the namespace mappings for the internal representation.
    *
    * @return the namespace mappings for the internal representation.
    */
   public NamespaceMappings getNamespaceMappings()
   {
      return nsMappings;
   }

   /**
    * @return the spell checker of this search index. If none is configured this
    *         method returns <code>null</code>.
    */
   public SpellChecker getSpellChecker()
   {
      return spellChecker;
   }

   /**
    * @return the synonym provider of this search index. If none is set for this
    *         search index the synonym provider of the parent handler is returned
    *         if there is any.
    */
   public SynonymProvider getSynonymProvider()
   {
      if (synProvider != null)
      {
         return synProvider;
      }
      QueryHandler handler = getContext().getParentHandler();
      if (handler instanceof SearchIndex)
         return ((SearchIndex)handler).getSynonymProvider();
      return null;

   }

   /**
    * Returns the analyzer in use for indexing.
    *
    * @return the analyzer in use for indexing.
    */
   public Analyzer getTextAnalyzer()
   {
      return queryHandlerConfig.getAnalyzer();
   }

   /**
    * Initializes this query handler by setting all properties in this class with
    * appropriate parameter values.
    *
    * @param context the context for this query handler.
    */
   public final void setContext(QueryHandlerContext queryHandlerContext) throws IOException
   {
      this.context = queryHandlerContext;
   }

   /**
    * Initializes this <code>QueryHandler</code>. This implementation requires
    * that a path parameter is set in the configuration. If this condition is not
    * met, a <code>IOException</code> is thrown.
    *
    * @throws IOException if an error occurs while initializing this handler.
    */
   public void init()
   {
      try
      {
         String indexDir = context.getIndexDirectory();
         if (indexDir != null)
         {
            indexDir = indexDir.replace("${java.io.tmpdir}", System.getProperty("java.io.tmpdir"));
            indexDirectory = new File(indexDir);
            if (!indexDirectory.exists())
               if (!indexDirectory.mkdirs())
                  throw new RepositoryException("fail to create index dir " + indexDir);
         }
         else
         {
            throw new IOException("SearchIndex requires 'path' parameter in configuration!");
         }

         extractor = context.getExtractor();
         synProvider = queryHandlerConfig.createSynonymProvider(cfm);
         // File indexDirFile = context.getFileSystem();

         if (context.getParentHandler() instanceof SearchIndex)
         {
            // use system namespace mappings
            SearchIndex sysIndex = (SearchIndex)context.getParentHandler();
            nsMappings = sysIndex.getNamespaceMappings();
         }
         else
         {
            // read local namespace mappings
            File mapFile = new File(indexDirectory, NS_MAPPING_FILE);
            if (mapFile.exists())
            {
               // be backward compatible and use ns_mappings.properties from
               // index folder
               nsMappings = new FileBasedNamespaceMappings(mapFile);
            }
            else
            {
               // otherwise use repository wide stable index prefix from
               // namespace registry
               nsMappings = new NSRegistryBasedNamespaceMappings(context.getNamespaceRegistry());
            }
         }
         npResolver = new LocationFactory(nsMappings);

         indexingConfig = queryHandlerConfig.createIndexingConfiguration(nsMappings, context, cfm);

         queryHandlerConfig.getAnalyzer().setIndexingConfig(indexingConfig);

         index = new MultiIndex(indexDirectory, this/* , excludedIDs */, nsMappings);

         if (index.numDocs() == 0)
         {
            index.createInitialIndex(context.getItemStateManager(), context.getRootNodeIdentifer());
         }
         if (queryHandlerConfig.isConsistencyCheckEnabled()
            && (index.getRedoLogApplied() || queryHandlerConfig.isForceConsistencyCheck()))
         {
            log.info("Running consistency check... ");

            ConsistencyCheck check = ConsistencyCheck.run(index, context.getItemStateManager());
            if (queryHandlerConfig.getAutoRepair())
            {
               check.repair(true);
            }
            else
            {
               List<ConsistencyCheckError> errors = check.getErrors();
               if (errors.size() == 0)
               {
                  log.info("No errors detected.");
               }
               for (Iterator<ConsistencyCheckError> it = errors.iterator(); it.hasNext();)
               {
                  ConsistencyCheckError err = it.next();
                  log.info(err.toString());
               }
            }
         }

         // initialize spell checker
         spellChecker = queryHandlerConfig.createSpellChecker(this);

         log.info("Index initialized: " + queryHandlerConfig.getIndexDir() + " Version: "
            + index.getIndexFormatVersion() + "");

         File file = new File(indexDir, ERROR_LOG);
         errorLog = new ErrorLog(file, queryHandlerConfig.getErrorLogSize());
         // reprocess any notfinished notifies;
         recoverErrorLog(errorLog);

      }
      catch (IOException e)
      {
         log.error(e.getLocalizedMessage());
         throw new RuntimeException(e);
      }
      catch (RepositoryException e)
      {
         log.error(e.getLocalizedMessage());
         throw new RuntimeException(e);
      }
      catch (RepositoryConfigurationException e)
      {
         log.error(e.getLocalizedMessage());
         throw new RuntimeException(e);
      }
   }

   private void recoverErrorLog(ErrorLog errlog) throws IOException, RepositoryException
   {
      final Set<String> rem = new HashSet<String>();
      final Set<String> add = new HashSet<String>();

      errlog.readChanges(rem, add);

      // check is any notifies in log
      if (rem.isEmpty() && add.isEmpty())
      {
         // there is no sense to continue
         return;
      }

      Iterator<String> removedStates = rem.iterator();

      // make a new iterator;
      Iterator<NodeData> addedStates = new Iterator<NodeData>()
      {
         private final Iterator<String> iter = add.iterator();

         public boolean hasNext()
         {
            return iter.hasNext();
         }

         public NodeData next()
         {
            String id;
            // we have to iterrate through items till will meet ones existing in
            // workspace
            while (iter.hasNext())
            {
               id = iter.next();

               try
               {
                  ItemData item = context.getItemStateManager().getItemData(id);
                  if (item != null)
                  {
                     if (item.isNode())
                     {
                        return (NodeData)item; // return node here
                     }
                     else
                        log.warn("Node expected but property found with id " + id + ". Skipping "
                           + item.getQPath().getAsString());
                  }
                  else
                  {
                     log.warn("Unable to recovery node index " + id + ". Node not found.");
                  }
               }
               catch (RepositoryException e)
               {
                  log.error("ErrorLog recovery error. Item id " + id + ". " + e, e);
               }
            }

            return null;
         }

         public void remove()
         {
            throw new UnsupportedOperationException();
         }
      };

      updateNodes(removedStates, addedStates);

      errlog.clear();
   }

   /**
    * Closes this <code>QueryHandler</code> and frees resources attached to this
    * handler.
    */
   public void close()
   {
      if (spellChecker != null)
      {
         spellChecker.close();
      }
      index.close();
      getContext().destroy();
      closed = true;

      log.info("Index closed: " + indexDirectory.getAbsolutePath());
   }

   /**
    * This implementation forwards the call to
    * {@link MultiIndex#update(java.util.Iterator, java.util.Iterator)} and
    * transforms the two iterators to the required types.
    *
    * @param remove uuids of nodes to remove.
    * @param add NodeStates to add. Calls to <code>next()</code> on this iterator
    *          may return <code>null</code>, to indicate that a node could not be
    *          indexed successfully.
    * @throws RepositoryException if an error occurs while indexing a node.
    * @throws IOException if an error occurs while updating the index.
    */
   public void updateNodes(final Iterator<String> remove, final Iterator<NodeData> add) throws RepositoryException,
      IOException
   {

      checkOpen();

      final Map<String, NodeData> aggregateRoots = new HashMap<String, NodeData>();
      final Set<String> removedNodeIds = new HashSet<String>();
      final Set<String> addedNodeIds = new HashSet<String>();

      index.update(new AbstractIteratorDecorator(remove)
      {
         public Object next()
         {
            String nodeId = (String)super.next();
            removedNodeIds.add(nodeId);
            return nodeId;
         }
      }, new AbstractIteratorDecorator(add)
      {
         public Object next()
         {
            NodeData state = (NodeData)super.next();
            if (state == null)
            {
               return null;
            }
            addedNodeIds.add(state.getIdentifier());
            removedNodeIds.remove(state.getIdentifier());
            Document doc = null;
            try
            {
               doc = createDocument(state, getNamespaceMappings(), index.getIndexFormatVersion());
               retrieveAggregateRoot(state, aggregateRoots);
            }
            catch (RepositoryException e)
            {
               log
                  .warn("Exception while creating document for node: " + state.getIdentifier() + ": " + e.toString(), e);

            }
            return doc;
         }
      });

      // remove any aggregateRoot nodes that are new
      // and therefore already up-to-date
      aggregateRoots.keySet().removeAll(addedNodeIds);

      // based on removed NodeIds get affected aggregate root nodes
      retrieveAggregateRoot(removedNodeIds, aggregateRoots);

      // update aggregates if there are any affected
      if (aggregateRoots.size() > 0)
      {
         index.update(new AbstractIteratorDecorator(aggregateRoots.keySet().iterator())
         {
            public Object next()
            {
               return super.next();
            }
         }, new AbstractIteratorDecorator(aggregateRoots.values().iterator())
         {
            public Object next()
            {
               NodeData state = (NodeData)super.next();
               try
               {
                  return createDocument(state, getNamespaceMappings(), index.getIndexFormatVersion());
               }
               catch (RepositoryException e)
               {
                  log
                     .warn("Exception while creating document for node: " + state.getIdentifier() + ": " + e.toString());
               }
               return null;
            }
         });
      }

   }

   /**
    * Creates a lucene <code>Document</code> for a node state using the namespace
    * mappings <code>nsMappings</code>.
    *
    * @param node the node state to index.
    * @param nsMappings the namespace mappings of the search index.
    * @param indexFormatVersion the index format version that should be used to
    *          index the passed node state.
    * @return a lucene <code>Document</code> that contains all properties of
    *         <code>node</code>.
    * @throws RepositoryException if an error occurs while indexing the
    *           <code>node</code>.
    */
   protected Document createDocument(NodeData node, NamespaceMappings nsMappings, IndexFormatVersion indexFormatVersion)
      throws RepositoryException
   {
      NodeIndexer indexer = new NodeIndexer(node, getContext().getItemStateManager(), nsMappings, extractor);
      indexer.setSupportHighlighting(queryHandlerConfig.getSupportHighlighting());
      indexer.setIndexingConfiguration(indexingConfig);
      indexer.setIndexFormatVersion(indexFormatVersion);
      Document doc = indexer.createDoc();
      mergeAggregatedNodeIndexes(node, doc);
      return doc;
   }

   // ----------------------------< internal >----------------------------------

   /**
    * Creates the SortFields for the order properties.
    *
    * @param orderProps the order properties.
    * @param orderSpecs the order specs for the properties.
    * @return an array of sort fields
    */
   protected SortField[] createSortFields(InternalQName[] orderProps, boolean[] orderSpecs)
   {
      List<SortField> sortFields = new ArrayList<SortField>();
      for (int i = 0; i < orderProps.length; i++)
      {
         String prop = null;
         if (Constants.JCR_SCORE.equals(orderProps[i]))
         {
            // order on jcr:score does not use the natural order as
            // implemented in lucene. score ascending in lucene means that
            // higher scores are first. JCR specs that lower score values
            // are first.
            sortFields.add(new SortField(null, SortField.SCORE, orderSpecs[i]));
         }
         else
         {
            try
            {
               prop = npResolver.createJCRName(orderProps[i]).getAsString();
            }
            catch (RepositoryException e)
            {
               e.printStackTrace();
               // will never happen
            }
            sortFields.add(new SortField(prop, SharedFieldSortComparator.PROPERTIES, !orderSpecs[i]));
         }
      }
      return sortFields.toArray(new SortField[sortFields.size()]);
   }

   /**
    * Returns the actual index.
    *
    * @return the actual index.
    */
   protected MultiIndex getIndex()
   {
      return index;
   }

   /**
    * This method returns the QueryNodeFactory used to parse Queries. This method
    * may be overridden to provide a customized QueryNodeFactory
    */
   protected DefaultQueryNodeFactory getQueryNodeFactory()
   {
      return DEFAULT_QUERY_NODE_FACTORY;
   }

   /**
    * Merges the fulltext indexed fields of the aggregated node states into
    * <code>doc</code>.
    *
    * @param state the node state on which <code>doc</code> was created.
    * @param doc the lucene document with index fields from <code>state</code>.
    */
   protected void mergeAggregatedNodeIndexes(NodeData state, Document doc)
   {
      if (indexingConfig != null)
      {
         AggregateRule aggregateRules[] = indexingConfig.getAggregateRules();
         if (aggregateRules == null)
         {
            return;
         }
         try
         {
            for (int i = 0; i < aggregateRules.length; i++)
            {
               NodeData[] aggregates = aggregateRules[i].getAggregatedNodeStates(state);
               if (aggregates == null)
               {
                  continue;
               }
               for (int j = 0; j < aggregates.length; j++)
               {
                  Document aDoc = createDocument(aggregates[j], getNamespaceMappings(), index.getIndexFormatVersion());
                  // transfer fields to doc if there are any
                  Field[] fulltextFields = aDoc.getFields(FieldNames.FULLTEXT);
                  if (fulltextFields != null)
                  {
                     for (int k = 0; k < fulltextFields.length; k++)
                     {
                        doc.add(fulltextFields[k]);
                     }
                     doc.add(new Field(FieldNames.AGGREGATED_NODE_UUID, aggregates[j].getIdentifier().toString(),
                        Field.Store.NO, Field.Index.NO_NORMS));
                  }
               }
               // only use first aggregate definition that matches
               break;
            }
         }
         catch (Exception e)
         {
            // do not fail if aggregate cannot be created
            log
               .warn("Exception while building indexing aggregate for " + "node with UUID: " + state.getIdentifier(), e);
         }
      }
   }

   /**
    * Retrieves the root of the indexing aggregate for <code>state</code> and
    * puts it into <code>map</code>.
    *
    * @param state the node state for which we want to retrieve the aggregate
    *          root.
    * @param map aggregate roots are collected in this map. Key=NodeId,
    *          value=NodeState.
    */
   protected void retrieveAggregateRoot(NodeData state, Map<String, NodeData> map)
   {
      if (indexingConfig != null)
      {
         AggregateRule aggregateRules[] = indexingConfig.getAggregateRules();
         if (aggregateRules == null)
         {
            return;
         }
         try
         {
            for (int i = 0; i < aggregateRules.length; i++)
            {
               NodeData root = aggregateRules[i].getAggregateRoot(state);
               if (root != null)
               {
                  map.put(root.getIdentifier(), root);
                  break;
               }
            }
         }
         catch (Exception e)
         {
            log.warn("Unable to get aggregate root for " + state.getIdentifier(), e);
         }
      }
   }

   /**
    * Retrieves the root of the indexing aggregate for
    * <code>removedNodeIds</code> and puts it into <code>map</code>.
    *
    * @param removedNodeIds the ids of removed nodes.
    * @param map aggregate roots are collected in this map. Key=NodeId,
    *          value=NodeState.
    */
   protected void retrieveAggregateRoot(Set<String> removedNodeIds, Map<String, NodeData> map)
   {
      if (indexingConfig != null)
      {
         AggregateRule aggregateRules[] = indexingConfig.getAggregateRules();
         if (aggregateRules == null)
         {
            return;
         }
         int found = 0;
         long time = System.currentTimeMillis();
         try
         {
            IndexReader reader = index.getIndexReader();
            try
            {
               Term aggregateUUIDs = new Term(FieldNames.AGGREGATED_NODE_UUID, "");
               TermDocs tDocs = reader.termDocs();
               try
               {
                  ItemDataConsumer ism = getContext().getItemStateManager();
                  for (Iterator<String> it = removedNodeIds.iterator(); it.hasNext();)
                  {
                     String id = it.next();
                     aggregateUUIDs = aggregateUUIDs.createTerm(id);
                     tDocs.seek(aggregateUUIDs);
                     while (tDocs.next())
                     {
                        Document doc = reader.document(tDocs.doc());
                        String uuid = doc.get(FieldNames.UUID);
                        ItemData itd = ism.getItemData(uuid);
                        if (itd == null)
                           continue;
                        if (!itd.isNode())
                           throw new RepositoryException("Item with id:" + uuid + " is not a node");
                        map.put(uuid, (NodeData)itd);
                        found++;
                     }
                  }
               }
               finally
               {
                  tDocs.close();
               }
            }
            finally
            {
               reader.close();
            }
         }
         catch (Exception e)
         {
            log.warn("Exception while retrieving aggregate roots", e);
         }
         time = System.currentTimeMillis() - time;
         log.debug("Retrieved " + new Integer(found) + " aggregate roots in " + new Long(time) + " ms.");
      }
   }

   /**
    * Checks if this <code>SearchIndex</code> is open, otherwise throws an
    * <code>IOException</code>.
    *
    * @throws IOException if this <code>SearchIndex</code> had been closed.
    */
   private void checkOpen() throws IOException
   {
      if (closed)
      {
         throw new IOException("query handler closed and cannot be used anymore.");
      }
   }

   /**
    * Combines multiple {@link CachingMultiIndexReader} into a
    * <code>MultiReader</code> with {@link HierarchyResolver} support.
    */
   protected static final class CombinedIndexReader extends MultiReader implements HierarchyResolver, MultiIndexReader
   {

      /**
       * Doc number starts for each sub reader
       */
      private int[] starts;

      /**
       * The sub readers.
       */
      final private CachingMultiIndexReader[] subReaders;

      public CombinedIndexReader(CachingMultiIndexReader[] indexReaders) throws IOException
      {
         super(indexReaders);
         this.subReaders = indexReaders;
         this.starts = new int[subReaders.length + 1];

         int maxDoc = 0;
         for (int i = 0; i < subReaders.length; i++)
         {
            starts[i] = maxDoc;
            maxDoc += subReaders[i].maxDoc();
         }
         starts[subReaders.length] = maxDoc;
      }

      /**
       * {@inheritDoc}
       */
      public ForeignSegmentDocId createDocId(String uuid) throws IOException
      {
         for (int i = 0; i < subReaders.length; i++)
         {
            CachingMultiIndexReader subReader = subReaders[i];
            ForeignSegmentDocId doc = subReader.createDocId(uuid);
            if (doc != null)
            {
               return doc;
            }
         }
         return null;
      }

      // -------------------------< MultiIndexReader >-------------------------

      public boolean equals(Object obj)
      {
         if (obj instanceof CombinedIndexReader)
         {
            CombinedIndexReader other = (CombinedIndexReader)obj;
            return Arrays.equals(subReaders, other.subReaders);
         }
         return false;
      }

      // ---------------------------< internal >-------------------------------

      /**
       * {@inheritDoc}
       */
      public int getDocumentNumber(ForeignSegmentDocId docId)
      {
         for (int i = 0; i < subReaders.length; i++)
         {
            CachingMultiIndexReader subReader = subReaders[i];
            int realDoc = subReader.getDocumentNumber(docId);
            if (realDoc >= 0)
            {
               return realDoc;
            }
         }
         return -1;
      }

      /**
       * {@inheritDoc}
       */
      public IndexReader[] getIndexReaders()
      {
         IndexReader readers[] = new IndexReader[subReaders.length];
         System.arraycopy(subReaders, 0, readers, 0, subReaders.length);
         return readers;
      }

      /**
       * @inheritDoc
       */
      public int getParent(int n) throws IOException
      {
         int i = readerIndex(n);
         DocId id = subReaders[i].getParentDocId(n - starts[i]);
         id = id.applyOffset(starts[i]);
         return id.getDocumentNumber(this);
      }

      public int hashCode()
      {
         int hash = 0;
         for (int i = 0; i < subReaders.length; i++)
         {
            hash = 31 * hash + subReaders[i].hashCode();
         }
         return hash;
      }

      /**
       * Returns the reader index for document <code>n</code>. Implementation
       * copied from lucene MultiReader class.
       *
       * @param n document number.
       * @return the reader index.
       */
      private int readerIndex(int n)
      {
         int lo = 0; // search starts array
         int hi = subReaders.length - 1; // for first element less

         while (hi >= lo)
         {
            int mid = (lo + hi) >> 1;
            int midValue = starts[mid];
            if (n < midValue)
            {
               hi = mid - 1;
            }
            else if (n > midValue)
            {
               lo = mid + 1;
            }
            else
            { // found a match
               while (mid + 1 < subReaders.length && starts[mid + 1] == midValue)
               {
                  mid++; // scan to last match
               }
               return mid;
            }
         }
         return hi;
      }
   }

   public QueryHandlerEntryWrapper getQueryHandlerConfig()
   {
      return queryHandlerConfig;
   }

   /**
    * Log unindexed changes into error.log
    *
    * @param removed set of removed node uuids
    * @param added map of added node states and uuids
    * @throws IOException
    */
   public void logErrorChanges(Set<String> removed, Set<String> added) throws IOException
   {
      // backup the remove and add iterators
      errorLog.writeChanges(removed, added);
   }

}
TOP

Related Classes of org.exoplatform.services.jcr.impl.core.query.lucene.SearchIndex

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.