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 }