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