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