/*
* 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.apache.jackrabbit.core;
import java.io.IOException;
import java.util.ArrayList;
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.NamespaceException;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.observation.Event;
import javax.jcr.observation.EventIterator;
import javax.jcr.query.InvalidQueryException;
import javax.jcr.query.Query;
import javax.jcr.query.qom.QueryObjectModel;
import org.apache.jackrabbit.core.config.SearchConfig;
import org.apache.jackrabbit.core.fs.FileSystem;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.observation.EventImpl;
import org.apache.jackrabbit.core.observation.SynchronousEventListener;
import org.apache.jackrabbit.core.persistence.PersistenceManager;
import org.apache.jackrabbit.core.query.AbstractQueryImpl;
import org.apache.jackrabbit.core.query.QueryHandler;
import org.apache.jackrabbit.core.query.QueryHandlerContext;
import org.apache.jackrabbit.core.query.QueryHandlerFactory;
import org.apache.jackrabbit.core.query.QueryObjectModelImpl;
import org.apache.jackrabbit.core.session.SessionContext;
import org.apache.jackrabbit.core.state.ItemStateException;
import org.apache.jackrabbit.core.state.NodeState;
import org.apache.jackrabbit.core.state.SharedItemStateManager;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException;
import org.apache.jackrabbit.spi.commons.query.qom.QueryObjectModelTree;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Acts as a global entry point to execute queries and index nodes.
*/
public class SearchManager implements SynchronousEventListener {
/**
* Logger instance for this class
*/
private static final Logger log = LoggerFactory.getLogger(SearchManager.class);
/**
* Namespace URI for xpath functions
*/
private static final String NS_FN_PREFIX = "fn";
public static final String NS_FN_URI = "http://www.w3.org/2005/xpath-functions";
/**
* Deprecated namespace URI for xpath functions
*/
private static final String NS_FN_OLD_PREFIX = "fn_old";
public static final String NS_FN_OLD_URI = "http://www.w3.org/2004/10/xpath-functions";
/**
* Namespace URI for XML schema
*/
private static final String NS_XS_PREFIX = "xs";
public static final String NS_XS_URI = "http://www.w3.org/2001/XMLSchema";
/**
* The shared item state manager instance for the workspace.
*/
private final SharedItemStateManager itemMgr;
/**
* QueryHandler where query execution is delegated to
*/
private QueryHandler handler;
/**
* QueryHandler of the parent search manager or <code>null</code> if there
* is none.
*/
private final QueryHandler parentHandler;
/**
* The namespace registry of the repository.
*/
private final NamespaceRegistryImpl nsReg;
/**
* Path that will be excluded from indexing.
*/
private Path excludePath;
/**
* Creates a new <code>SearchManager</code>.
*
* @param workspace the workspace name
* @param repositoryContext the repository context
* @param qhf the query handler factory
* @param itemMgr the shared item state manager.
* @param pm the underlying persistence manager.
* @param rootNodeId the id of the root node.
* @param parentMgr the parent search manager or <code>null</code> if
* there is no parent search manager.
* @param excludedNodeId id of the node that should be excluded from
* indexing. Any descendant of that node will also be
* excluded from indexing.
* @throws RepositoryException if the search manager cannot be initialized
*/
public SearchManager(
String workspace,
RepositoryContext repositoryContext,
QueryHandlerFactory qhf,
SharedItemStateManager itemMgr,
PersistenceManager pm,
NodeId rootNodeId,
SearchManager parentMgr,
NodeId excludedNodeId) throws RepositoryException {
this.nsReg = repositoryContext.getNamespaceRegistry();
this.itemMgr = itemMgr;
this.parentHandler = (parentMgr != null) ? parentMgr.handler : null;
// register namespaces
safeRegisterNamespace(NS_XS_PREFIX, NS_XS_URI);
try {
if (nsReg.getPrefix(NS_FN_OLD_URI).equals(NS_FN_PREFIX)) {
// old uri is mapped to 'fn' prefix -> re-map
String prefix = NS_FN_OLD_PREFIX;
try {
// Find a free prefix
for (int i = 2; true; i++) {
nsReg.getURI(prefix);
prefix = NS_FN_OLD_PREFIX + i;
}
} catch (NamespaceException e) {
// Re-map the old fn URI to that prefix
nsReg.registerNamespace(prefix, NS_FN_OLD_URI);
}
}
} catch (NamespaceException e) {
// does not yet exist
safeRegisterNamespace(NS_FN_OLD_PREFIX, NS_FN_OLD_URI);
}
// at this point the 'fn' prefix shouldn't be assigned anymore
safeRegisterNamespace(NS_FN_PREFIX, NS_FN_URI);
if (excludedNodeId != null) {
HierarchyManagerImpl hmgr =
new HierarchyManagerImpl(rootNodeId, itemMgr);
excludePath = hmgr.getPath(excludedNodeId);
}
// initialize query handler
this.handler = qhf.getQueryHandler(new QueryHandlerContext(workspace,
repositoryContext, itemMgr, pm, rootNodeId, parentHandler,
excludedNodeId));
}
/**
* Registers a namespace using the given prefix hint. Does nothing
* if the namespace is already registered. If the given prefix hint
* is not yet registered as a prefix, then it is used as the prefix
* of the registered namespace. Otherwise a unique prefix is generated
* based on the given hint.
*
* @param prefixHint the prefix hint
* @param uri the namespace URI
* @throws NamespaceException if an illegal attempt is made to register
* a mapping
* @throws RepositoryException if an unexpected error occurs
* @see javax.jcr.NamespaceRegistry#registerNamespace(String, String)
*/
private void safeRegisterNamespace(String prefixHint, String uri)
throws NamespaceException, RepositoryException {
try {
// Check if the namespace is already registered
nsReg.getPrefix(uri);
// ... it is, so do nothing.
} catch (NamespaceException e1) {
// ... it is not, try to find a unique prefix.
String prefix = prefixHint;
try {
for (int suffix = 2; true; suffix++) {
// Is this prefix already registered?
nsReg.getURI(prefix);
// ... it is, generate a new prefix and try again.
prefix = prefixHint + suffix;
}
} catch (NamespaceException e2) {
// ... it is not, register the namespace with this prefix.
nsReg.registerNamespace(prefix, uri);
}
}
}
/**
* Closes this <code>SearchManager</code> and also closes the
* {@link FileSystem} configured in {@link SearchConfig}.
*/
public void close() {
try {
shutdownQueryHandler();
} catch (IOException e) {
log.error("Exception closing QueryHandler.", e);
}
}
/**
* Creates a query object that can be executed on the workspace.
*
* @param sessionContext component context of the current session
* @param statement the actual query statement.
* @param language the syntax of the query statement.
* @param node a nt:query node where the query was read from or
* <code>null</code> if it is not a stored query.
* @return a <code>Query</code> instance to execute.
* @throws InvalidQueryException if the query is malformed or the
* <code>language</code> is unknown.
* @throws RepositoryException if any other error occurs.
*/
public Query createQuery(
SessionContext sessionContext,
String statement, String language, Node node)
throws InvalidQueryException, RepositoryException {
AbstractQueryImpl query = createQueryInstance();
query.init(sessionContext, handler, statement, language, node);
return query;
}
/**
* Creates a query object model that can be executed on the workspace.
*
* @param sessionContext component context of the current session
* @param qomTree the query object model tree, representing the query.
* @param langugage the original language of the query statement.
* @param node a nt:query node where the query was read from or
* <code>null</code> if it is not a stored query.
* @return the query object model for the query.
* @throws InvalidQueryException the the query object model tree is
* considered invalid by the query handler
* implementation.
* @throws RepositoryException if any other error occurs.
*/
public QueryObjectModel createQueryObjectModel(
SessionContext sessionContext, QueryObjectModelTree qomTree,
String langugage, Node node)
throws InvalidQueryException, RepositoryException {
QueryObjectModelImpl qom = new QueryObjectModelImpl();
qom.init(sessionContext, handler, qomTree, langugage, node);
return qom;
}
/**
* Returns the ids of the nodes that refer to the node with <code>id</code>
* by weak references.
*
* @param id the id of the target node.
* @return the ids of the referring nodes.
* @throws RepositoryException if an error occurs.
* @throws IOException if an error occurs while reading from the
* index.
*/
public Iterable<NodeId> getWeaklyReferringNodes(NodeId id)
throws RepositoryException, IOException {
return handler.getWeaklyReferringNodes(id);
}
/**
* Checks if the given event should be excluded based on the
* {@link #excludePath} setting.
*
* @param event observation event
* @return <code>true</code> if the event should be excluded,
* <code>false</code> otherwise
*/
private boolean isExcluded(EventImpl event) {
try {
return excludePath != null
&& excludePath.isAncestorOf(event.getQPath());
} catch (MalformedPathException ex) {
log.error("Error filtering events.", ex);
return false;
} catch (RepositoryException ex) {
log.error("Error filtering events.", ex);
return false;
}
}
//------------------------< for testing only >------------------------------
/**
* @return the query handler implementation.
*/
public QueryHandler getQueryHandler() {
return handler;
}
//---------------< EventListener interface >--------------------------------
public void onEvent(EventIterator events) {
log.debug("onEvent: indexing started");
long time = System.currentTimeMillis();
// nodes that need to be removed from the index.
final Set<NodeId> removedNodes = new HashSet<NodeId>();
// nodes that need to be added to the index.
final Map<NodeId, EventImpl> addedNodes = new HashMap<NodeId, EventImpl>();
// property events
List<EventImpl> propEvents = new ArrayList<EventImpl>();
while (events.hasNext()) {
EventImpl e = (EventImpl) events.nextEvent();
if (!isExcluded(e)) {
long type = e.getType();
if (type == Event.NODE_ADDED) {
addedNodes.put(e.getChildId(), e);
if (e.isShareableChildNode()) {
// simply re-index shareable nodes
removedNodes.add(e.getChildId());
}
} else if (type == Event.NODE_REMOVED) {
removedNodes.add(e.getChildId());
if (e.isShareableChildNode()) {
// check if there is a node remaining in the shared set
if (itemMgr.hasItemState(e.getChildId())) {
addedNodes.put(e.getChildId(), e);
}
}
} else {
propEvents.add(e);
}
}
}
// sort out property events
for (EventImpl e : propEvents) {
NodeId nodeId = e.getParentId();
if (e.getType() == Event.PROPERTY_ADDED) {
if (addedNodes.put(nodeId, e) == null) {
// only property added
// need to re-index
removedNodes.add(nodeId);
} else {
// the node where this prop belongs to is also new
}
} else if (e.getType() == Event.PROPERTY_CHANGED) {
// need to re-index
addedNodes.put(nodeId, e);
removedNodes.add(nodeId);
} else {
// property removed event is only generated when node still exists
addedNodes.put(nodeId, e);
removedNodes.add(nodeId);
}
}
Iterator<NodeState> addedStates = new Iterator<NodeState>() {
private final Iterator<NodeId> iter = addedNodes.keySet().iterator();
public void remove() {
throw new UnsupportedOperationException();
}
public boolean hasNext() {
return iter.hasNext();
}
public NodeState next() {
NodeState item = null;
NodeId id = (NodeId) iter.next();
try {
item = (NodeState) itemMgr.getItemState(id);
} catch (ItemStateException ise) {
// check whether this item state change originated from
// an external event
EventImpl e = addedNodes.get(id);
if (e == null || !e.isExternal()) {
log.error("Unable to index node " + id + ": does not exist");
} else {
log.info("Node no longer available " + id + ", skipped.");
}
}
return item;
}
};
Iterator<NodeId> removedIds = removedNodes.iterator();
if (removedNodes.size() > 0 || addedNodes.size() > 0) {
try {
handler.updateNodes(removedIds, addedStates);
} catch (RepositoryException e) {
log.error("Error indexing node.", e);
} catch (IOException e) {
log.error("Error indexing node.", e);
}
}
if (log.isDebugEnabled()) {
log.debug("onEvent: indexing finished in "
+ String.valueOf(System.currentTimeMillis() - time)
+ " ms.");
}
}
/**
* Creates a new instance of an {@link AbstractQueryImpl} which is not
* initialized.
*
* @return an new query instance.
* @throws RepositoryException if an error occurs while creating a new query
* instance.
*/
protected AbstractQueryImpl createQueryInstance() throws RepositoryException {
try {
String queryImplClassName = handler.getQueryClass();
Object obj = Class.forName(queryImplClassName).newInstance();
if (obj instanceof AbstractQueryImpl) {
return (AbstractQueryImpl) obj;
} else {
throw new IllegalArgumentException(queryImplClassName
+ " is not of type " + AbstractQueryImpl.class.getName());
}
} catch (Throwable t) {
throw new RepositoryException("Unable to create query: " + t.toString(), t);
}
}
//------------------------< internal >--------------------------------------
/**
* Shuts down the query handler. If the query handler is already shut down
* this method does nothing.
*
* @throws IOException if an error occurs while shutting down the query
* handler.
*/
private void shutdownQueryHandler() throws IOException {
if (handler != null) {
handler.close();
handler = null;
}
}
}