/*
* 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 org.exoplatform.services.jcr.dataflow.ItemDataConsumer;
import org.exoplatform.services.jcr.datamodel.IllegalNameException;
import org.exoplatform.services.jcr.datamodel.IllegalPathException;
import org.exoplatform.services.jcr.datamodel.InternalQName;
import org.exoplatform.services.jcr.datamodel.ItemData;
import org.exoplatform.services.jcr.datamodel.ItemType;
import org.exoplatform.services.jcr.datamodel.NodeData;
import org.exoplatform.services.jcr.datamodel.PropertyData;
import org.exoplatform.services.jcr.datamodel.QPath;
import org.exoplatform.services.jcr.datamodel.QPathEntry;
import org.exoplatform.services.jcr.impl.Constants;
import org.exoplatform.services.jcr.impl.core.LocationFactory;
import org.exoplatform.services.jcr.util.Text;
import org.w3c.dom.CharacterData;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import javax.jcr.PathNotFoundException;
import javax.jcr.RepositoryException;
/**
* <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 LocationFactory resolver;
/**
* The node type of the root node of the indexing aggregate.
*/
private final InternalQName 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 ItemDataConsumer ism;
/**
* 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 RepositoryException
*/
AggregateRuleImpl(Node config, LocationFactory resolver, ItemDataConsumer ism) throws IllegalNameException,
RepositoryException
{
this.resolver = resolver;
this.nodeTypeName = getNodeTypeName(config);
this.nodeIncludes = getNodeIncludes(config);
this.propertyIncludes = getPropertyIncludes(config);
this.ism = ism;
}
/**
* 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 NodeData getAggregateRoot(NodeData nodeState) throws RepositoryException
{
for (int i = 0; i < nodeIncludes.length; i++)
{
NodeData aggregateRoot = nodeIncludes[i].matches(nodeState);
if (aggregateRoot != null && aggregateRoot.getPrimaryTypeName().equals(nodeTypeName))
{
return aggregateRoot;
}
}
// check property includes
for (int i = 0; i < propertyIncludes.length; i++)
{
NodeData aggregateRoot = propertyIncludes[i].matches(nodeState);
if (aggregateRoot != null && aggregateRoot.getPrimaryTypeName().equals(nodeTypeName))
{
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 RepositoryException
* @throws ItemStateException if an error occurs.
*/
public NodeData[] getAggregatedNodeStates(NodeData nodeState) throws RepositoryException
{
if (nodeState.getPrimaryTypeName().equals(nodeTypeName))
{
List<NodeData> nodeStates = new ArrayList<NodeData>();
for (int i = 0; i < nodeIncludes.length; i++)
{
nodeStates.addAll(Arrays.asList(nodeIncludes[i].resolve(nodeState)));
}
if (nodeStates.size() > 0)
{
return (NodeData[])nodeStates.toArray(new NodeData[nodeStates.size()]);
}
}
return null;
}
/**
* {@inheritDoc}
* @throws RepositoryException
*/
public PropertyData[] getAggregatedPropertyStates(NodeData nodeState) throws RepositoryException
{
if (nodeState.getPrimaryTypeName().equals(nodeTypeName))
{
List<PropertyData> propStates = new ArrayList<PropertyData>();
for (int i = 0; i < propertyIncludes.length; i++)
{
propStates.addAll(Arrays.asList(propertyIncludes[i].resolvePropertyStates(nodeState)));
}
if (propStates.size() > 0)
{
return (PropertyData[])propStates.toArray(new PropertyData[propStates.size()]);
}
}
return null;
}
//---------------------------< 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 RepositoryException
*/
private InternalQName getNodeTypeName(Node config) throws IllegalNameException, RepositoryException
{
String ntString = config.getAttributes().getNamedItem("primaryType").getNodeValue();
return resolver.parseJCRName(ntString).getInternalName();
}
/**
* 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 RepositoryException
*/
private NodeInclude[] getNodeIncludes(Node config) throws IllegalNameException, RepositoryException
{
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"))
{
InternalQName ntName = null;
Node ntAttr = n.getAttributes().getNamedItem("primaryType");
if (ntAttr != null)
{
ntName = resolver.parseJCRName(ntAttr.getNodeValue()).getInternalName();
}
String[] elements = Text.explode(getTextContent(n), '/');
QPathEntry[] path = new QPathEntry[elements.length];
for (int j = 0; j < elements.length; j++)
{
if (elements[j].equals("*"))
{
path[j] = new QPathEntry(Constants.JCR_ANY_NAME, 0);
}
else
{
path[j] = new QPathEntry(resolver.parseJCRName(elements[j]).getInternalName(), 0);
}
}
includes.add(new NodeInclude(new QPath(path), ntName));
}
}
return (NodeInclude[])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 RepositoryException
*/
private PropertyInclude[] getPropertyIncludes(Node config) throws IllegalNameException, 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"))
{
String[] elements = Text.explode(getTextContent(n), '/');
QPathEntry[] path = new QPathEntry[elements.length];
for (int j = 0; j < elements.length; j++)
{
if (elements[j].equals("*"))
{
throw new IllegalNameException("* not supported in include-property");
}
path[j] = new QPathEntry(resolver.parseJCRName(elements[j]).getInternalName(), 1);
}
includes.add(new PropertyInclude(new QPath(path)));
}
}
return (PropertyInclude[])includes.toArray(new PropertyInclude[includes.size()]);
}
//---------------------------< internal >-----------------------------------
/**
* @param node a node.
* @return the text content of the <code>node</code>.
*/
private static String getTextContent(Node node)
{
StringBuilder content = new StringBuilder();
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 InternalQName nodeTypeName;
/**
* A relative path pattern.
*/
protected final QPath 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(QPath pattern, InternalQName 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.
*/
NodeData matches(NodeData nodeState) throws RepositoryException
{
// first check node type
if (nodeTypeName == null || nodeState.getPrimaryTypeName().equals(nodeTypeName))
{
// check pattern
QPathEntry[] elements = pattern.getEntries();
for (int e = elements.length - 1; e >= 0; e--)
{
String parentId = nodeState.getParentIdentifier();
if (parentId == null)
{
// nodeState is root node
return null;
}
NodeData parent = (NodeData)ism.getItemData(parentId);
if (elements[e].getName().equals("*"))
{
// match any parent
nodeState = parent;
}
else
{
// check name
InternalQName name = nodeState.getQPath().getName();
if (elements[e].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 RepositoryException
* @throws ItemStateException if an error occurs while accessing node
* states.
*/
protected void resolve(NodeData nodeState, List<NodeData> collector, int offset) throws RepositoryException
{
QPathEntry currentName = pattern.getEntries()[offset];// [offset].getName();
List<NodeData> cne;
if (currentName.getAsString().equals("*"))
{
// matches all
cne = ism.getChildNodesData(nodeState);// nodeState.getChildNodeEntries();
}
else
{
cne = new ArrayList<NodeData>();
ItemData item = ism.getItemData(nodeState, currentName, ItemType.NODE);
if (item != null && item.isNode())
{
cne.add((NodeData)item);
}
}
if (pattern.getEntries().length - 1 == offset)
{
// last segment -> add to collector if node type matches
for (Iterator<NodeData> it = cne.iterator(); it.hasNext();)
{
NodeData ns = it.next();
if (nodeTypeName == null || (ns != null && ns.getPrimaryTypeName().equals(nodeTypeName)))
{
collector.add(ns);
}
}
}
else
{
// traverse
offset++;
for (Iterator<NodeData> it = cne.iterator(); it.hasNext();)
{
NodeData nodeData = it.next();
if (nodeData != null)
resolve(nodeData, 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(QPath pattern, InternalQName 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 RepositoryException
* @throws ItemStateException if an error occurs while resolving the
* node states.
*/
NodeData[] resolve(NodeData nodeState) throws RepositoryException
{
List<NodeData> nodeStates = new ArrayList<NodeData>();
resolve(nodeState, nodeStates, 0);
return (NodeData[])nodeStates.toArray(new NodeData[nodeStates.size()]);
}
}
private final class PropertyInclude extends AbstractInclude
{
private final InternalQName propertyName;
PropertyInclude(QPath pattern) throws PathNotFoundException, IllegalPathException
{
super(new QPath(pattern.getRelPath(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 RepositoryException
* @throws ItemStateException if an error occurs while resolving the
* property states.
*/
PropertyData[] resolvePropertyStates(NodeData nodeState) throws RepositoryException
{
List<NodeData> nodeStates = new ArrayList<NodeData>();
resolve(nodeState, nodeStates, 0);
List<PropertyData> propStates = new ArrayList<PropertyData>();
for (Iterator<NodeData> it = nodeStates.iterator(); it.hasNext();)
{
NodeData state = it.next();
ItemData prop = ism.getItemData(state, new QPathEntry(propertyName, 1), ItemType.PROPERTY);
if (prop != null && !prop.isNode())
{
propStates.add((PropertyData)prop);
}
// if (state.hasPropertyName(propertyName)) {
// PropertyId propId = new PropertyId(state.getNodeId(), propertyName);
// propStates.add(ism.getItemState(propId));
// }
}
return (PropertyData[])propStates.toArray(new PropertyData[propStates.size()]);
}
}
}