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 }