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 }