001 /* 002 * JBoss DNA (http://www.jboss.org/dna) 003 * See the COPYRIGHT.txt file distributed with this work for information 004 * regarding copyright ownership. Some portions may be licensed 005 * to Red Hat, Inc. under one or more contributor license agreements. 006 * See the AUTHORS.txt file in the distribution for a full listing of 007 * individual contributors. 008 * 009 * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA 010 * is licensed to you under the terms of the GNU Lesser General Public License as 011 * published by the Free Software Foundation; either version 2.1 of 012 * the License, or (at your option) any later version. 013 * 014 * JBoss DNA is distributed in the hope that it will be useful, 015 * but WITHOUT ANY WARRANTY; without even the implied warranty of 016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 017 * Lesser General Public License for more details. 018 * 019 * You should have received a copy of the GNU Lesser General Public 020 * License along with this software; if not, write to the Free 021 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 022 * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 023 */ 024 package org.jboss.dna.repository.sequencer; 025 026 import java.io.Serializable; 027 import java.util.HashMap; 028 import java.util.Map; 029 import java.util.regex.Pattern; 030 import net.jcip.annotations.Immutable; 031 import org.jboss.dna.common.util.CheckArg; 032 import org.jboss.dna.common.util.HashCode; 033 import org.jboss.dna.graph.property.PathExpression; 034 import org.jboss.dna.repository.RepositoryI18n; 035 036 /** 037 * An expression that defines a selection of some change in the repository that signals a sequencing operation should be run, and 038 * the location where the sequencing output should be placed. Sequencer path expressions are used within the 039 * {@link SequencerConfig sequencer configurations} and used to determine whether information in the repository needs to be 040 * sequenced. 041 * <p> 042 * A simple example is the following: 043 * 044 * <pre> 045 * /a/b/c@title => /d/e/f 046 * </pre> 047 * 048 * which means that a sequencer (that uses this expression in its configuration) should be run any time there is a new or modified 049 * <code>title</code> property on the <code>/a/b/c</code> node, and that the output of the sequencing should be placed at 050 * <code>/d/e/f</code>. 051 * </p> 052 * 053 * @author Randall Hauch 054 */ 055 @Immutable 056 public class SequencerPathExpression implements Serializable { 057 058 /** 059 */ 060 private static final long serialVersionUID = 229464314137494765L; 061 062 /** 063 * The pattern used to break the initial input string into the two major parts, the selection and output expressions. Group 1 064 * contains the selection expression, and group 2 contains the output expression. 065 */ 066 private static final Pattern TWO_PART_PATTERN = Pattern.compile("((?:[^=]|=(?!>))+)(?:=>(.+))?"); 067 068 protected static final String DEFAULT_OUTPUT_EXPRESSION = "."; 069 070 private static final String PARENT_PATTERN_STRING = "[^/]+/\\.\\./"; // [^/]+/\.\./ 071 private static final Pattern PARENT_PATTERN = Pattern.compile(PARENT_PATTERN_STRING); 072 073 private static final String REPLACEMENT_VARIABLE_PATTERN_STRING = "(?<!\\\\)\\$(\\d+)"; // (?<!\\)\$(\d+) 074 private static final Pattern REPLACEMENT_VARIABLE_PATTERN = Pattern.compile(REPLACEMENT_VARIABLE_PATTERN_STRING); 075 076 /** 077 * Compile the supplied expression and return the resulting SequencerPathExpression instance. 078 * 079 * @param expression the expression 080 * @return the path expression; never null 081 * @throws IllegalArgumentException if the expression is null 082 * @throws InvalidSequencerPathExpression if the expression is blank or is not a valid expression 083 */ 084 public static final SequencerPathExpression compile( String expression ) throws InvalidSequencerPathExpression { 085 CheckArg.isNotNull(expression, "sequencer path expression"); 086 expression = expression.trim(); 087 if (expression.length() == 0) { 088 throw new InvalidSequencerPathExpression(RepositoryI18n.pathExpressionMayNotBeBlank.text()); 089 } 090 java.util.regex.Matcher matcher = TWO_PART_PATTERN.matcher(expression); 091 if (!matcher.matches()) { 092 throw new InvalidSequencerPathExpression(RepositoryI18n.pathExpressionIsInvalid.text(expression)); 093 } 094 String selectExpression = matcher.group(1); 095 String outputExpression = matcher.group(2); 096 return new SequencerPathExpression(PathExpression.compile(selectExpression), outputExpression); 097 } 098 099 private final PathExpression selectExpression; 100 private final String outputExpression; 101 private final int hc; 102 103 protected SequencerPathExpression( PathExpression selectExpression, 104 String outputExpression ) throws InvalidSequencerPathExpression { 105 CheckArg.isNotNull(selectExpression, "select expression"); 106 this.selectExpression = selectExpression; 107 this.outputExpression = outputExpression != null ? outputExpression.trim() : DEFAULT_OUTPUT_EXPRESSION; 108 this.hc = HashCode.compute(this.selectExpression, this.outputExpression); 109 } 110 111 /** 112 * @return selectExpression 113 */ 114 public String getSelectExpression() { 115 return this.selectExpression.getSelectExpression(); 116 } 117 118 /** 119 * @return outputExpression 120 */ 121 public String getOutputExpression() { 122 return this.outputExpression; 123 } 124 125 /** 126 * {@inheritDoc} 127 */ 128 @Override 129 public int hashCode() { 130 return this.hc; 131 } 132 133 /** 134 * {@inheritDoc} 135 */ 136 @Override 137 public boolean equals( Object obj ) { 138 if (obj == this) return true; 139 if (obj instanceof SequencerPathExpression) { 140 SequencerPathExpression that = (SequencerPathExpression)obj; 141 if (!this.selectExpression.equals(that.selectExpression)) return false; 142 if (!this.outputExpression.equalsIgnoreCase(that.outputExpression)) return false; 143 return true; 144 } 145 return false; 146 } 147 148 /** 149 * {@inheritDoc} 150 */ 151 @Override 152 public String toString() { 153 return this.selectExpression + "=>" + this.outputExpression; 154 } 155 156 /** 157 * @param absolutePath 158 * @return the matcher 159 */ 160 public Matcher matcher( String absolutePath ) { 161 PathExpression.Matcher inputMatcher = selectExpression.matcher(absolutePath); 162 String outputPath = null; 163 if (inputMatcher.matches()) { 164 // Grab the named groups ... 165 Map<Integer, String> replacements = new HashMap<Integer, String>(); 166 for (int i = 0, count = inputMatcher.groupCount(); i <= count; ++i) { 167 replacements.put(i, inputMatcher.group(i)); 168 } 169 170 // Grab the selected path ... 171 String selectedPath = inputMatcher.getSelectedNodePath(); 172 173 // Find the output path using the groups from the match pattern ... 174 outputPath = this.outputExpression; 175 if (!DEFAULT_OUTPUT_EXPRESSION.equals(outputPath)) { 176 java.util.regex.Matcher replacementMatcher = REPLACEMENT_VARIABLE_PATTERN.matcher(outputPath); 177 StringBuffer sb = new StringBuffer(); 178 if (replacementMatcher.find()) { 179 do { 180 String variable = replacementMatcher.group(1); 181 String replacement = replacements.get(Integer.valueOf(variable)); 182 if (replacement == null) replacement = replacementMatcher.group(0); 183 replacementMatcher.appendReplacement(sb, replacement); 184 } while (replacementMatcher.find()); 185 replacementMatcher.appendTail(sb); 186 outputPath = sb.toString(); 187 } 188 // Make sure there is a trailing '/' ... 189 if (!outputPath.endsWith("/")) outputPath = outputPath + "/"; 190 191 // Replace all references to "/./" with "/" ... 192 outputPath = outputPath.replaceAll("/\\./", "/"); 193 194 // Remove any path segment followed by a parent reference ... 195 java.util.regex.Matcher parentMatcher = PARENT_PATTERN.matcher(outputPath); 196 while (parentMatcher.find()) { 197 outputPath = parentMatcher.replaceAll(""); 198 // Make sure there is a trailing '/' ... 199 if (!outputPath.endsWith("/")) outputPath = outputPath + "/"; 200 parentMatcher = PARENT_PATTERN.matcher(outputPath); 201 } 202 203 // Remove all multiple occurrences of '/' ... 204 outputPath = outputPath.replaceAll("/{2,}", "/"); 205 206 // Remove the trailing '/@property' ... 207 outputPath = outputPath.replaceAll("/@[^/\\[\\]]+$", ""); 208 209 // Remove a trailing '/' ... 210 outputPath = outputPath.replaceAll("/$", ""); 211 212 // If the output path is blank, then use the default output expression ... 213 if (outputPath.length() == 0) outputPath = DEFAULT_OUTPUT_EXPRESSION; 214 215 } 216 if (DEFAULT_OUTPUT_EXPRESSION.equals(outputPath)) { 217 // The output path is the default expression, so use the selected path ... 218 outputPath = selectedPath; 219 } 220 } 221 222 return new Matcher(inputMatcher, outputPath); 223 } 224 225 @Immutable 226 public static class Matcher { 227 228 private final PathExpression.Matcher inputMatcher; 229 private final String outputPath; 230 private final int hc; 231 232 protected Matcher( PathExpression.Matcher inputMatcher, 233 String outputPath ) { 234 this.inputMatcher = inputMatcher; 235 this.outputPath = outputPath; 236 this.hc = HashCode.compute(super.hashCode(), this.outputPath); 237 } 238 239 public boolean matches() { 240 return inputMatcher.matches() && this.outputPath != null; 241 } 242 243 /** 244 * @return inputPath 245 */ 246 public String getInputPath() { 247 return inputMatcher.getInputPath(); 248 } 249 250 /** 251 * @return selectPattern 252 */ 253 public String getSelectedPath() { 254 return inputMatcher.getSelectedNodePath(); 255 } 256 257 /** 258 * @return outputPath 259 */ 260 public String getOutputPath() { 261 return this.outputPath; 262 } 263 264 /** 265 * {@inheritDoc} 266 */ 267 @Override 268 public int hashCode() { 269 return this.hc; 270 } 271 272 /** 273 * {@inheritDoc} 274 */ 275 @Override 276 public boolean equals( Object obj ) { 277 if (obj == this) return true; 278 if (obj instanceof SequencerPathExpression.Matcher) { 279 SequencerPathExpression.Matcher that = (SequencerPathExpression.Matcher)obj; 280 if (!super.equals(that)) return false; 281 if (!this.outputPath.equalsIgnoreCase(that.outputPath)) return false; 282 return true; 283 } 284 return false; 285 } 286 287 /** 288 * {@inheritDoc} 289 */ 290 @Override 291 public String toString() { 292 return inputMatcher + " => " + this.outputPath; 293 } 294 } 295 296 }