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