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