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 }