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