/*
* 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.query.lucene;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.jcr.NamespaceException;
import javax.jcr.RepositoryException;
import org.apache.jackrabbit.core.HierarchyManager;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.id.PropertyId;
import org.apache.jackrabbit.core.state.ChildNodeEntry;
import org.apache.jackrabbit.core.state.ItemStateException;
import org.apache.jackrabbit.core.state.ItemStateManager;
import org.apache.jackrabbit.core.state.NodeState;
import org.apache.jackrabbit.core.state.PropertyState;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException;
import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException;
import org.apache.jackrabbit.spi.commons.conversion.NameResolver;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.apache.jackrabbit.spi.commons.name.PathBuilder;
import org.apache.jackrabbit.util.Text;
import org.w3c.dom.CharacterData;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* <code>AggregateRule</code> defines a configuration for a node index
* aggregate. It defines rules for items that should be included in the node
* scope index of an ancestor. Per default the values of properties are only
* added to the node scope index of the parent node.
*/
class AggregateRuleImpl implements AggregateRule {
/**
* A name resolver for parsing QNames in the configuration.
*/
private final NameResolver resolver;
/**
* The node type of the root node of the indexing aggregate.
*/
private final Name nodeTypeName;
/**
* The node includes of this indexing aggregate.
*/
private final NodeInclude[] nodeIncludes;
/**
* The property includes of this indexing aggregate.
*/
private final PropertyInclude[] propertyIncludes;
/**
* The item state manager to retrieve additional item states.
*/
private final ItemStateManager ism;
/**
* A hierarchy resolver for the item state manager.
*/
private final HierarchyManager hmgr;
/**
* recursive aggregation (for same type nodes) default value.
*/
private static final boolean RECURSIVE_AGGREGATION_DEFAULT = false;
/**
* flag to enable recursive aggregation (for same type nodes).
*/
private final boolean recursiveAggregation;
/**
* recursive aggregation (for same type nodes) limit default value.
*/
protected static final long RECURSIVE_AGGREGATION_LIMIT_DEFAULT = 100;
/**
* recursive aggregation (for same type nodes) limit. embedded aggregation
* of nodes that have the same type can go only this levels up.
*
* A value eq to 0 gives unlimited aggregation.
*/
private final long recursiveAggregationLimit;
/**
* Creates a new indexing aggregate using the given <code>config</code>.
*
* @param config the configuration for this indexing aggregate.
* @param resolver the name resolver for parsing Names within the config.
* @param ism the item state manager of the workspace.
* @param hmgr a hierarchy manager for the item state manager.
* @throws MalformedPathException if a path in the configuration is
* malformed.
* @throws IllegalNameException if a node type name contains illegal
* characters.
* @throws NamespaceException if a node type contains an unknown
* prefix.
* @throws RepositoryException If another error occurs.
*/
AggregateRuleImpl(Node config,
NameResolver resolver,
ItemStateManager ism,
HierarchyManager hmgr) throws MalformedPathException,
IllegalNameException, NamespaceException, RepositoryException {
this.resolver = resolver;
this.nodeTypeName = getNodeTypeName(config);
this.nodeIncludes = getNodeIncludes(config);
this.propertyIncludes = getPropertyIncludes(config);
this.ism = ism;
this.hmgr = hmgr;
this.recursiveAggregation = getRecursiveAggregation(config);
this.recursiveAggregationLimit = getRecursiveAggregationLimit(config);
}
/**
* Returns root node state for the indexing aggregate where
* <code>nodeState</code> belongs to.
*
* @param nodeState the node state.
* @return the root node state of the indexing aggregate or
* <code>null</code> if <code>nodeState</code> does not belong to an
* indexing aggregate.
* @throws ItemStateException if an error occurs.
* @throws RepositoryException if an error occurs.
*/
public NodeState getAggregateRoot(NodeState nodeState)
throws ItemStateException, RepositoryException {
for (NodeInclude nodeInclude : nodeIncludes) {
NodeState aggregateRoot = nodeInclude.matches(nodeState);
if (aggregateRoot != null && aggregateRoot.getNodeTypeName().equals(nodeTypeName)) {
boolean sameNodeTypeAsRoot = nodeState.getNodeTypeName().equals(aggregateRoot.getNodeTypeName());
if(!sameNodeTypeAsRoot || (sameNodeTypeAsRoot && recursiveAggregation)){
return aggregateRoot;
}
}
}
// check property includes
for (PropertyInclude propertyInclude : propertyIncludes) {
NodeState aggregateRoot = propertyInclude.matches(nodeState);
if (aggregateRoot != null && aggregateRoot.getNodeTypeName().equals(nodeTypeName)) {
boolean sameNodeTypeAsRoot = nodeState.getNodeTypeName().equals(aggregateRoot.getNodeTypeName());
if(!sameNodeTypeAsRoot || (sameNodeTypeAsRoot && recursiveAggregation)){
return aggregateRoot;
}
}
}
return null;
}
/**
* Returns the node states that are part of the indexing aggregate of the
* <code>nodeState</code>.
*
* @param nodeState a node state
* @return the node states that are part of the indexing aggregate of
* <code>nodeState</code>. Returns <code>null</code> if this
* aggregate does not apply to <code>nodeState</code>.
* @throws ItemStateException if an error occurs.
*/
public NodeState[] getAggregatedNodeStates(NodeState nodeState)
throws ItemStateException {
if (nodeState.getNodeTypeName().equals(nodeTypeName)) {
List<NodeState> nodeStates = new ArrayList<NodeState>();
for (NodeInclude nodeInclude : nodeIncludes) {
for (NodeState childNs : nodeInclude.resolve(nodeState)) {
boolean sameNodeTypeAsRoot = nodeState.getNodeTypeName().equals(childNs.getNodeTypeName());
if (!sameNodeTypeAsRoot || (sameNodeTypeAsRoot && recursiveAggregation)) {
nodeStates.add(childNs);
}
}
}
if (nodeStates.size() > 0) {
return nodeStates.toArray(new NodeState[nodeStates.size()]);
}
}
return null;
}
/**
* {@inheritDoc}
*/
public PropertyState[] getAggregatedPropertyStates(NodeState nodeState)
throws ItemStateException {
if (nodeState.getNodeTypeName().equals(nodeTypeName)) {
List<PropertyState> propStates = new ArrayList<PropertyState>();
for (PropertyInclude propertyInclude : propertyIncludes) {
propStates.addAll(Arrays.asList(propertyInclude.resolvePropertyStates(nodeState)));
}
if (propStates.size() > 0) {
return propStates.toArray(new PropertyState[propStates.size()]);
}
}
return null;
}
/**
* {@inheritDoc}
*/
public long getRecursiveAggregationLimit() {
return recursiveAggregationLimit;
}
//---------------------------< internal >-----------------------------------
/**
* Reads the node type of the root node of the indexing aggregate.
*
* @param config the configuration.
* @return the name of the node type.
* @throws IllegalNameException if the node type name contains illegal
* characters.
* @throws NamespaceException if the node type contains an unknown
* prefix.
*/
private Name getNodeTypeName(Node config)
throws IllegalNameException, NamespaceException {
String ntString = config.getAttributes().getNamedItem("primaryType").getNodeValue();
return resolver.getQName(ntString);
}
/**
* Creates node includes defined in the <code>config</code>.
*
* @param config the indexing aggregate configuration.
* @return the node includes defined in the <code>config</code>.
* @throws MalformedPathException if a path in the configuration is
* malformed.
* @throws IllegalNameException if the node type name contains illegal
* characters.
* @throws NamespaceException if the node type contains an unknown
* prefix.
*/
private NodeInclude[] getNodeIncludes(Node config)
throws MalformedPathException, IllegalNameException, NamespaceException {
List<NodeInclude> includes = new ArrayList<NodeInclude>();
NodeList childNodes = config.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
Node n = childNodes.item(i);
if (n.getNodeName().equals("include")) {
Name ntName = null;
Node ntAttr = n.getAttributes().getNamedItem("primaryType");
if (ntAttr != null) {
ntName = resolver.getQName(ntAttr.getNodeValue());
}
PathBuilder builder = new PathBuilder();
for (String element : Text.explode(getTextContent(n), '/')) {
if (element.equals("*")) {
builder.addLast(NameConstants.ANY_NAME);
} else {
builder.addLast(resolver.getQName(element));
}
}
includes.add(new NodeInclude(builder.getPath(), ntName));
}
}
return includes.toArray(new NodeInclude[includes.size()]);
}
/**
* Creates property includes defined in the <code>config</code>.
*
* @param config the indexing aggregate configuration.
* @return the property includes defined in the <code>config</code>.
* @throws MalformedPathException if a path in the configuration is
* malformed.
* @throws IllegalNameException if the node type name contains illegal
* characters.
* @throws NamespaceException if the node type contains an unknown
* prefix.
* @throws RepositoryException If the PropertyInclude cannot be builded
* due to unknown ancestor relationship.
*/
private PropertyInclude[] getPropertyIncludes(Node config) throws
MalformedPathException, IllegalNameException, NamespaceException,
RepositoryException {
List<PropertyInclude> includes = new ArrayList<PropertyInclude>();
NodeList childNodes = config.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
Node n = childNodes.item(i);
if (n.getNodeName().equals("include-property")) {
PathBuilder builder = new PathBuilder();
for (String element : Text.explode(getTextContent(n), '/')) {
if (element.equals("*")) {
throw new IllegalNameException("* not supported in include-property");
}
builder.addLast(resolver.getQName(element));
}
includes.add(new PropertyInclude(builder.getPath()));
}
}
return includes.toArray(new PropertyInclude[includes.size()]);
}
private boolean getRecursiveAggregation(Node config) {
Node rAttr = config.getAttributes().getNamedItem("recursive");
if (rAttr == null) {
return RECURSIVE_AGGREGATION_DEFAULT;
}
return Boolean.valueOf(rAttr.getNodeValue());
}
private long getRecursiveAggregationLimit(Node config)
throws RepositoryException {
Node rAttr = config.getAttributes().getNamedItem("recursiveLimit");
if (rAttr == null) {
return RECURSIVE_AGGREGATION_LIMIT_DEFAULT;
}
try {
return Long.valueOf(rAttr.getNodeValue());
} catch (NumberFormatException e) {
throw new RepositoryException(
"Unable to read indexing configuration (recursiveLimit).",
e);
}
}
//---------------------------< internal >-----------------------------------
/**
* @param node a node.
* @return the text content of the <code>node</code>.
*/
private static String getTextContent(Node node) {
StringBuffer content = new StringBuffer();
NodeList nodes = node.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
Node n = nodes.item(i);
if (n.getNodeType() == Node.TEXT_NODE) {
content.append(((CharacterData) n).getData());
}
}
return content.toString();
}
private abstract class AbstractInclude {
/**
* Optional node type name.
*/
protected final Name nodeTypeName;
/**
* A relative path pattern.
*/
protected final Path pattern;
/**
* Creates a new rule with a relative path pattern and an optional node
* type name.
*
* @param nodeTypeName node type name or <code>null</code> if all node
* types are allowed.
* @param pattern a relative path pattern.
*/
AbstractInclude(Path pattern, Name nodeTypeName) {
this.nodeTypeName = nodeTypeName;
this.pattern = pattern;
}
/**
* If the given <code>nodeState</code> matches this rule the root node
* state of the indexing aggregate is returned.
*
* @param nodeState a node state.
* @return the root node state of the indexing aggregate or
* <code>null</code> if <code>nodeState</code> does not belong
* to an indexing aggregate defined by this rule.
* @throws ItemStateException if an error occurs while accessing node
* states.
* @throws RepositoryException if another error occurs.
*/
NodeState matches(NodeState nodeState)
throws ItemStateException, RepositoryException {
// first check node type
if (nodeTypeName == null
|| nodeState.getNodeTypeName().equals(nodeTypeName)) {
// check pattern
Path.Element[] elements = pattern.getElements();
for (int e = elements.length - 1; e >= 0; e--) {
NodeId parentId = nodeState.getParentId();
if (parentId == null) {
// nodeState is root node
return null;
}
NodeState parent = (NodeState) ism.getItemState(parentId);
if (elements[e].getName().getLocalName().equals("*")) {
// match any parent
nodeState = parent;
} else {
// check name
Name name = hmgr.getName(nodeState.getId());
if (elements[e].getName().equals(name)) {
nodeState = parent;
} else {
return null;
}
}
}
// if we get here nodeState became the root
// of the indexing aggregate and is valid
return nodeState;
}
return null;
}
//-----------------------------< internal >-----------------------------
/**
* Recursively resolves node states along the path {@link #pattern}.
*
* @param nodeState the current node state.
* @param collector resolved node states are collected using the list.
* @param offset the current path element offset into the path
* pattern.
* @throws ItemStateException if an error occurs while accessing node
* states.
*/
protected void resolve(NodeState nodeState, List<NodeState> collector, int offset)
throws ItemStateException {
Name currentName = pattern.getElements()[offset].getName();
List<ChildNodeEntry> cne;
if (currentName.getLocalName().equals("*")) {
// matches all
cne = nodeState.getChildNodeEntries();
} else {
cne = nodeState.getChildNodeEntries(currentName);
}
if (pattern.getLength() - 1 == offset) {
// last segment -> add to collector if node type matches
for (ChildNodeEntry entry : cne) {
NodeState ns = (NodeState) ism.getItemState(entry.getId());
if (nodeTypeName == null || ns.getNodeTypeName().equals(nodeTypeName)) {
collector.add(ns);
}
}
} else {
// traverse
offset++;
for (ChildNodeEntry entry : cne) {
NodeId id = entry.getId();
resolve((NodeState) ism.getItemState(id), collector, offset);
}
}
}
}
private final class NodeInclude extends AbstractInclude {
/**
* Creates a new node include with a relative path pattern and an
* optional node type name.
*
* @param nodeTypeName node type name or <code>null</code> if all node
* types are allowed.
* @param pattern a relative path pattern.
*/
NodeInclude(Path pattern, Name nodeTypeName) {
super(pattern, nodeTypeName);
}
/**
* Resolves the <code>nodeState</code> using this rule.
*
* @param nodeState the root node of the enclosing indexing aggregate.
* @return the descendant node states as defined by this rule.
* @throws ItemStateException if an error occurs while resolving the
* node states.
*/
NodeState[] resolve(NodeState nodeState) throws ItemStateException {
List<NodeState> nodeStates = new ArrayList<NodeState>();
resolve(nodeState, nodeStates, 0);
return nodeStates.toArray(new NodeState[nodeStates.size()]);
}
}
private final class PropertyInclude extends AbstractInclude {
private final Name propertyName;
PropertyInclude(Path pattern)
throws RepositoryException {
super(pattern.getAncestor(1), null);
this.propertyName = pattern.getName();
}
/**
* Resolves the <code>nodeState</code> using this rule.
*
* @param nodeState the root node of the enclosing indexing aggregate.
* @return the descendant property states as defined by this rule.
* @throws ItemStateException if an error occurs while resolving the
* property states.
*/
PropertyState[] resolvePropertyStates(NodeState nodeState)
throws ItemStateException {
List<NodeState> nodeStates = new ArrayList<NodeState>();
resolve(nodeState, nodeStates, 0);
List<PropertyState> propStates = new ArrayList<PropertyState>();
for (NodeState state : nodeStates) {
if (state.hasPropertyName(propertyName)) {
PropertyId propId = new PropertyId(state.getNodeId(), propertyName);
propStates.add((PropertyState) ism.getItemState(propId));
}
}
return propStates.toArray(new PropertyState[propStates.size()]);
}
}
}