/* * 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()]); } } }