View Javadoc

1   /*
2    * ModeShape (http://www.modeshape.org)
3    * See the COPYRIGHT.txt file distributed with this work for information
4    * regarding copyright ownership.  Some portions may be licensed
5    * to Red Hat, Inc. under one or more contributor license agreements.
6    * See the AUTHORS.txt file in the distribution for a full listing of 
7    * individual contributors. 
8    *
9    * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10   * is licensed to you under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation; either version 2.1 of
12   * the License, or (at your option) any later version.
13   *
14   * ModeShape is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this software; if not, write to the Free
21   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23   */
24  package org.modeshape.repository.sequencer;
25  
26  import java.io.Serializable;
27  import java.util.HashMap;
28  import java.util.Map;
29  import java.util.regex.Pattern;
30  import net.jcip.annotations.Immutable;
31  import org.modeshape.common.util.CheckArg;
32  import org.modeshape.common.util.HashCode;
33  import org.modeshape.graph.property.PathExpression;
34  import org.modeshape.repository.RepositoryI18n;
35  
36  /**
37   * An expression that defines a selection of some change in the repository that signals a sequencing operation should be run, and
38   * the location where the sequencing output should be placed. Sequencer path expressions are used within the
39   * {@link SequencerConfig sequencer configurations} and used to determine whether information in the repository needs to be
40   * sequenced.
41   * <p>
42   * A simple example is the following:
43   * 
44   * <pre>
45   *     /a/b/c@title =&gt; /d/e/f
46   * </pre>
47   * 
48   * which means that a sequencer (that uses this expression in its configuration) should be run any time there is a new or modified
49   * <code>title</code> property on the <code>/a/b/c</code> node, and that the output of the sequencing should be placed at
50   * <code>/d/e/f</code>.
51   * </p>
52   */
53  @Immutable
54  public class SequencerPathExpression implements Serializable {
55  
56      /**
57       */
58      private static final long serialVersionUID = 229464314137494765L;
59  
60      /**
61       * The pattern used to break the initial input string into the two major parts, the selection and output expressions. Group 1
62       * contains the selection expression, and group 2 contains the output expression.
63       */
64      private static final Pattern TWO_PART_PATTERN = Pattern.compile("((?:[^=]|=(?!>))+)(?:=>(.+))?");
65  
66      protected static final String DEFAULT_OUTPUT_EXPRESSION = ".";
67  
68      private static final String PARENT_PATTERN_STRING = "[^/]+/\\.\\./"; // [^/]+/\.\./
69      private static final Pattern PARENT_PATTERN = Pattern.compile(PARENT_PATTERN_STRING);
70  
71      private static final String REPLACEMENT_VARIABLE_PATTERN_STRING = "(?<!\\\\)\\$(\\d+)"; // (?<!\\)\$(\d+)
72      private static final Pattern REPLACEMENT_VARIABLE_PATTERN = Pattern.compile(REPLACEMENT_VARIABLE_PATTERN_STRING);
73  
74      /**
75       * Compile the supplied expression and return the resulting SequencerPathExpression instance.
76       * 
77       * @param expression the expression
78       * @return the path expression; never null
79       * @throws IllegalArgumentException if the expression is null
80       * @throws InvalidSequencerPathExpression if the expression is blank or is not a valid expression
81       */
82      public static final SequencerPathExpression compile( String expression ) throws InvalidSequencerPathExpression {
83          CheckArg.isNotNull(expression, "sequencer path expression");
84          expression = expression.trim();
85          if (expression.length() == 0) {
86              throw new InvalidSequencerPathExpression(RepositoryI18n.pathExpressionMayNotBeBlank.text());
87          }
88          java.util.regex.Matcher matcher = TWO_PART_PATTERN.matcher(expression);
89          if (!matcher.matches()) {
90              throw new InvalidSequencerPathExpression(RepositoryI18n.pathExpressionIsInvalid.text(expression));
91          }
92          String selectExpression = matcher.group(1);
93          String outputExpression = matcher.group(2);
94          return new SequencerPathExpression(PathExpression.compile(selectExpression), outputExpression);
95      }
96  
97      private final PathExpression selectExpression;
98      private final String outputExpression;
99      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 }