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.connector.federation;
25  
26  import java.io.Serializable;
27  import java.lang.reflect.Method;
28  import java.util.ArrayList;
29  import java.util.Collections;
30  import java.util.HashSet;
31  import java.util.Iterator;
32  import java.util.LinkedList;
33  import java.util.List;
34  import java.util.Set;
35  import java.util.concurrent.CopyOnWriteArrayList;
36  import java.util.regex.Matcher;
37  import java.util.regex.Pattern;
38  import net.jcip.annotations.Immutable;
39  import org.modeshape.common.text.TextEncoder;
40  import org.modeshape.common.util.CheckArg;
41  import org.modeshape.common.util.HashCode;
42  import org.modeshape.common.util.Logger;
43  import org.modeshape.graph.ExecutionContext;
44  import org.modeshape.graph.GraphI18n;
45  import org.modeshape.graph.connector.RepositorySource;
46  import org.modeshape.graph.property.NamespaceRegistry;
47  import org.modeshape.graph.property.Path;
48  import org.modeshape.graph.property.PathFactory;
49  
50  /**
51   * A projection of content from a source into the integrated/federated repository. Each project consists of a set of {@link Rule
52   * rules} for a particular source, where each rule defines how content within a source is
53   * {@link Rule#getPathInRepository(Path, PathFactory) is project into the repository} and how the repository content is
54   * {@link Rule#getPathInSource(Path, PathFactory) projected into the source}. Different rule subclasses are used for different
55   * types.
56   */
57  @Immutable
58  public class Projection implements Comparable<Projection>, Serializable {
59  
60      /**
61       * Initial version
62       */
63      private static final long serialVersionUID = 1L;
64      protected static final List<Method> parserMethods;
65      private static final Logger LOGGER = Logger.getLogger(Projection.class);
66  
67      static {
68          parserMethods = new CopyOnWriteArrayList<Method>();
69          try {
70              parserMethods.add(Projection.class.getDeclaredMethod("parsePathRule", String.class, ExecutionContext.class));
71          } catch (Throwable err) {
72              LOGGER.error(err, GraphI18n.errorAddingProjectionRuleParseMethod);
73          }
74      }
75  
76      /**
77       * Add a static method that can be used to parse {@link Rule#getString(NamespaceRegistry, TextEncoder) rule definition
78       * strings}. These methods must be static, must accept a {@link String} definition as the first parameter and an
79       * {@link ExecutionContext} environment reference as the second parameter, and should return the resulting {@link Rule} (or
80       * null if the definition format could not be understood by the method. Any exceptions during
81       * {@link Method#invoke(Object, Object...) invocation} will be logged at the
82       * {@link Logger#trace(Throwable, String, Object...) trace} level.
83       * 
84       * @param method the method to be added
85       * @see #addRuleParser(ClassLoader, String, String)
86       */
87      public static void addRuleParser( Method method ) {
88          if (method != null) parserMethods.add(method);
89      }
90  
91      /**
92       * Add a static method that can be used to parse {@link Rule#getString(NamespaceRegistry, TextEncoder) rule definition
93       * strings}. These methods must be static, must accept a {@link String} definition as the first parameter and an
94       * {@link ExecutionContext} environment reference as the second parameter, and should return the resulting {@link Rule} (or
95       * null if the definition format could not be understood by the method. Any exceptions during
96       * {@link Method#invoke(Object, Object...) invocation} will be logged at the
97       * {@link Logger#trace(Throwable, String, Object...) trace} level.
98       * 
99       * @param classLoader the class loader that should be used to load the class on which the method is defined; may not be null
100      * @param className the name of the class on which the static method is defined; may not be null
101      * @param methodName the name of the method
102      * @throws SecurityException if there is a security exception while loading the class or getting the method
103      * @throws NoSuchMethodException if the method does not exist on the class
104      * @throws ClassNotFoundException if the class could not be found given the supplied class loader
105      * @throws IllegalArgumentException if the class loader reference is null, or if the class name or method name are null or
106      *         empty
107      * @see #addRuleParser(Method)
108      */
109     public static void addRuleParser( ClassLoader classLoader,
110                                       String className,
111                                       String methodName ) throws SecurityException, NoSuchMethodException, ClassNotFoundException {
112         CheckArg.isNotNull(classLoader, "classLoader");
113         CheckArg.isNotEmpty(className, "className");
114         CheckArg.isNotEmpty(methodName, "methodName");
115         Class<?> clazz = Class.forName(className, true, classLoader);
116         parserMethods.add(clazz.getMethod(className, String.class, ExecutionContext.class));
117     }
118 
119     /**
120      * Remove the rule parser method.
121      * 
122      * @param method the method to remove
123      * @return true if the method was removed, or false if the method was not a registered rule parser method
124      */
125     public static boolean removeRuleParser( Method method ) {
126         return parserMethods.remove(method);
127     }
128 
129     /**
130      * Remove the rule parser method.
131      * 
132      * @param declaringClassName the name of the class on which the static method is defined; may not be null
133      * @param methodName the name of the method
134      * @return true if the method was removed, or false if the method was not a registered rule parser method
135      * @throws IllegalArgumentException if the class loader reference is null, or if the class name or method name are null or
136      *         empty
137      */
138     public static boolean removeRuleParser( String declaringClassName,
139                                             String methodName ) {
140         CheckArg.isNotEmpty(declaringClassName, "declaringClassName");
141         CheckArg.isNotEmpty(methodName, "methodName");
142         for (Method method : parserMethods) {
143             if (method.getName().equals(methodName) && method.getDeclaringClass().getName().equals(declaringClassName)) {
144                 return parserMethods.remove(method);
145             }
146         }
147         return false;
148     }
149 
150     /**
151      * Parse the string form of a rule definition and return the rule
152      * 
153      * @param definition the definition of the rule that is to be parsed
154      * @param context the environment in which this method is being executed; may not be null
155      * @return the rule, or null if the definition could not be parsed
156      */
157     public static Rule fromString( String definition,
158                                    ExecutionContext context ) {
159         CheckArg.isNotNull(context, "env");
160         definition = definition != null ? definition.trim() : "";
161         if (definition.length() == 0) return null;
162         for (Method method : parserMethods) {
163             try {
164                 Rule rule = (Rule)method.invoke(null, definition, context);
165                 if (rule != null) return rule;
166             } catch (Throwable err) {
167                 String msg = "Error while parsing project rule definition \"{0}\" using {1}";
168                 context.getLogger(Projection.class).trace(err, msg, definition, method);
169             }
170         }
171         return null;
172     }
173 
174     /**
175      * Pattern that identifies the form:
176      * 
177      * <pre>
178      *    repository_path =&gt; source_path [$ exception ]*
179      * </pre>
180      * 
181      * where the following groups are captured on the first call to {@link Matcher#find()}:
182      * <ol>
183      * <li><code>repository_path</code></li>
184      * <li><code>source_path</code></li>
185      * </ol>
186      * and the following groups are captured on subsequent calls to {@link Matcher#find()}:
187      * <ol>
188      * <li>exception</code></li>
189      * </ol>
190      * <p>
191      * The regular expression is:
192      * 
193      * <pre>
194      * ((?:[&circ;=$]|=(?!&gt;))+)(?:(?:=&gt;((?:[&circ;=$]|=(?!&gt;))+))( \$ (?:(?:[&circ;=]|=(?!&gt;))+))*)?
195      * </pre>
196      * 
197      * </p>
198      */
199     protected static final String PATH_RULE_PATTERN_STRING = "((?:[^=$]|=(?!>))+)(?:(?:=>((?:[^=$]|=(?!>))+))( \\$ (?:(?:[^=]|=(?!>))+))*)?";
200     protected static final Pattern PATH_RULE_PATTERN = Pattern.compile(PATH_RULE_PATTERN_STRING);
201 
202     /**
203      * Parse the string definition of a {@link PathRule}. This method is automatically registered in the {@link #parserMethods
204      * parser methods} by the static initializer of {@link Projection}.
205      * 
206      * @param definition the definition
207      * @param context the environment
208      * @return the path rule, or null if the definition is not in the right form
209      */
210     public static PathRule parsePathRule( String definition,
211                                           ExecutionContext context ) {
212         definition = definition != null ? definition.trim() : "";
213         if (definition.length() == 0) return null;
214         Matcher matcher = PATH_RULE_PATTERN.matcher(definition);
215         if (!matcher.find()) return null;
216         String reposPathStr = matcher.group(1);
217         String sourcePathStr = matcher.group(2);
218         if (reposPathStr == null || sourcePathStr == null) return null;
219         reposPathStr = reposPathStr.trim();
220         sourcePathStr = sourcePathStr.trim();
221         if (reposPathStr.length() == 0 || sourcePathStr.length() == 0) return null;
222         PathFactory pathFactory = context.getValueFactories().getPathFactory();
223         Path repositoryPath = pathFactory.create(reposPathStr);
224         Path sourcePath = pathFactory.create(sourcePathStr);
225 
226         // Grab the exceptions ...
227         List<Path> exceptions = new LinkedList<Path>();
228         while (matcher.find()) {
229             String exceptionStr = matcher.group(1);
230             Path exception = pathFactory.create(exceptionStr);
231             exceptions.add(exception);
232         }
233         return new PathRule(repositoryPath, sourcePath, exceptions);
234     }
235 
236     private final String sourceName;
237     private final String workspaceName;
238     private final List<Rule> rules;
239     private final boolean simple;
240     private final boolean readOnly;
241     private final int hc;
242 
243     /**
244      * Create a new federated projection for the supplied source, using the supplied rules.
245      * 
246      * @param sourceName the name of the source
247      * @param workspaceName the name of the workspace in the source; may be null if the default workspace is to be used
248      * @param readOnly true if this projection is considered read-only, or false if the content of the projection may be modified
249      *        by the federated clients
250      * @param rules the projection rules
251      * @throws IllegalArgumentException if the source name or rule array is null, empty, or contains all nulls
252      */
253     public Projection( String sourceName,
254                        String workspaceName,
255                        boolean readOnly,
256                        Rule... rules ) {
257         CheckArg.isNotEmpty(sourceName, "sourceName");
258         CheckArg.isNotEmpty(rules, "rules");
259         this.sourceName = sourceName;
260         this.workspaceName = workspaceName;
261         List<Rule> rulesList = new ArrayList<Rule>();
262         for (Rule rule : rules) {
263             if (rule != null) rulesList.add(rule);
264         }
265         this.readOnly = readOnly;
266         this.rules = Collections.unmodifiableList(rulesList);
267         CheckArg.isNotEmpty(this.rules, "rules");
268         this.simple = computeSimpleProjection(this.rules);
269         this.hc = HashCode.compute(this.sourceName, this.workspaceName);
270     }
271 
272     /**
273      * Get the name of the source to which this projection applies.
274      * 
275      * @return the source name
276      * @see RepositorySource#getName()
277      */
278     public String getSourceName() {
279         return sourceName;
280     }
281 
282     /**
283      * Get the name of the workspace in the source to which this projection applies.
284      * 
285      * @return the workspace name, or null if the default workspace of the {@link #getSourceName() source} is to be used
286      */
287     public String getWorkspaceName() {
288         return workspaceName;
289     }
290 
291     /**
292      * Get the rules that define this projection.
293      * 
294      * @return the unmodifiable list of immutable rules; never null
295      */
296     public List<Rule> getRules() {
297         return rules;
298     }
299 
300     /**
301      * Get the paths in the source that correspond to the supplied path within the repository. This method computes the paths
302      * given all of the rules. In general, most sources will probably project a node onto a single repository node. However, some
303      * sources may be configured such that the same node in the repository is a projection of multiple nodes within the source.
304      * 
305      * @param canonicalPathInRepository the canonical path of the node within the repository; may not be null
306      * @param factory the path factory; may not be null
307      * @return the set of unique paths in the source projected from the repository path; never null
308      * @throws IllegalArgumentException if the factory reference is null
309      */
310     public Set<Path> getPathsInSource( Path canonicalPathInRepository,
311                                        PathFactory factory ) {
312         CheckArg.isNotNull(factory, "factory");
313         assert canonicalPathInRepository == null ? true : canonicalPathInRepository.equals(canonicalPathInRepository.getCanonicalPath());
314         Set<Path> paths = new HashSet<Path>();
315         for (Rule rule : getRules()) {
316             Path pathInSource = rule.getPathInSource(canonicalPathInRepository, factory);
317             if (pathInSource != null) paths.add(pathInSource);
318         }
319         return paths;
320     }
321 
322     /**
323      * Get the paths in the repository that correspond to the supplied path within the source. This method computes the paths
324      * given all of the rules. In general, most sources will probably project a node onto a single repository node. However, some
325      * sources may be configured such that the same node in the source is projected into multiple nodes within the repository.
326      * 
327      * @param canonicalPathInSource the canonical path of the node within the source; may not be null
328      * @param factory the path factory; may not be null
329      * @return the set of unique paths in the repository projected from the source path; never null
330      * @throws IllegalArgumentException if the factory reference is null
331      */
332     public Set<Path> getPathsInRepository( Path canonicalPathInSource,
333                                            PathFactory factory ) {
334         CheckArg.isNotNull(factory, "factory");
335         assert canonicalPathInSource == null ? true : canonicalPathInSource.equals(canonicalPathInSource.getCanonicalPath());
336         Set<Path> paths = new HashSet<Path>();
337         for (Rule rule : getRules()) {
338             Path pathInRepository = rule.getPathInRepository(canonicalPathInSource, factory);
339             if (pathInRepository != null) paths.add(pathInRepository);
340         }
341         return paths;
342     }
343 
344     /**
345      * Get the paths in the repository that serve as top-level nodes exposed by this projection.
346      * 
347      * @param factory the path factory that can be used to create new paths; may not be null
348      * @return the list of top-level paths, in the proper order and containing no duplicates; never null
349      */
350     public List<Path> getTopLevelPathsInRepository( PathFactory factory ) {
351         CheckArg.isNotNull(factory, "factory");
352         List<Rule> rules = getRules();
353         Set<Path> uniquePaths = new HashSet<Path>();
354         List<Path> paths = new ArrayList<Path>(rules.size());
355         for (Rule rule : getRules()) {
356             for (Path path : rule.getTopLevelPathsInRepository(factory)) {
357                 if (!uniquePaths.contains(path)) {
358                     paths.add(path);
359                     uniquePaths.add(path);
360                 }
361             }
362         }
363         return paths;
364     }
365 
366     /**
367      * Determine whether the supplied repositoryPath is considered one of the top-level nodes in this projection.
368      * 
369      * @param repositoryPath path in the repository; may not be null
370      * @return true if the supplied repository path is one of the top-level nodes exposed by this projection, or false otherwise
371      */
372     public boolean isTopLevelPath( Path repositoryPath ) {
373         for (Rule rule : getRules()) {
374             if (rule.isTopLevelPath(repositoryPath)) return true;
375         }
376         return false;
377     }
378 
379     /**
380      * Determine whether this project is a simple projection that only involves for any one repository path no more than a single
381      * source path.
382      * 
383      * @return true if this projection is a simple projection, or false if the projection is not simple (or it cannot be
384      *         determined if it is simple)
385      */
386     public boolean isSimple() {
387         return simple;
388     }
389 
390     /**
391      * Determine whether the content projected by this projection is read-only.
392      * 
393      * @return true if the content is read-only, or false if it can be modified
394      */
395     public boolean isReadOnly() {
396         return readOnly;
397     }
398 
399     protected boolean computeSimpleProjection( List<Rule> rules ) {
400         // Get the set of repository paths for the rules, and see if they overlap ...
401         Set<Path> repositoryPaths = new HashSet<Path>();
402         for (Rule rule : rules) {
403             if (rule instanceof PathRule) {
404                 PathRule pathRule = (PathRule)rule;
405                 Path repoPath = pathRule.getPathInRepository();
406                 if (!repositoryPaths.isEmpty()) {
407                     if (repositoryPaths.contains(repoPath)) return false;
408                     for (Path path : repositoryPaths) {
409                         if (path.isAtOrAbove(repoPath)) return false;
410                         if (repoPath.isAtOrAbove(path)) return false;
411                     }
412                 }
413                 repositoryPaths.add(repoPath);
414             } else {
415                 return false;
416             }
417         }
418         return true;
419     }
420 
421     /**
422      * {@inheritDoc}
423      * 
424      * @see java.lang.Object#hashCode()
425      */
426     @Override
427     public int hashCode() {
428         return this.hc;
429     }
430 
431     /**
432      * {@inheritDoc}
433      * 
434      * @see java.lang.Object#equals(java.lang.Object)
435      */
436     @Override
437     public boolean equals( Object obj ) {
438         if (obj == this) return true;
439         if (obj instanceof Projection) {
440             Projection that = (Projection)obj;
441             if (this.hashCode() != that.hashCode()) return false;
442             if (!this.getSourceName().equals(that.getSourceName())) return false;
443             if (!this.getWorkspaceName().equals(that.getWorkspaceName())) return false;
444             if (!this.getRules().equals(that.getRules())) return false;
445             return true;
446         }
447         return false;
448     }
449 
450     /**
451      * {@inheritDoc}
452      * 
453      * @see java.lang.Comparable#compareTo(java.lang.Object)
454      */
455     public int compareTo( Projection that ) {
456         if (this == that) return 0;
457         int diff = this.getSourceName().compareTo(that.getSourceName());
458         if (diff != 0) return diff;
459         diff = this.getWorkspaceName().compareTo(that.getWorkspaceName());
460         if (diff != 0) return diff;
461         Iterator<Rule> thisIter = this.getRules().iterator();
462         Iterator<Rule> thatIter = that.getRules().iterator();
463         while (thisIter.hasNext() && thatIter.hasNext()) {
464             diff = thisIter.next().compareTo(thatIter.next());
465             if (diff != 0) return diff;
466         }
467         if (thisIter.hasNext()) return 1;
468         if (thatIter.hasNext()) return -1;
469         return 0;
470     }
471 
472     /**
473      * {@inheritDoc}
474      * 
475      * @see java.lang.Object#toString()
476      */
477     @Override
478     public String toString() {
479         StringBuilder sb = new StringBuilder();
480         if (this.workspaceName != null) {
481             sb.append(this.workspaceName);
482             sb.append('@');
483         }
484         sb.append(this.sourceName);
485         sb.append(" { ");
486         boolean first = true;
487         for (Rule rule : this.getRules()) {
488             if (!first) sb.append(" ; ");
489             sb.append(rule.toString());
490             first = false;
491         }
492         sb.append(" }");
493         return sb.toString();
494     }
495 
496     /**
497      * A rule used within a project do define how content within a source is projected into the federated repository. This mapping
498      * is bi-directional, meaning it's possible to determine
499      * <ul>
500      * <li>the path in repository given a path in source; and</li>
501      * <li>the path in source given a path in repository.</li>
502      * </ul>
503      * 
504      * @author Randall Hauch
505      */
506     @Immutable
507     public static abstract class Rule implements Comparable<Rule> {
508 
509         /**
510          * Get the paths in the repository that serve as top-level nodes exposed by this rule.
511          * 
512          * @param factory the path factory that can be used to create new paths; may not be null
513          * @return the list of top-level paths, which are ordered and which must be unique; never null
514          */
515         public abstract List<Path> getTopLevelPathsInRepository( PathFactory factory );
516 
517         /**
518          * Determine if the supplied path is the same as one of the top-level nodes exposed by this rule.
519          * 
520          * @param path the path; may not be null
521          * @return true if the supplied path is also one of the {@link #getTopLevelPathsInRepository(PathFactory) top-level paths}
522          *         , or false otherwise
523          */
524         public abstract boolean isTopLevelPath( Path path );
525 
526         /**
527          * Get the path in source that is projected from the supplied repository path, or null if the supplied repository path is
528          * not projected into the source.
529          * 
530          * @param pathInRepository the path in the repository; may not be null
531          * @param factory the path factory; may not be null
532          * @return the path in source if it is projected by this rule, or null otherwise
533          */
534         public abstract Path getPathInSource( Path pathInRepository,
535                                               PathFactory factory );
536 
537         /**
538          * Get the path in repository that is projected from the supplied source path, or null if the supplied source path is not
539          * projected into the repository.
540          * 
541          * @param pathInSource the path in the source; may not be null
542          * @param factory the path factory; may not be null
543          * @return the path in repository if it is projected by this rule, or null otherwise
544          */
545         public abstract Path getPathInRepository( Path pathInSource,
546                                                   PathFactory factory );
547 
548         public abstract String getString( NamespaceRegistry registry,
549                                           TextEncoder encoder );
550 
551         public abstract String getString( TextEncoder encoder );
552 
553         public abstract String getString();
554     }
555 
556     /**
557      * A rule that is defined with a single {@link #getPathInSource() path in source} and a single {@link #getPathInRepository()
558      * path in repository}, and which has a set of {@link #getExceptionsToRule() path exceptions} (relative paths below the path
559      * in source).
560      * 
561      * @author Randall Hauch
562      */
563     @Immutable
564     public static class PathRule extends Rule {
565         /** The path of the content as known to the source */
566         private final Path sourcePath;
567         /** The path where the content is to be placed ("projected") into the repository */
568         private final Path repositoryPath;
569         /** The paths (relative to the source path) that identify exceptions to this rule */
570         private final List<Path> exceptions;
571         private final int hc;
572         private final List<Path> topLevelRepositoryPaths;
573 
574         public PathRule( Path repositoryPath,
575                          Path sourcePath ) {
576             this(repositoryPath, sourcePath, (Path[])null);
577         }
578 
579         public PathRule( Path repositoryPath,
580                          Path sourcePath,
581                          Path... exceptions ) {
582             CheckArg.isNotNull(sourcePath, "sourcePath");
583             CheckArg.isNotNull(repositoryPath, "repositoryPath");
584             this.sourcePath = sourcePath;
585             this.repositoryPath = repositoryPath;
586             if (exceptions == null || exceptions.length == 0) {
587                 this.exceptions = Collections.emptyList();
588             } else {
589                 List<Path> exceptionList = new ArrayList<Path>();
590                 for (Path exception : exceptions) {
591                     if (exception != null) exceptionList.add(exception);
592                 }
593                 this.exceptions = Collections.unmodifiableList(exceptionList);
594             }
595             this.hc = HashCode.compute(sourcePath, repositoryPath, exceptions);
596             if (this.exceptions != null) {
597                 for (Path path : this.exceptions) {
598                     if (path.isAbsolute()) {
599                         throw new IllegalArgumentException(GraphI18n.pathIsNotRelative.text(path));
600                     }
601                 }
602             }
603             this.topLevelRepositoryPaths = Collections.singletonList(getPathInRepository());
604         }
605 
606         public PathRule( Path repositoryPath,
607                          Path sourcePath,
608                          List<Path> exceptions ) {
609             CheckArg.isNotNull(sourcePath, "sourcePath");
610             CheckArg.isNotNull(repositoryPath, "repositoryPath");
611             this.sourcePath = sourcePath;
612             this.repositoryPath = repositoryPath;
613             if (exceptions == null || exceptions.isEmpty()) {
614                 this.exceptions = Collections.emptyList();
615             } else {
616                 this.exceptions = Collections.unmodifiableList(new ArrayList<Path>(exceptions));
617             }
618             this.hc = HashCode.compute(sourcePath, repositoryPath, exceptions);
619             if (this.exceptions != null) {
620                 for (Path path : this.exceptions) {
621                     if (path.isAbsolute()) {
622                         throw new IllegalArgumentException(GraphI18n.pathIsNotRelative.text(path));
623                     }
624                 }
625             }
626             this.topLevelRepositoryPaths = Collections.singletonList(getPathInRepository());
627         }
628 
629         /**
630          * The path where the content is to be placed ("projected") into the repository.
631          * 
632          * @return the projected path of the content in the repository; never null
633          */
634         public Path getPathInRepository() {
635             return repositoryPath;
636         }
637 
638         /**
639          * The path of the content as known to the source
640          * 
641          * @return the source-specific path of the content; never null
642          */
643         public Path getPathInSource() {
644             return sourcePath;
645         }
646 
647         /**
648          * Get whether this rule has any exceptions.
649          * 
650          * @return true if this rule has exceptions, or false if it has none.
651          */
652         public boolean hasExceptionsToRule() {
653             return exceptions.size() != 0;
654         }
655 
656         /**
657          * Get the paths that define the exceptions to this rule. These paths are always relative to the
658          * {@link #getPathInSource() path in source}.
659          * 
660          * @return the unmodifiable exception paths; never null but possibly empty
661          */
662         public List<Path> getExceptionsToRule() {
663             return exceptions;
664         }
665 
666         /**
667          * @param pathInSource
668          * @return true if the source path is included by this rule
669          */
670         protected boolean includes( Path pathInSource ) {
671             // Check whether the path is outside the source-specific path ...
672             if (pathInSource != null && this.sourcePath.isAtOrAbove(pathInSource)) {
673 
674                 // The path is inside the source-specific region, so check the exceptions ...
675                 List<Path> exceptions = getExceptionsToRule();
676                 if (exceptions.size() != 0) {
677                     Path subpathInSource = pathInSource.relativeTo(this.sourcePath);
678                     if (subpathInSource.size() != 0) {
679                         for (Path exception : exceptions) {
680                             if (subpathInSource.isAtOrBelow(exception)) return false;
681                         }
682                     }
683                 }
684                 return true;
685             }
686             return false;
687         }
688 
689         /**
690          * {@inheritDoc}
691          * 
692          * @see Rule#getTopLevelPathsInRepository(org.modeshape.graph.property.PathFactory)
693          */
694         @Override
695         public List<Path> getTopLevelPathsInRepository( PathFactory factory ) {
696             return topLevelRepositoryPaths;
697         }
698 
699         /**
700          * {@inheritDoc}
701          * 
702          * @see org.modeshape.graph.connector.federation.Projection.Rule#isTopLevelPath(org.modeshape.graph.property.Path)
703          */
704         @Override
705         public boolean isTopLevelPath( Path path ) {
706             for (Path topLevel : topLevelRepositoryPaths) {
707                 if (topLevel.equals(path)) return true;
708             }
709             return false;
710         }
711 
712         /**
713          * {@inheritDoc}
714          * <p>
715          * This method considers a path that is at or below the rule's {@link #getPathInSource() source path} to be included,
716          * except if there are {@link #getExceptionsToRule() exceptions} that explicitly disallow the path.
717          * </p>
718          * 
719          * @see Rule#getPathInSource(Path, PathFactory)
720          */
721         @Override
722         public Path getPathInSource( Path pathInRepository,
723                                      PathFactory factory ) {
724             assert pathInRepository.equals(pathInRepository.getCanonicalPath());
725             // Project the repository path into the equivalent source path ...
726             Path pathInSource = projectPathInRepositoryToPathInSource(pathInRepository, factory);
727 
728             // Check whether the source path is included by this rule ...
729             return includes(pathInSource) ? pathInSource : null;
730         }
731 
732         /**
733          * {@inheritDoc}
734          * 
735          * @see Rule#getPathInRepository(org.modeshape.graph.property.Path, org.modeshape.graph.property.PathFactory)
736          */
737         @Override
738         public Path getPathInRepository( Path pathInSource,
739                                          PathFactory factory ) {
740             assert pathInSource.equals(pathInSource.getCanonicalPath());
741             // Check whether the source path is included by this rule ...
742             if (!includes(pathInSource)) return null;
743 
744             // Project the repository path into the equivalent source path ...
745             return projectPathInSourceToPathInRepository(pathInSource, factory);
746         }
747 
748         /**
749          * Convert a path defined in the source system into an equivalent path in the repository system.
750          * 
751          * @param pathInSource the path in the source system, which may include the {@link #getPathInSource()}
752          * @param factory the path factory; may not be null
753          * @return the path in the repository system, which will be normalized and absolute (including the
754          *         {@link #getPathInRepository()}), or null if the path is not at or under the {@link #getPathInSource()}
755          */
756         protected Path projectPathInSourceToPathInRepository( Path pathInSource,
757                                                               PathFactory factory ) {
758             if (this.sourcePath.equals(pathInSource)) return this.repositoryPath;
759             if (!this.sourcePath.isAncestorOf(pathInSource)) return null;
760             // Remove the leading source path ...
761             Path relativeSourcePath = pathInSource.relativeTo(this.sourcePath);
762             // Prepend the region's root path ...
763             Path result = factory.create(this.repositoryPath, relativeSourcePath);
764             return result.getNormalizedPath();
765         }
766 
767         /**
768          * Convert a path defined in the repository system into an equivalent path in the source system.
769          * 
770          * @param pathInRepository the path in the repository system, which may include the {@link #getPathInRepository()}
771          * @param factory the path factory; may not be null
772          * @return the path in the source system, which will be normalized and absolute (including the {@link #getPathInSource()}
773          *         ), or null if the path is not at or under the {@link #getPathInRepository()}
774          */
775         protected Path projectPathInRepositoryToPathInSource( Path pathInRepository,
776                                                               PathFactory factory ) {
777             if (this.repositoryPath.equals(pathInRepository)) return this.sourcePath;
778             if (!this.repositoryPath.isAncestorOf(pathInRepository)) return null;
779             // Find the relative path from the root of this region ...
780             Path pathInRegion = pathInRepository.relativeTo(this.repositoryPath);
781             // Prepend the path in source ...
782             Path result = factory.create(this.sourcePath, pathInRegion);
783             return result.getNormalizedPath();
784         }
785 
786         @Override
787         public String getString( NamespaceRegistry registry,
788                                  TextEncoder encoder ) {
789             StringBuilder sb = new StringBuilder();
790             sb.append(this.getPathInRepository().getString(registry, encoder));
791             sb.append(" => ");
792             sb.append(this.getPathInSource().getString(registry, encoder));
793             if (this.getExceptionsToRule().size() != 0) {
794                 for (Path exception : this.getExceptionsToRule()) {
795                     sb.append(" $ ");
796                     sb.append(exception.getString(registry, encoder));
797                 }
798             }
799             return sb.toString();
800         }
801 
802         /**
803          * {@inheritDoc}
804          * 
805          * @see Rule#getString(org.modeshape.common.text.TextEncoder)
806          */
807         @Override
808         public String getString( TextEncoder encoder ) {
809             StringBuilder sb = new StringBuilder();
810             sb.append(this.getPathInRepository().getString(encoder));
811             sb.append(" => ");
812             sb.append(this.getPathInSource().getString(encoder));
813             if (this.getExceptionsToRule().size() != 0) {
814                 for (Path exception : this.getExceptionsToRule()) {
815                     sb.append(" $ ");
816                     sb.append(exception.getString(encoder));
817                 }
818             }
819             return sb.toString();
820         }
821 
822         /**
823          * {@inheritDoc}
824          * 
825          * @see Rule#getString()
826          */
827         @Override
828         public String getString() {
829             return getString(Path.JSR283_ENCODER);
830         }
831 
832         /**
833          * {@inheritDoc}
834          * 
835          * @see java.lang.Object#hashCode()
836          */
837         @Override
838         public int hashCode() {
839             return hc;
840         }
841 
842         /**
843          * {@inheritDoc}
844          * 
845          * @see java.lang.Object#equals(java.lang.Object)
846          */
847         @Override
848         public boolean equals( Object obj ) {
849             if (obj == this) return true;
850             if (obj instanceof PathRule) {
851                 PathRule that = (PathRule)obj;
852                 if (!this.getPathInRepository().equals(that.getPathInRepository())) return false;
853                 if (!this.getPathInSource().equals(that.getPathInSource())) return false;
854                 if (!this.getExceptionsToRule().equals(that.getExceptionsToRule())) return false;
855                 return true;
856             }
857             return false;
858         }
859 
860         /**
861          * {@inheritDoc}
862          * 
863          * @see java.lang.Comparable#compareTo(java.lang.Object)
864          */
865         public int compareTo( Rule other ) {
866             if (other == this) return 0;
867             if (other instanceof PathRule) {
868                 PathRule that = (PathRule)other;
869                 int diff = this.getPathInRepository().compareTo(that.getPathInRepository());
870                 if (diff != 0) return diff;
871                 diff = this.getPathInSource().compareTo(that.getPathInSource());
872                 if (diff != 0) return diff;
873                 Iterator<Path> thisIter = this.getExceptionsToRule().iterator();
874                 Iterator<Path> thatIter = that.getExceptionsToRule().iterator();
875                 while (thisIter.hasNext() && thatIter.hasNext()) {
876                     diff = thisIter.next().compareTo(thatIter.next());
877                     if (diff != 0) return diff;
878                 }
879                 if (thisIter.hasNext()) return 1;
880                 if (thatIter.hasNext()) return -1;
881                 return 0;
882             }
883             return other.getClass().getName().compareTo(this.getClass().getName());
884         }
885 
886         /**
887          * {@inheritDoc}
888          * 
889          * @see java.lang.Object#toString()
890          */
891         @Override
892         public String toString() {
893             return getString();
894         }
895     }
896 }