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