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 => 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 * ((?:[ˆ=$]|=(?!>))+)(?:(?:=>((?:[ˆ=$]|=(?!>))+))( \$ (?:(?:[ˆ=]|=(?!>))+))*)?
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 }