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.graph.property;
25  
26  import java.io.Serializable;
27  import java.util.regex.Pattern;
28  import java.util.regex.PatternSyntaxException;
29  import net.jcip.annotations.Immutable;
30  import org.modeshape.common.util.CheckArg;
31  import org.modeshape.common.util.HashCode;
32  import org.modeshape.common.util.ObjectUtil;
33  import org.modeshape.graph.GraphI18n;
34  
35  /**
36   * An expression that defines an acceptable path using a regular-expression-like language. Path expressions can be used to
37   * represent node paths or properties.
38   * <p>
39   * Let's first look at some simple examples of path expressions:
40   * </p>
41   * <table>
42   * <tr>
43   * <th>Path expression</th>
44   * <th>Description</th>
45   * </tr>
46   * <tr>
47   * <td>/a/b</td>
48   * <td>Match node "<code>b</code>" that is a child of the top level node "<code>a</code>". Neither node may have any
49   * same-name-sibilings.</td>
50   * </tr>
51   * <tr>
52   * <td>/a/*</td>
53   * <td>Match any child node of the top level node "<code>a</code>".</td>
54   * </tr>
55   * <tr>
56   * <td>/a/*.txt</td>
57   * <td>Match any child node of the top level node "<code>a</code>" that also has a name ending in "<code>.txt</code>".</td>
58   * </tr>
59   * <tr>
60   * <td>/a/b@c</td>
61   * <td>Match the property "<code>c</code>" of node "<code>/a/b</code>".</td>
62   * </tr>
63   * <tr>
64   * <td>/a/b[2]</td>
65   * <td>The second child named "<code>b</code>" below the top level node "<code>a</code>".</td>
66   * </tr>
67   * <tr>
68   * <td>/a/b[2,3,4]</td>
69   * <td>The second, third or fourth child named "<code>b</code>" below the top level node "<code>a</code>".</td>
70   * </tr>
71   * <tr>
72   * <td>/a/b[*]</td>
73   * <td>Any (and every) child named "<code>b</code>" below the top level node "<code>a</code>".</td>
74   * </tr>
75   * <tr>
76   * <td>//a/b</td>
77   * <td>Any node named "<code>b</code>" that exists below a node named "<code>a</code>", regardless of where node "<code>a</code>"
78   * occurs. Again, neither node may have any same-name-sibilings.</td>
79   * </tr>
80   * </table>
81   * <p>
82   * With these simple examples, you can probably discern the most important rules. First, the '<code>*</code>' is a wildcard
83   * character that matches any character or sequence of characters in a node's name (or index if appearing in between square
84   * brackets), and can be used in conjunction with other characters (e.g., "<code>*.txt</code>").
85   * </p>
86   * <p>
87   * Second, square brackets (i.e., '<code>[</code>' and '<code>]</code>') are used to match a node's same-name-sibiling index. You
88   * can put a single non-negative number or a comma-separated list of non-negative numbers. Use '0' to match a node that has no
89   * same-name-sibilings, or any positive number to match the specific same-name-sibling.
90   * </p>
91   * <p>
92   * Third, combining two delimiters (e.g., "<code>//</code>") matches any sequence of nodes, regardless of what their names are or
93   * how many nodes. Often used with other patterns to identify nodes at any level matching other patterns. Three or more sequential
94   * slash characters are treated as two.
95   * </p>
96   * <p>
97   * Many path expressions can be created using just these simple rules. However, input paths can be more complicated. Here are some
98   * more examples:
99   * </p>
100  * <table>
101  * <tr>
102  * <th>Path expressions</th>
103  * <th>Description</th>
104  * </tr>
105  * <tr>
106  * <td>/a/(b|c|d)</td>
107  * <td>Match children of the top level node "<code>a</code>" that are named "<code>a</code>", "<code>b</code>" or "<code>c</code>
108  * ". None of the nodes may have same-name-sibling indexes.</td>
109  * </tr>
110  * <tr>
111  * <td>/a/b[c/d]</td>
112  * <td>Match node "<code>b</code>" child of the top level node "<code>a</code>", when node "<code>b</code>" has a child named "
113  * <code>c</code>", and "<code>c</code>" has a child named "<code>d</code>". Node "<code>b</code>
114  * " is the selected node, while nodes "<code>b</code>" and "<code>b</code>" are used as criteria but are not selected.</td>
115  * </tr>
116  * <tr>
117  * <td>/a(/(b|c|d|)/e)[f/g/@something]</td>
118  * <td>Match node "<code>/a/b/e</code>", "<code>/a/c/e</code>", "<code>/a/d/e</code>", or "<code>/a/e</code>
119  * " when they also have a child "<code>f</code>" that itself has a child "<code>g</code>" with property "<code>something</code>".
120  * None of the nodes may have same-name-sibling indexes.</td>
121  * </tr>
122  * </table>
123  * <p>
124  * These examples show a few more advanced rules. Parentheses (i.e., '<code>(</code>' and '<code>)</code>') can be used to define
125  * a set of options for names, as shown in the first and third rules. Whatever part of the selected node's path appears between
126  * the parentheses is captured for use within the output path. Thus, the first input path in the previous table would match node "
127  * <code>/a/b</code>", and "b" would be captured and could be used within the output path using "<code>$1</code>", where the
128  * number used in the output path identifies the parentheses.
129  * </p>
130  * <p>
131  * Square brackets can also be used to specify criteria on a node's properties or children. Whatever appears in between the square
132  * brackets does not appear in the selected node.
133  * </p>
134  * <h3>Repository and Workspace names</h3>
135  * <p>
136  * Path expressions can also specify restrictions on the repository name and workspace name, to constrain the path expression to
137  * matching only paths from workspaces in repositories meeting the name criteria. Of course, if the path expression doesn't
138  * include these restrictions, the repository and workspace names are not considered when matching paths.
139  * </p>
140  */
141 @Immutable
142 public class PathExpression implements Serializable {
143 
144     /**
145      * Initial version
146      */
147     private static final long serialVersionUID = 1L;
148 
149     /**
150      * Compile the supplied expression and return the resulting path expression instance.
151      * 
152      * @param expression the expression
153      * @return the path expression; never null
154      * @throws IllegalArgumentException if the expression is null
155      * @throws InvalidPathExpressionException if the expression is blank or is not a valid expression
156      */
157     public static final PathExpression compile( String expression ) throws InvalidPathExpressionException {
158         return new PathExpression(expression);
159     }
160 
161     private static final String SEQUENCE_PATTERN_STRING = "\\[(\\d+(?:,\\d+)*)\\]"; // \[(\d+(,\d+)*)\]
162     private static final Pattern SEQUENCE_PATTERN = Pattern.compile(SEQUENCE_PATTERN_STRING);
163 
164     /**
165      * Regular expression used to find unusable XPath predicates within an expression. This pattern results in unusable predicates
166      * in group 1. Note that some predicates may be valid at the end but not valid elsewhere.
167      * <p>
168      * Currently, only index-like predicates (including sequences) are allowed everywhere. Predicates with paths and properties
169      * are allowed only as the last predicate. Predicates with any operators are unused.
170      * </p>
171      * <p>
172      * Nested predicates are not currently allowed.
173      * </p>
174      */
175     // \[(?:(?:\d+(?:,\d+)*)|\*)\]|(?:\[[^\]\+\-\*=\!><'"\s]+\])$|(\[[^\]]+\])
176     private static final String UNUSABLE_PREDICATE_PATTERN_STRING = "\\[(?:(?:\\d+(?:,\\d+)*)|\\*)\\]|(?:\\[[^\\]\\+\\-\\*=\\!><'\"\\s]+\\])$|(\\[[^\\]]+\\])";
177     private static final Pattern UNUSABLE_PREDICATE_PATTERN = Pattern.compile(UNUSABLE_PREDICATE_PATTERN_STRING);
178 
179     /**
180      * Regular expression used to find all XPath predicates except index and sequence patterns. This pattern results in the
181      * predicates to be removed in group 1.
182      */
183     // \[(?:(?:\d+(?:,\d+)*)|\*)\]|(\[[^\]]+\])
184     private static final String NON_INDEX_PREDICATE_PATTERN_STRING = "\\[(?:(?:\\d+(?:,\\d+)*)|\\*)\\]|(\\[[^\\]]+\\])";
185     private static final Pattern NON_INDEX_PREDICATE_PATTERN = Pattern.compile(NON_INDEX_PREDICATE_PATTERN_STRING);
186 
187     /**
188      * The regular expression that is used to extract the repository name, workspace name, and path from an path expression (or a
189      * real path). The regular expression is <code>((([^:/]*):)?(([^:/]*):))?(.*)</code>. Group 3 will contain the repository
190      * name, group 5 the workspace name, and group 6 the path.
191      */
192     private static final String REPOSITORY_AND_WORKSPACE_AND_PATH_PATTERN_STRING = "((([^:/]*):)?(([^:/]*):))?(.*)";
193     private static final Pattern REPOSITORY_AND_WORKSPACE_AND_PATH_PATTERN = Pattern.compile(REPOSITORY_AND_WORKSPACE_AND_PATH_PATTERN_STRING);
194 
195     private final String expression;
196 
197     /**
198      * This is the pattern that is used to determine if the particular path is from a particular repository. This pattern will be
199      * null if the expression does not constrain the repository name.
200      */
201     private final Pattern repositoryPattern;
202 
203     /**
204      * This is the pattern that is used to determine if the particular path is from a particular workspace. This pattern will be
205      * null if the expression does not constrain the workspace name.
206      */
207     private final Pattern workspacePattern;
208     /**
209      * This is the pattern that is used to determine if there is a match with particular paths.
210      */
211     private final Pattern matchPattern;
212     /**
213      * This is the pattern that is used to determine which parts of the particular input paths are included in the
214      * {@link Matcher#getSelectedNodePath() selected path}, only after the input path has already matched.
215      */
216     private final Pattern selectPattern;
217 
218     /**
219      * Create the supplied expression.
220      * 
221      * @param expression the expression
222      * @throws IllegalArgumentException if the expression is null
223      * @throws InvalidPathExpressionException if the expression is blank or is not a valid expression
224      */
225     public PathExpression( String expression ) throws InvalidPathExpressionException {
226         CheckArg.isNotNull(expression, "path expression");
227         this.expression = expression.trim();
228         if (this.expression.length() == 0) {
229             throw new InvalidPathExpressionException(GraphI18n.pathExpressionMayNotBeBlank.text());
230         }
231 
232         // Separate out the repository name, workspace name, and path fragments into separate match patterns ...
233         RepositoryPath repoPath = parseRepositoryPath(this.expression);
234         if (repoPath == null) {
235             throw new InvalidPathExpressionException(GraphI18n.pathExpressionHasInvalidMatch.text(this.expression,
236                                                                                                   this.expression));
237         }
238         String repoPatternStr = repoPath.repositoryName != null ? repoPath.repositoryName : ".*";
239         String workPatternStr = repoPath.workspaceName != null ? repoPath.workspaceName : ".*";
240         String pathPatternStr = repoPath.path;
241         this.repositoryPattern = Pattern.compile(repoPatternStr);
242         this.workspacePattern = Pattern.compile(workPatternStr);
243 
244         // Build the repository match pattern ...
245 
246         // Build the match pattern, which determines whether a path matches the condition ...
247         String matchString = pathPatternStr;
248         try {
249             matchString = removeUnusedPredicates(matchString);
250             matchString = replaceXPathPatterns(matchString);
251             this.matchPattern = Pattern.compile(matchString, Pattern.CASE_INSENSITIVE);
252         } catch (PatternSyntaxException e) {
253             String msg = GraphI18n.pathExpressionHasInvalidMatch.text(matchString, this.expression);
254             throw new InvalidPathExpressionException(msg, e);
255         }
256         // Build the select pattern, which determines the path that will be selected ...
257         String selectString = pathPatternStr;
258         try {
259             selectString = removeAllPredicatesExceptIndexes(selectString);
260             selectString = replaceXPathPatterns(selectString);
261             selectString = "(" + selectString + ").*"; // group 1 will have selected path ...
262             this.selectPattern = Pattern.compile(selectString, Pattern.CASE_INSENSITIVE);
263         } catch (PatternSyntaxException e) {
264             String msg = GraphI18n.pathExpressionHasInvalidSelect.text(selectString, this.expression);
265             throw new InvalidPathExpressionException(msg, e);
266         }
267     }
268 
269     /**
270      * @return expression
271      */
272     public String getExpression() {
273         return expression;
274     }
275 
276     /**
277      * Replace certain XPath patterns that are not used or understood.
278      * 
279      * @param expression the input regular expressions string; may not be null
280      * @return the regular expression with all unused XPath patterns removed; never null
281      */
282     protected String removeUnusedPredicates( String expression ) {
283         assert expression != null;
284         java.util.regex.Matcher matcher = UNUSABLE_PREDICATE_PATTERN.matcher(expression);
285         StringBuffer sb = new StringBuffer();
286         if (matcher.find()) {
287             do {
288                 // Remove those predicates that show up in group 1 ...
289                 String predicateStr = matcher.group(0);
290                 String unusablePredicateStr = matcher.group(1);
291                 if (unusablePredicateStr != null) {
292                     predicateStr = "";
293                 }
294                 matcher.appendReplacement(sb, predicateStr);
295             } while (matcher.find());
296             matcher.appendTail(sb);
297             expression = sb.toString();
298         }
299         return expression;
300     }
301 
302     /**
303      * Remove all XPath predicates from the supplied regular expression string.
304      * 
305      * @param expression the input regular expressions string; may not be null
306      * @return the regular expression with all XPath predicates removed; never null
307      */
308     protected String removeAllPredicatesExceptIndexes( String expression ) {
309         assert expression != null;
310         java.util.regex.Matcher matcher = NON_INDEX_PREDICATE_PATTERN.matcher(expression);
311         StringBuffer sb = new StringBuffer();
312         if (matcher.find()) {
313             do {
314                 // Remove those predicates that show up in group 1 ...
315                 String predicateStr = matcher.group(0);
316                 String unusablePredicateStr = matcher.group(1);
317                 if (unusablePredicateStr != null) {
318                     predicateStr = "";
319                 }
320                 matcher.appendReplacement(sb, predicateStr);
321             } while (matcher.find());
322             matcher.appendTail(sb);
323             expression = sb.toString();
324         }
325         return expression;
326     }
327 
328     /**
329      * Replace certain XPath patterns, including some predicates, with substrings that are compatible with regular expressions.
330      * 
331      * @param expression the input regular expressions string; may not be null
332      * @return the regular expression with XPath patterns replaced with regular expression fragments; never null
333      */
334     protected String replaceXPathPatterns( String expression ) {
335         assert expression != null;
336         // replace 2 or more sequential '|' characters in an OR expression
337         expression = expression.replaceAll("[\\|]{2,}", "|");
338         // if there is an empty expression in an OR expression, make the whole segment optional ...
339         // (e.g., "/a/b/(c|)/d" => "a/b(/(c))?/d"
340         expression = expression.replaceAll("/(\\([^|]+)(\\|){2,}([^)]+\\))", "(/$1$2$3)?");
341         expression = expression.replaceAll("/\\(\\|+([^)]+)\\)", "(?:/($1))?");
342         expression = expression.replaceAll("/\\((([^|]+)(\\|[^|]+)*)\\|+\\)", "(?:/($1))?");
343 
344         // // Allow any path (that doesn't contain an explicit counter) to contain a counter,
345         // // done by replacing any '/' or '|' that isn't preceded by ']' or '*' or '/' or '(' with '(\[\d+\])?/'...
346         // input = input.replaceAll("(?<=[^\\]\\*/(])([/|])", "(?:\\\\[\\\\d+\\\\])?$1");
347 
348         // Does the path contain any '[]' or '[*]' or '[0]' or '[n]' (where n is any positive integers)...
349         // '[*]/' => '(\[\d+\])?/'
350         expression = expression.replaceAll("\\[\\]", "(?:\\\\[\\\\d+\\\\])?"); // index is optional
351         // '[]/' => '(\[\d+\])?/'
352         expression = expression.replaceAll("\\[[*]\\]", "(?:\\\\[\\\\d+\\\\])?"); // index is optional
353         // '[0]/' => '(\[0\])?/'
354         expression = expression.replaceAll("\\[0\\]", "(?:\\\\[0\\\\])?"); // index is optional
355         // '[n]/' => '\[n\]/'
356         expression = expression.replaceAll("\\[([1-9]\\d*)\\]", "\\\\[$1\\\\]"); // index is required
357 
358         // Change any other end predicates to not be wrapped by braces but to begin with a slash ...
359         // ...'[x]' => ...'/x'
360         expression = expression.replaceAll("(?<!\\\\)\\[([^\\]]*)\\]$", "/$1");
361 
362         // Replace all '[n,m,o,p]' type sequences with '[(n|m|o|p)]'
363         java.util.regex.Matcher matcher = SEQUENCE_PATTERN.matcher(expression);
364         StringBuffer sb = new StringBuffer();
365         boolean result = matcher.find();
366         if (result) {
367             do {
368                 String sequenceStr = matcher.group(1);
369                 boolean optional = false;
370                 if (sequenceStr.startsWith("0,")) {
371                     sequenceStr = sequenceStr.replaceFirst("^0,", "");
372                     optional = true;
373                 }
374                 if (sequenceStr.endsWith(",0")) {
375                     sequenceStr = sequenceStr.replaceFirst(",0$", "");
376                     optional = true;
377                 }
378                 if (sequenceStr.contains(",0,")) {
379                     sequenceStr = sequenceStr.replaceAll(",0,", ",");
380                     optional = true;
381                 }
382                 sequenceStr = sequenceStr.replaceAll(",", "|");
383                 String replacement = "\\\\[(?:" + sequenceStr + ")\\\\]";
384                 if (optional) {
385                     replacement = "(?:" + replacement + ")?";
386                 }
387                 matcher.appendReplacement(sb, replacement);
388                 result = matcher.find();
389             } while (result);
390             matcher.appendTail(sb);
391             expression = sb.toString();
392         }
393 
394         // Order is important here
395         expression = expression.replaceAll("[*]([^/(\\\\])", "[^/$1]*$1"); // '*' not followed by '/', '\\', or '('
396         expression = expression.replaceAll("(?<!\\[\\^/\\])[*]", "[^/]*");
397         expression = expression.replaceAll("[/]{2,}$", "(?:/[^/]*)*"); // ending '//'
398         expression = expression.replaceAll("[/]{2,}", "(?:/[^/]*)*/"); // other '//'
399         return expression;
400     }
401 
402     /**
403      * @return the expression
404      */
405     public String getSelectExpression() {
406         return this.expression;
407     }
408 
409     /**
410      * {@inheritDoc}
411      */
412     @Override
413     public int hashCode() {
414         return this.expression.hashCode();
415     }
416 
417     /**
418      * {@inheritDoc}
419      */
420     @Override
421     public boolean equals( Object obj ) {
422         if (obj == this) return true;
423         if (obj instanceof PathExpression) {
424             PathExpression that = (PathExpression)obj;
425             if (!this.expression.equalsIgnoreCase(that.expression)) return false;
426             return true;
427         }
428         return false;
429     }
430 
431     /**
432      * {@inheritDoc}
433      */
434     @Override
435     public String toString() {
436         return this.expression;
437     }
438 
439     /**
440      * Obtain a Matcher that can be used to convert the supplied absolute path (with repository name and workspace name) into an
441      * output repository, and output workspace name, and output path.
442      * 
443      * @param absolutePath the path, of the form <code>{repoName}:{workspaceName}:{absPath}</code>, where
444      *        <code>{repoName}:{workspaceName}:</code> is optional
445      * @return the matcher; never null
446      */
447     public Matcher matcher( String absolutePath ) {
448         // Extra the repository name, workspace name and absPath from the supplied path ...
449         RepositoryPath repoPath = parseRepositoryPath(absolutePath);
450         if (repoPath == null) {
451             // No match, so return immediately ...
452             return new Matcher(null, absolutePath, null, null, null);
453         }
454         String repoName = repoPath.repositoryName != null ? repoPath.repositoryName : "";
455         String workspaceName = repoPath.workspaceName != null ? repoPath.workspaceName : "";
456         String path = repoPath.path;
457 
458         // Determine if the input repository matches the repository name pattern ...
459         if (!repositoryPattern.matcher(repoName).matches() || !workspacePattern.matcher(workspaceName).matches()) {
460             // No match, so return immediately ...
461             return new Matcher(null, path, null, null, null);
462         }
463 
464         // Determine if the input path match the select expression ...
465         String originalAbsolutePath = path;
466         // if (!absolutePath.endsWith("/")) absolutePath = absolutePath + "/";
467         // Remove all trailing '/' ...
468         path = path.replaceAll("/+$", "");
469 
470         // See if the supplied absolute path matches the pattern ...
471         final java.util.regex.Matcher matcher = this.matchPattern.matcher(path);
472         if (!matcher.matches()) {
473             // No match, so return immediately ...
474             return new Matcher(matcher, originalAbsolutePath, null, null, null);
475         }
476 
477         // The absolute path does match the pattern, so use the select pattern and try to grab the selected path ...
478         final java.util.regex.Matcher selectMatcher = this.selectPattern.matcher(path);
479         if (!selectMatcher.matches()) {
480             // Nothing can be selected, so return immediately ...
481             return new Matcher(matcher, null, null, null, null);
482         }
483         // Grab the selected path ...
484         String selectedPath = selectMatcher.group(1);
485 
486         // Remove the trailing '/@property' ...
487         selectedPath = selectedPath.replaceAll("/@[^/\\[\\]]+$", "");
488 
489         return new Matcher(matcher, originalAbsolutePath, repoName, workspaceName, selectedPath);
490     }
491 
492     @Immutable
493     public static class Matcher {
494 
495         private final String inputPath;
496         private final String selectedRepository;
497         private final String selectedWorkspace;
498         private final String selectedPath;
499         private final java.util.regex.Matcher inputMatcher;
500         private final int hc;
501 
502         protected Matcher( java.util.regex.Matcher inputMatcher,
503                            String inputPath,
504                            String selectedRepository,
505                            String selectedWorkspace,
506                            String selectedPath ) {
507             this.inputMatcher = inputMatcher;
508             this.inputPath = inputPath;
509             this.selectedRepository = selectedRepository == null || selectedRepository.length() == 0 ? null : selectedRepository;
510             this.selectedWorkspace = selectedWorkspace == null || selectedWorkspace.length() == 0 ? null : selectedWorkspace;
511             this.selectedPath = selectedPath;
512             this.hc = HashCode.compute(this.inputPath, this.selectedPath);
513         }
514 
515         public boolean matches() {
516             return this.inputMatcher != null && this.selectedPath != null;
517         }
518 
519         /**
520          * @return inputPath
521          */
522         public String getInputPath() {
523             return this.inputPath;
524         }
525 
526         /**
527          * @return selectPattern
528          */
529         public String getSelectedNodePath() {
530             return this.selectedPath;
531         }
532 
533         /**
534          * Get the name of the selected repository.
535          * 
536          * @return the repository name, or null if there is none specified
537          */
538         public String getSelectedRepositoryName() {
539             return this.selectedRepository;
540         }
541 
542         /**
543          * Get the name of the selected workspace.
544          * 
545          * @return the workspace name, or null if there is none specified
546          */
547         public String getSelectedWorkspaceName() {
548             return this.selectedWorkspace;
549         }
550 
551         public int groupCount() {
552             if (this.inputMatcher == null) return 0;
553             return this.inputMatcher.groupCount();
554         }
555 
556         public String group( int groupNumber ) {
557             return this.inputMatcher.group(groupNumber);
558         }
559 
560         /**
561          * {@inheritDoc}
562          */
563         @Override
564         public int hashCode() {
565             return this.hc;
566         }
567 
568         /**
569          * {@inheritDoc}
570          */
571         @Override
572         public boolean equals( Object obj ) {
573             if (obj == this) return true;
574             if (obj instanceof PathExpression.Matcher) {
575                 PathExpression.Matcher that = (PathExpression.Matcher)obj;
576                 if (!this.inputPath.equalsIgnoreCase(that.inputPath)) return false;
577                 if (!this.selectedPath.equalsIgnoreCase(that.selectedPath)) return false;
578                 return true;
579             }
580             return false;
581         }
582 
583         /**
584          * {@inheritDoc}
585          */
586         @Override
587         public String toString() {
588             return this.selectedPath;
589         }
590     }
591 
592     /**
593      * Regular expression used to determine if the expression matches any single-level wildcard.
594      */
595     // /*(?:[*.](?:\[\*?\])?/*)*
596     private static final String ANYTHING_PATTERN_STRING = "/*(?:[*.](?:\\[\\*?\\])?/*)*";
597     private static final Pattern ANYTHING_PATTERN = Pattern.compile(ANYTHING_PATTERN_STRING);
598 
599     /**
600      * Return whether this expression matches anything and therefore is not restrictive. These include expressions of any nodes ("
601      * <code>/</code>"), any sequence of nodes ("<code>//</code>"), the self reference ("<code>.</code>"), or wildcard ("
602      * <code>*</code>", "<code>*[]</code>" or "<code>*[*]</code>"). Combinations of these individual expressions are also
603      * considered to match anything.
604      * 
605      * @return true if the expression matches anything, or false otherwise
606      */
607     public boolean matchesAnything() {
608         return ANYTHING_PATTERN.matcher(expression).matches();
609     }
610 
611     public static PathExpression all() {
612         return ALL_PATHS_EXPRESSION;
613     }
614 
615     private static final PathExpression ALL_PATHS_EXPRESSION = PathExpression.compile("//");
616 
617     /**
618      * Parse a path of the form <code>{repoName}:{workspaceName}:{absolutePath}</code> or <code>{absolutePath}</code>.
619      * 
620      * @param path the path
621      * @return the repository path, or null if the supplied path doesn't match any of the path patterns
622      */
623     public static RepositoryPath parseRepositoryPath( String path ) {
624         // Extra the repository name, workspace name and absPath from the supplied path ...
625         java.util.regex.Matcher pathMatcher = REPOSITORY_AND_WORKSPACE_AND_PATH_PATTERN.matcher(path);
626         if (!pathMatcher.matches()) {
627             // No match ...
628             return null;
629         }
630         String repoName = pathMatcher.group(3);
631         String workspaceName = pathMatcher.group(5);
632         String absolutePath = pathMatcher.group(6);
633         if (repoName == null || repoName.length() == 0 || repoName.trim().length() == 0) repoName = null;
634         if (workspaceName == null || workspaceName.length() == 0 || workspaceName.trim().length() == 0) workspaceName = null;
635         return new RepositoryPath(repoName, workspaceName, absolutePath);
636     }
637 
638     @Immutable
639     public static class RepositoryPath {
640         public final String repositoryName;
641         public final String workspaceName;
642         public final String path;
643 
644         public RepositoryPath( String repositoryName,
645                                String workspaceName,
646                                String path ) {
647             this.repositoryName = repositoryName;
648             this.workspaceName = workspaceName;
649             this.path = path;
650         }
651 
652         /**
653          * {@inheritDoc}
654          * 
655          * @see java.lang.Object#hashCode()
656          */
657         @Override
658         public int hashCode() {
659             return path.hashCode();
660         }
661 
662         /**
663          * {@inheritDoc}
664          * 
665          * @see java.lang.Object#equals(java.lang.Object)
666          */
667         @Override
668         public boolean equals( Object obj ) {
669             if (obj == this) return true;
670             if (obj instanceof RepositoryPath) {
671                 RepositoryPath that = (RepositoryPath)obj;
672                 if (!ObjectUtil.isEqualWithNulls(this.repositoryName, that.repositoryName)) return false;
673                 if (!ObjectUtil.isEqualWithNulls(this.workspaceName, that.workspaceName)) return false;
674                 return this.path.equals(that.path);
675             }
676             return false;
677         }
678 
679         /**
680          * {@inheritDoc}
681          * 
682          * @see java.lang.Object#toString()
683          */
684         @Override
685         public String toString() {
686             return (repositoryName != null ? repositoryName : "") + ":" + (workspaceName != null ? workspaceName : "") + ":"
687                    + path;
688         }
689 
690         public RepositoryPath withRepositoryName( String repositoryName ) {
691             return new RepositoryPath(repositoryName, workspaceName, path);
692         }
693 
694         public RepositoryPath withWorkspaceName( String workspaceName ) {
695             return new RepositoryPath(repositoryName, workspaceName, path);
696         }
697 
698         public RepositoryPath withPath( String path ) {
699             return new RepositoryPath(repositoryName, workspaceName, path);
700         }
701     }
702 
703 }