Java tutorial
/* * (c) 2014 Adobe. All rights reserved. * This file is licensed 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 REPRESENTATIONS * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ package com.adobe.cq.dialogconversion.impl.rules; import com.adobe.cq.dialogconversion.DialogRewriteException; import com.adobe.cq.dialogconversion.DialogRewriteRule; import com.adobe.cq.dialogconversion.DialogRewriteUtils; import com.day.cq.commons.jcr.JcrUtil; import org.apache.commons.lang3.StringUtils; import org.apache.jackrabbit.commons.flat.TreeTraverser; import org.apache.sling.api.resource.ResourceResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.Property; import javax.jcr.PropertyIterator; import javax.jcr.PropertyType; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.Value; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * A rule that rewrites a tree based on a given node structure. The node structure * has the following form: * * <pre> * rule * - jcr:primaryType = nt:unstructured * - cq:rewriteRanking = 4 * + patterns * - jcr:primaryType = nt:unstructured * + foo * - ... * + ... * + foo1 * - ... * + ... * + replacement * + bar * - ... * + ... * </pre> * * <p>This example defines a rule containing two patterns (the trees rooted at <code>foo</code> and <code>foo1</code>) * and a replacement (the tree rooted at <code>bar</code>). The pattern and replacement trees are arbitrary trees * containing nodes and properties. The rule matches a subtree if any of the defined patterns matches. In order for * a pattern to match, the tree in question must contain the same nodes as the pattern (matching names, except for the * root), and all properties defined in the pattern must match the properties on the tree. A node in a pattern * can be marked as optional by setting <code>cq:rewriteOptional</code> to <code>true</code>, in which case it * doesn't necessarily have to present for a tree to match.</p> * * <p>In the case of a match, the matched subtree (called original tree) will be substituted by the replacement. The * replacement tree can define mapped properties that will inherit the value of a property in the original tree. They * need to be of type <code>string</code> and have the following format: <code>${<path>}</code>. If the referenced * property doesn't exist in the original tree, then the property is omitted. Alternatively, a default value can be * specified for that case (only possible for <code>string</code> properties): * <code>${<path>:<default>}</code>. Properties that contain ':' characters can be single quoted to avoid * conflict with providing a default value. Boolean properties are negated if the expression is prefixed with * <code>!</code>. Mapped properties can also be multivalued, in which case they will be assigned the value of the first * property that exists in the original tree. The following example illustrates mapping properties:</p> * * <pre> * rule * ... * + replacement * + bar * - prop = ${./some/prop} * // 'prop' will be assigned the value of 'some/prop' in the original tree * - negated = !${./some/boolean/prop} * // 'negated' will be assigned the negated value of 'some/boolean/prop' in the original tree * - default = ${./some/prop:default string value} * // 'default' will be assigned the value of 'some/prop' if it exists, else the string 'default string' * - multi = [${./some/prop1}, ${./some/prop2}] * // 'multi' will be assigned the value of 'some/prop1' if it exists, else the value of 'some/prop2' * </pre> * * The replacement tree supports following special properties: * * <ul> * <li> * <code>cq:rewriteMapChildren</code> (string)<br /> * Copies the children of the referenced node in the original tree to the node containing this property * (e.g. <code>cq:rewriteMapChildren=./items</code> will copy the children of <code>./items</code> to the * current node). * </li> * <li> * <code>cq:rewriteFinal</code> (boolean)<br /> * Set this property on a node that is final and can be disregarded for the rest of the conversion as an * optimization measure. When placed on the replacement node itself (i.e. on <code>rule/replacement</code>), * the whole replacement tree is considered final. * </li> * <li> * <code>cq:rewriteCommonAttrs</code> (boolean)<br /> * Set this property on the replacement node (<code>rule/replacement</code>) to map relevant properties * of the original root node to Granite common attribute equivalents in the copy root. It will handle data * attributes by copying/creating the granite:data subnode on the target and writing data-* properties there. * </li> * <li> * <code>cq:rewriteRenderCondition</code> (boolean)<br /> * Set this property on the replacement node (<code>rule/replacement</code>) to copy any render condition * (rendercondition or granite:rendercondition) child node from the original root node to a * granite:rendercondition child of the copy root. * </li> * </ul> * * In addition, a special <code><cq:rewriteProperties></code> node can be added to a replacement node to define * string rewrites for mapped properties in the result. The node is removed from the replacement. * The properties of the <code><cq:rewriteProperties></code> node must * be named the same as those which they are rewriting and accept a string array with two parameters: * * - pattern: regexp to match against. e.g. "(?:coral-Icon--)(.+)" * - replacement: provided to the matcher <code>replaceAll</code> function. e.g. "$1" * * Example: * * <pre> * rule * ... * + replacement * + bar * - icon = ${./icon} * + cq:rewriteProperties * - icon = [(?:coral-Icon--)(.+), $1] * </pre> * */ public class NodeBasedRewriteRule implements DialogRewriteRule { // pattern that matches the regex for mapped properties: ${<path>} private static final Pattern MAPPED_PATTERN = Pattern.compile("^(\\!{0,1})\\$\\{(\'.*?\'|.*?)(:(.+))?\\}$"); // special properties private static final String PROPERTY_RANKING = "cq:rewriteRanking"; private static final String PROPERTY_OPTIONAL = "cq:rewriteOptional"; private static final String PROPERTY_MAP_CHILDREN = "cq:rewriteMapChildren"; private static final String PROPERTY_IS_FINAL = "cq:rewriteFinal"; private static final String PROPERTY_COMMON_ATTRS = "cq:rewriteCommonAttrs"; private static final String PROPERTY_RENDER_CONDITION = "cq:rewriteRenderCondition"; // special nodes private static final String NN_CQ_REWRITE_PROPERTIES = "cq:rewriteProperties"; // node names private static final String NN_RENDER_CONDITION = "rendercondition"; private static final String NN_GRANITE_RENDER_CONDITION = "granite:rendercondition"; private static final String NN_GRANITE_DATA = "granite:data"; // Granite private static final String[] GRANITE_COMMON_ATTR_PROPERTIES = { "id", "rel", "class", "title", "hidden", "itemscope", "itemtype", "itemprop" }; private static final String RENDER_CONDITION_CORAL2_RESOURCE_TYPE_PREFIX = "granite/ui/components/foundation/renderconditions"; private static final String RENDER_CONDITION_CORAL3_RESOURCE_TYPE_PREFIX = "granite/ui/components/coral/foundation/renderconditions"; private static final String DATA_PREFIX = "data-"; private Logger logger = LoggerFactory.getLogger(NodeBasedRewriteRule.class); private Node ruleNode; private Integer ranking = null; public NodeBasedRewriteRule(Node ruleNode) { this.ruleNode = ruleNode; } public boolean matches(Node root) throws RepositoryException { if (!ruleNode.hasNode("patterns")) { // the 'patterns' subnode does not exist return false; } Node patterns = ruleNode.getNode("patterns"); if (!patterns.hasNodes()) { // no patterns are defined return false; } // iterate over all defined patterns NodeIterator iterator = patterns.getNodes(); while (iterator.hasNext()) { Node pattern = iterator.nextNode(); if (matches(root, pattern)) { return true; } } // no pattern matched return false; } private boolean matches(Node root, Node pattern) throws RepositoryException { // check if the primary types match if (!DialogRewriteUtils.hasPrimaryType(root, pattern.getPrimaryNodeType().getName())) { return false; } // check that all properties of the pattern match PropertyIterator propertyIterator = pattern.getProperties(); while (propertyIterator.hasNext()) { Property property = propertyIterator.nextProperty(); String name = property.getName(); if (property.getDefinition().isProtected()) { // skip protected properties continue; } if (PROPERTY_OPTIONAL.equals(name)) { // skip cq:rewriteOptional property continue; } if (!root.hasProperty(name)) { // property present on pattern does not exist in tree return false; } if (!root.getProperty(name).getValue().equals(property.getValue())) { // property values on pattern and tree differ return false; } } // check that the tree contains all children defined in the pattern (optimization measure, before // checking all children recursively) NodeIterator nodeIterator = pattern.getNodes(); while (nodeIterator.hasNext()) { Node child = nodeIterator.nextNode(); // if the node is marked as optional, we can skip the check if (child.hasProperty(PROPERTY_OPTIONAL) && child.getProperty(PROPERTY_OPTIONAL).getBoolean()) { continue; } if (!root.hasNode(child.getName())) { // this child is not present in subject tree return false; } } // check child nodes recursively nodeIterator = pattern.getNodes(); while (nodeIterator.hasNext()) { Node child = nodeIterator.nextNode(); // if the node is marked as optional and is not present, then we skip it if (child.hasProperty(PROPERTY_OPTIONAL) && child.getProperty(PROPERTY_OPTIONAL).getBoolean() && !root.hasNode(child.getName())) { continue; } return matches(root.getNode(child.getName()), child); } // base case (leaf node) return true; } public Node applyTo(Node root, Set<Node> finalNodes) throws DialogRewriteException, RepositoryException { // check if the 'replacement' node exists if (!ruleNode.hasNode("replacement")) { throw new DialogRewriteException("The rule does not define a replacement node"); } // if the replacement node has no children, we replace the tree by the empty tree, // i.e. we remove the original tree Node replacement = ruleNode.getNode("replacement"); if (!replacement.hasNodes()) { root.remove(); return null; } // true if the replacement tree is final and all its nodes are excluded from // further processing by the algorithm boolean treeIsFinal = false; if (replacement.hasProperty(PROPERTY_IS_FINAL)) { treeIsFinal = replacement.getProperty(PROPERTY_IS_FINAL).getBoolean(); } /** * Approach: * - we move (rename) the tree to be rewritten to a temporary name * - we copy the replacement tree to be a new child of the original tree's parent * - we process the copied replacement tree (mapped properties, children etc) * - at the end, we remove the original tree */ // move (rename) original tree Node parent = root.getParent(); String rootName = root.getName(); DialogRewriteUtils.rename(root); // copy replacement to original tree under original name Node replacementNext = replacement.getNodes().nextNode(); Node copy = JcrUtil.copy(replacementNext, parent, rootName); // common attribute mapping if (replacement.hasProperty(PROPERTY_COMMON_ATTRS)) { addCommonAttrMappings(root, copy); } // render condition mapping if (replacement.hasProperty(PROPERTY_RENDER_CONDITION)) { if (root.hasNode(NN_GRANITE_RENDER_CONDITION) || root.hasNode(NN_RENDER_CONDITION)) { Node renderConditionRoot = root.hasNode(NN_GRANITE_RENDER_CONDITION) ? root.getNode(NN_GRANITE_RENDER_CONDITION) : root.getNode(NN_RENDER_CONDITION); Node renderConditionCopy = JcrUtil.copy(renderConditionRoot, copy, NN_GRANITE_RENDER_CONDITION); // convert render condition resource types recursively TreeTraverser renderConditionTraverser = new TreeTraverser(renderConditionCopy); Iterator<Node> renderConditionIterator = renderConditionTraverser.iterator(); while (renderConditionIterator.hasNext()) { Node renderConditionNode = renderConditionIterator.next(); String resourceType = renderConditionNode.getProperty(ResourceResolver.PROPERTY_RESOURCE_TYPE) .getString(); if (resourceType.startsWith(RENDER_CONDITION_CORAL2_RESOURCE_TYPE_PREFIX)) { resourceType = resourceType.replace(RENDER_CONDITION_CORAL2_RESOURCE_TYPE_PREFIX, RENDER_CONDITION_CORAL3_RESOURCE_TYPE_PREFIX); renderConditionNode.setProperty(ResourceResolver.PROPERTY_RESOURCE_TYPE, resourceType); } } } } // collect mappings: (node in original tree) -> (node in replacement tree) Map<String, String> mappings = new HashMap<String, String>(); // traverse nodes of newly copied replacement tree TreeTraverser traverser = new TreeTraverser(copy); Iterator<Node> nodeIterator = traverser.iterator(); while (nodeIterator.hasNext()) { Node node = nodeIterator.next(); // iterate over all properties PropertyIterator propertyIterator = node.getProperties(); Node rewritePropertiesNode = null; if (node.hasNode(NN_CQ_REWRITE_PROPERTIES)) { rewritePropertiesNode = node.getNode(NN_CQ_REWRITE_PROPERTIES); } while (propertyIterator.hasNext()) { Property property = propertyIterator.nextProperty(); // skip protected properties if (property.getDefinition().isProtected()) { continue; } // add mapping to collection if (PROPERTY_MAP_CHILDREN.equals(property.getName())) { mappings.put(property.getString(), node.getPath()); // remove property, as we don't want it to be part of the result property.remove(); continue; } // add single node to final nodes if (PROPERTY_IS_FINAL.equals(property.getName())) { if (!treeIsFinal) { finalNodes.add(node); } property.remove(); continue; } // set value from original tree in case this is a mapped property Property mappedProperty = mapProperty(root, property); if (mappedProperty != null && rewritePropertiesNode != null) { if (rewritePropertiesNode.hasProperty("./" + mappedProperty.getName())) { rewriteProperty(property, rewritePropertiesNode.getProperty("./" + mappedProperty.getName())); } } } // remove <cq:rewriteProperties> node post-mapping if (rewritePropertiesNode != null) { rewritePropertiesNode.remove(); } } // copy children from original tree to replacement tree according to the mappings found Session session = root.getSession(); for (Map.Entry<String, String> mapping : mappings.entrySet()) { if (!root.hasNode(mapping.getKey())) { // the node specified in the mapping does not exist in the original tree continue; } Node source = root.getNode(mapping.getKey()); Node destination = session.getNode(mapping.getValue()); NodeIterator iterator = source.getNodes(); // copy over the source's children to the destination while (iterator.hasNext()) { Node child = iterator.nextNode(); JcrUtil.copy(child, destination, child.getName()); } } // we add the complete subtree to the final nodes if (treeIsFinal) { traverser = new TreeTraverser(copy); nodeIterator = traverser.iterator(); while (nodeIterator.hasNext()) { finalNodes.add(nodeIterator.next()); } } // remove original tree and return rewritten tree root.remove(); return copy; } /** * Replaces the value of a mapped property with a value from the original tree. * * @param root the root node of the original tree * @param property the (potentially) mapped property in the replacement copy tree * @return the mapped property if there was a successful mapping, null otherwise */ private Property mapProperty(Node root, Property property) throws RepositoryException { if (property.getType() != PropertyType.STRING) { // a mapped property must be of type string return null; } // array containing the expressions: ${<path>} Value[] values; if (property.isMultiple()) { values = property.getValues(); } else { values = new Value[1]; values[0] = property.getValue(); } boolean deleteProperty = false; for (Value value : values) { Matcher matcher = MAPPED_PATTERN.matcher(value.getString()); if (matcher.matches()) { // this is a mapped property, we will delete it if the mapped destination // property doesn't exist deleteProperty = true; String path = matcher.group(2); // unwrap quoted property paths path = StringUtils.removeStart(StringUtils.stripEnd(path, "\'"), "\'"); if (root.hasProperty(path)) { // replace property by mapped value in the original tree Property originalProperty = root.getProperty(path); String name = property.getName(); Node parent = property.getParent(); property.remove(); Property newProperty = JcrUtil.copy(originalProperty, parent, name); // negate boolean properties if negation character has been set String negate = matcher.group(1); if ("!".equals(negate) && originalProperty.getType() == PropertyType.BOOLEAN) { newProperty.setValue(!newProperty.getBoolean()); } // the mapping was successful deleteProperty = false; break; } else { String defaultValue = matcher.group(4); if (defaultValue != null) { if (property.isMultiple()) { // the property is multiple in the replacement, // recreate it so we can set the property to the default String name = property.getName(); Node parent = property.getParent(); property.remove(); parent.setProperty(name, defaultValue); } else { property.setValue(defaultValue); } deleteProperty = false; break; } } } } if (deleteProperty) { // mapped destination does not exist, we don't include the property in replacement tree property.remove(); return null; } return property; } /** * Applies a string rewrite to a property. * * @param property the property to rewrite * @param rewriteProperty the property that defines the string rewrite */ private void rewriteProperty(Property property, Property rewriteProperty) throws RepositoryException { if (property.getType() == PropertyType.STRING) { if (rewriteProperty.isMultiple() && rewriteProperty.getValues().length == 2) { Value[] rewrite = rewriteProperty.getValues(); if (rewrite[0].getType() == PropertyType.STRING && rewrite[1].getType() == PropertyType.STRING) { String pattern = rewrite[0].toString(); String replacement = rewrite[1].toString(); Pattern compiledPattern = Pattern.compile(pattern); Matcher matcher = compiledPattern.matcher(property.getValue().toString()); property.setValue(matcher.replaceAll(replacement)); } } } } /** * Adds property mappings on a replacement node for Granite common attributes. * * @param root the root node * @param node the replacement node */ private void addCommonAttrMappings(Node root, Node node) throws RepositoryException { for (String property : GRANITE_COMMON_ATTR_PROPERTIES) { String[] mapping = { "${./" + property + "}", "${\'./granite:" + property + "\'}" }; mapProperty(root, node.setProperty("granite:" + property, mapping)); } if (root.hasNode(NN_GRANITE_DATA)) { // the root has granite:data defined, copy it before applying data-* properties JcrUtil.copy(root.getNode(NN_GRANITE_DATA), node, NN_GRANITE_DATA); } // map data-* prefixed properties to granite:data child PropertyIterator propertyIterator = root.getProperties(DATA_PREFIX + "*"); while (propertyIterator.hasNext()) { Property property = propertyIterator.nextProperty(); String name = property.getName(); // skip protected properties if (property.getDefinition().isProtected()) { continue; } // add the granite:data child if necessary if (!node.hasNode(NN_GRANITE_DATA)) { node.addNode(NN_GRANITE_DATA); } // set up the property mapping if (node.hasNode(NN_GRANITE_DATA)) { Node dataNode = node.getNode(NN_GRANITE_DATA); String nameWithoutPrefix = name.substring(DATA_PREFIX.length()); mapProperty(root, dataNode.setProperty(nameWithoutPrefix, "${./" + name + "}")); } } } @Override public String toString() { String path = null; try { path = ruleNode.getPath(); } catch (RepositoryException e) { // ignore } return "NodeBasedRewriteRule[" + (path == null ? "" : "path=" + path + ",") + "ranking=" + getRanking() + "]"; } public int getRanking() { if (ranking == null) { try { if (ruleNode.hasProperty(PROPERTY_RANKING)) { long ranking = ruleNode.getProperty(PROPERTY_RANKING).getLong(); this.ranking = new Long(ranking).intValue(); } else { this.ranking = Integer.MAX_VALUE; } } catch (RepositoryException e) { logger.warn("Caught exception while reading the " + PROPERTY_RANKING + " property"); } } return this.ranking; } }