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