001// Copyright 2006-2014 The Apache Software Foundation 002// 003// Licensed under the Apache License, Version 2.0 (the "License"); 004// you may not use this file except in compliance with the License. 005// You may obtain a copy of the License at 006// 007// http://www.apache.org/licenses/LICENSE-2.0 008// 009// Unless required by applicable law or agreed to in writing, software 010// distributed under the License is distributed on an "AS IS" BASIS, 011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012// See the License for the specific language governing permissions and 013// limitations under the License. 014 015package org.apache.tapestry5.internal.services; 016 017import org.apache.tapestry5.SymbolConstants; 018import org.apache.tapestry5.func.F; 019import org.apache.tapestry5.internal.InternalConstants; 020import org.apache.tapestry5.ioc.annotations.Symbol; 021import org.apache.tapestry5.ioc.internal.util.CollectionFactory; 022import org.apache.tapestry5.ioc.internal.util.InternalUtils; 023import org.apache.tapestry5.ioc.services.ClassNameLocator; 024import org.apache.tapestry5.ioc.util.AvailableValues; 025import org.apache.tapestry5.ioc.util.UnknownValueException; 026import org.apache.tapestry5.services.ComponentClassResolver; 027import org.apache.tapestry5.services.InvalidationListener; 028import org.apache.tapestry5.services.LibraryMapping; 029import org.apache.tapestry5.services.transform.ControlledPackageType; 030import org.slf4j.Logger; 031 032import java.util.*; 033import java.util.regex.Pattern; 034 035public class ComponentClassResolverImpl implements ComponentClassResolver, InvalidationListener 036{ 037 private static final String CORE_LIBRARY_PREFIX = "core/"; 038 039 private static final Pattern SPLIT_PACKAGE_PATTERN = Pattern.compile("\\."); 040 041 private static final Pattern SPLIT_FOLDER_PATTERN = Pattern.compile("/"); 042 043 private static final int LOGICAL_NAME_BUFFER_SIZE = 40; 044 045 private final Logger logger; 046 047 private final ClassNameLocator classNameLocator; 048 049 private final String startPageName; 050 051 // Map from library name to a list of root package names (usuallly just one). 052 private final Map<String, List<String>> libraryNameToPackageNames = CollectionFactory.newCaseInsensitiveMap(); 053 054 private final Map<String, ControlledPackageType> packageNameToType = CollectionFactory.newMap(); 055 056 /** 057 * Maps from a root package name to a component library name, including the empty string as the 058 * library name of the application. 059 */ 060 private final Map<String, String> packageNameToLibraryName = CollectionFactory.newMap(); 061 062 // Flag indicating that the maps have been cleared following an invalidation 063 // and need to be rebuilt. The flag and the four maps below are not synchronized 064 // because they are only modified inside a synchronized block. That should be strong enough ... 065 // and changes made will become "visible" at the end of the synchronized block. Because of the 066 // structure of Tapestry, there should not be any reader threads while the write thread 067 // is operating. 068 069 private volatile boolean needsRebuild = true; 070 071 private class Data 072 { 073 074 /** 075 * Logical page name to class name. 076 */ 077 private final Map<String, String> pageToClassName = CollectionFactory.newCaseInsensitiveMap(); 078 079 /** 080 * Component type to class name. 081 */ 082 private final Map<String, String> componentToClassName = CollectionFactory.newCaseInsensitiveMap(); 083 084 /** 085 * Mixing type to class name. 086 */ 087 private final Map<String, String> mixinToClassName = CollectionFactory.newCaseInsensitiveMap(); 088 089 /** 090 * Page class name to logical name (needed to build URLs). This one is case sensitive, since class names do always 091 * have a particular case. 092 */ 093 private final Map<String, String> pageClassNameToLogicalName = CollectionFactory.newMap(); 094 095 /** 096 * Used to convert a logical page name to the canonical form of the page name; this ensures that uniform case for 097 * page names is used. 098 */ 099 private final Map<String, String> pageNameToCanonicalPageName = CollectionFactory.newCaseInsensitiveMap(); 100 101 private void rebuild(String pathPrefix, String rootPackage) 102 { 103 fillNameToClassNameMap(pathPrefix, rootPackage, InternalConstants.PAGES_SUBPACKAGE, pageToClassName); 104 fillNameToClassNameMap(pathPrefix, rootPackage, InternalConstants.COMPONENTS_SUBPACKAGE, componentToClassName); 105 fillNameToClassNameMap(pathPrefix, rootPackage, InternalConstants.MIXINS_SUBPACKAGE, mixinToClassName); 106 } 107 108 private void fillNameToClassNameMap(String pathPrefix, String rootPackage, String subPackage, 109 Map<String, String> logicalNameToClassName) 110 { 111 String searchPackage = rootPackage + "." + subPackage; 112 boolean isPage = subPackage.equals(InternalConstants.PAGES_SUBPACKAGE); 113 114 Collection<String> classNames = classNameLocator.locateClassNames(searchPackage); 115 116 int startPos = searchPackage.length() + 1; 117 118 for (String name : classNames) 119 { 120 String logicalName = toLogicalName(name, pathPrefix, startPos, true); 121 String unstrippedName = toLogicalName(name, pathPrefix, startPos, false); 122 123 if (isPage) 124 { 125 int lastSlashx = logicalName.lastIndexOf("/"); 126 127 String lastTerm = lastSlashx < 0 ? logicalName : logicalName.substring(lastSlashx + 1); 128 129 if (lastTerm.equalsIgnoreCase("index") || lastTerm.equalsIgnoreCase(startPageName)) 130 { 131 String reducedName = lastSlashx < 0 ? "" : logicalName.substring(0, lastSlashx); 132 133 // Make the super-stripped name another alias to the class. 134 // TAP5-1444: Everything else but a start page has precedence 135 136 if (!(lastTerm.equalsIgnoreCase(startPageName) && logicalNameToClassName.containsKey(reducedName))) 137 { 138 logicalNameToClassName.put(reducedName, name); 139 pageNameToCanonicalPageName.put(reducedName, logicalName); 140 } 141 } 142 143 pageClassNameToLogicalName.put(name, logicalName); 144 pageNameToCanonicalPageName.put(logicalName, logicalName); 145 pageNameToCanonicalPageName.put(unstrippedName, logicalName); 146 } 147 148 logicalNameToClassName.put(logicalName, name); 149 logicalNameToClassName.put(unstrippedName, name); 150 } 151 } 152 153 /** 154 * Converts a fully qualified class name to a logical name 155 * 156 * @param className 157 * fully qualified class name 158 * @param pathPrefix 159 * prefix to be placed on the logical name (to identify the library from in which the class 160 * lives) 161 * @param startPos 162 * start position within the class name to extract the logical name (i.e., after the final '.' in 163 * "rootpackage.pages."). 164 * @param stripTerms 165 * @return a short logical name in folder format ('.' replaced with '/') 166 */ 167 private String toLogicalName(String className, String pathPrefix, int startPos, boolean stripTerms) 168 { 169 List<String> terms = CollectionFactory.newList(); 170 171 addAll(terms, SPLIT_FOLDER_PATTERN, pathPrefix); 172 173 addAll(terms, SPLIT_PACKAGE_PATTERN, className.substring(startPos)); 174 175 StringBuilder builder = new StringBuilder(LOGICAL_NAME_BUFFER_SIZE); 176 String sep = ""; 177 178 String logicalName = terms.remove(terms.size() - 1); 179 180 String unstripped = logicalName; 181 182 for (String term : terms) 183 { 184 builder.append(sep); 185 builder.append(term); 186 187 sep = "/"; 188 189 if (stripTerms) 190 logicalName = stripTerm(term, logicalName); 191 } 192 193 if (logicalName.equals("")) 194 logicalName = unstripped; 195 196 builder.append(sep); 197 builder.append(logicalName); 198 199 return builder.toString(); 200 } 201 202 private void addAll(List<String> terms, Pattern splitter, String input) 203 { 204 for (String term : splitter.split(input)) 205 { 206 if (term.equals("")) 207 continue; 208 209 terms.add(term); 210 } 211 } 212 213 private String stripTerm(String term, String logicalName) 214 { 215 if (isCaselessPrefix(term, logicalName)) 216 { 217 logicalName = logicalName.substring(term.length()); 218 } 219 220 if (isCaselessSuffix(term, logicalName)) 221 { 222 logicalName = logicalName.substring(0, logicalName.length() - term.length()); 223 } 224 225 return logicalName; 226 } 227 228 private boolean isCaselessPrefix(String prefix, String string) 229 { 230 return string.regionMatches(true, 0, prefix, 0, prefix.length()); 231 } 232 233 private boolean isCaselessSuffix(String suffix, String string) 234 { 235 return string.regionMatches(true, string.length() - suffix.length(), suffix, 0, suffix.length()); 236 } 237 } 238 239 private volatile Data data = new Data(); 240 241 public ComponentClassResolverImpl(Logger logger, 242 243 ClassNameLocator classNameLocator, 244 245 @Symbol(SymbolConstants.START_PAGE_NAME) 246 String startPageName, 247 248 Collection<LibraryMapping> mappings) 249 { 250 this.logger = logger; 251 this.classNameLocator = classNameLocator; 252 253 this.startPageName = startPageName; 254 255 for (LibraryMapping mapping : mappings) 256 { 257 String libraryName = mapping.libraryName; 258 259 List<String> packages = this.libraryNameToPackageNames.get(libraryName); 260 261 if (packages == null) 262 { 263 packages = CollectionFactory.newList(); 264 this.libraryNameToPackageNames.put(libraryName, packages); 265 } 266 267 packages.add(mapping.rootPackage); 268 269 // These packages, which will contain classes subject to class transformation, 270 // must be registered with the component instantiator (which is responsible 271 // for transformation). 272 273 addSubpackagesToPackageMapping(mapping.rootPackage); 274 275 packageNameToLibraryName.put(mapping.rootPackage, libraryName); 276 } 277 } 278 279 private void addSubpackagesToPackageMapping(String rootPackage) 280 { 281 for (String subpackage : InternalConstants.SUBPACKAGES) 282 { 283 packageNameToType.put(rootPackage + "." + subpackage, ControlledPackageType.COMPONENT); 284 } 285 } 286 287 public Map<String, ControlledPackageType> getControlledPackageMapping() 288 { 289 return Collections.unmodifiableMap(packageNameToType); 290 } 291 292 /** 293 * When the class loader is invalidated, clear any cached page names or component types. 294 */ 295 public synchronized void objectWasInvalidated() 296 { 297 needsRebuild = true; 298 } 299 300 /** 301 * Invoked from within a withRead() block, checks to see if a rebuild is needed, and then performs the rebuild 302 * within a withWrite() block. 303 */ 304 private Data getData() 305 { 306 if (!needsRebuild) 307 { 308 return data; 309 } 310 311 Data newData = new Data(); 312 313 for (String prefix : libraryNameToPackageNames.keySet()) 314 { 315 List<String> packages = libraryNameToPackageNames.get(prefix); 316 317 String folder = prefix + "/"; 318 319 for (String packageName : packages) 320 newData.rebuild(folder, packageName); 321 } 322 323 showChanges("pages", data.pageToClassName, newData.pageToClassName); 324 showChanges("components", data.componentToClassName, newData.componentToClassName); 325 showChanges("mixins", data.mixinToClassName, newData.mixinToClassName); 326 327 needsRebuild = false; 328 329 data = newData; 330 331 return data; 332 } 333 334 private static int countUnique(Map<String, String> map) 335 { 336 return CollectionFactory.newSet(map.values()).size(); 337 } 338 339 /** 340 * Log (at INFO level) the changes between the two logical-name-to-class-name maps 341 * @param title the title of the things in the maps (e.g. "pages" or "components") 342 * @param savedMap the old map 343 * @param newMap the new map 344 */ 345 private void showChanges(String title, Map<String, String> savedMap, Map<String, String> newMap) 346 { 347 if (savedMap.equals(newMap) || !logger.isInfoEnabled()) // nothing to log? 348 { 349 return; 350 } 351 352 Map<String, String> core = CollectionFactory.newMap(); 353 Map<String, String> nonCore = CollectionFactory.newMap(); 354 355 356 int maxLength = 0; 357 358 // Pass # 1: Get all the stuff in the core library 359 360 for (String name : newMap.keySet()) 361 { 362 if (name.startsWith(CORE_LIBRARY_PREFIX)) 363 { 364 // Strip off the "core/" prefix. 365 366 String key = name.substring(CORE_LIBRARY_PREFIX.length()); 367 368 maxLength = Math.max(maxLength, key.length()); 369 370 core.put(key, newMap.get(name)); 371 } else 372 { 373 maxLength = Math.max(maxLength, name.length()); 374 375 nonCore.put(name, newMap.get(name)); 376 } 377 } 378 379 // Merge the non-core mappings into the core mappings. Where there are conflicts on name, it 380 // means the application overrode a core page/component/mixin and that's ok ... the 381 // merged core map will reflect the application's mapping. 382 383 core.putAll(nonCore); 384 385 StringBuilder builder = new StringBuilder(2000); 386 Formatter f = new Formatter(builder); 387 388 int oldCount = countUnique(savedMap); 389 int newCount = countUnique(newMap); 390 391 f.format("Available %s (%d", title, newCount); 392 393 if (oldCount > 0 && oldCount != newCount) 394 { 395 f.format(", +%d", newCount - oldCount); 396 } 397 398 builder.append("):\n"); 399 400 String formatString = "%" + maxLength + "s: %s\n"; 401 402 List<String> sorted = InternalUtils.sortedKeys(core); 403 404 for (String name : sorted) 405 { 406 String className = core.get(name); 407 408 if (name.equals("")) 409 name = "(blank)"; 410 411 f.format(formatString, name, className); 412 } 413 414 // log multi-line string with OS-specific line endings (TAP5-2294) 415 logger.info(builder.toString().replaceAll("\\n", System.getProperty("line.separator"))); 416 } 417 418 419 public String resolvePageNameToClassName(final String pageName) 420 { 421 Data data = getData(); 422 423 String result = locate(pageName, data.pageToClassName); 424 425 if (result == null) 426 { 427 throw new UnknownValueException(String.format("Unable to resolve '%s' to a page class name.", 428 pageName), new AvailableValues("Page names", presentableNames(data.pageToClassName))); 429 } 430 431 return result; 432 } 433 434 public boolean isPageName(final String pageName) 435 { 436 return locate(pageName, getData().pageToClassName) != null; 437 } 438 439 public boolean isPage(final String pageClassName) 440 { 441 return locate(pageClassName, getData().pageClassNameToLogicalName) != null; 442 } 443 444 445 public List<String> getPageNames() 446 { 447 Data data = getData(); 448 449 List<String> result = CollectionFactory.newList(data.pageClassNameToLogicalName.values()); 450 451 Collections.sort(result); 452 453 return result; 454 } 455 456 public String resolveComponentTypeToClassName(final String componentType) 457 { 458 Data data = getData(); 459 460 String result = locate(componentType, data.componentToClassName); 461 462 if (result == null) 463 { 464 throw new UnknownValueException(String.format("Unable to resolve '%s' to a component class name.", 465 componentType), new AvailableValues("Component types", 466 presentableNames(data.componentToClassName))); 467 } 468 469 return result; 470 } 471 472 Collection<String> presentableNames(Map<String, ?> map) 473 { 474 Set<String> result = CollectionFactory.newSet(); 475 476 for (String name : map.keySet()) 477 { 478 479 if (name.startsWith(CORE_LIBRARY_PREFIX)) 480 { 481 result.add(name.substring(CORE_LIBRARY_PREFIX.length())); 482 continue; 483 } 484 485 result.add(name); 486 } 487 488 return result; 489 } 490 491 public String resolveMixinTypeToClassName(final String mixinType) 492 { 493 Data data = getData(); 494 495 String result = locate(mixinType, data.mixinToClassName); 496 497 if (result == null) 498 { 499 throw new UnknownValueException(String.format("Unable to resolve '%s' to a mixin class name.", 500 mixinType), new AvailableValues("Mixin types", presentableNames(data.mixinToClassName))); 501 } 502 503 return result; 504 } 505 506 /** 507 * Locates a class name within the provided map, given its logical name. If not found naturally, a search inside the 508 * "core" library is included. 509 * 510 * @param logicalName 511 * name to search for 512 * @param logicalNameToClassName 513 * mapping from logical name to class name 514 * @return the located class name or null 515 */ 516 private String locate(String logicalName, Map<String, String> logicalNameToClassName) 517 { 518 String result = logicalNameToClassName.get(logicalName); 519 520 // If not found, see if it exists under the core package. In this way, 521 // anything in core is "inherited" (but overridable) by the application. 522 523 if (result != null) 524 { 525 return result; 526 } 527 528 return logicalNameToClassName.get(CORE_LIBRARY_PREFIX + logicalName); 529 } 530 531 public String resolvePageClassNameToPageName(final String pageClassName) 532 { 533 String result = getData().pageClassNameToLogicalName.get(pageClassName); 534 535 if (result == null) 536 { 537 throw new IllegalArgumentException(String.format("Unable to resolve class name %s to a logical page name.", pageClassName)); 538 } 539 540 return result; 541 } 542 543 public String canonicalizePageName(final String pageName) 544 { 545 Data data = getData(); 546 547 String result = locate(pageName, data.pageNameToCanonicalPageName); 548 549 if (result == null) 550 { 551 throw new UnknownValueException(String.format("Unable to resolve '%s' to a known page name.", 552 pageName), new AvailableValues("Page names", presentableNames(data.pageNameToCanonicalPageName))); 553 } 554 555 return result; 556 } 557 558 public Map<String, String> getFolderToPackageMapping() 559 { 560 Map<String, String> result = CollectionFactory.newCaseInsensitiveMap(); 561 562 for (String folder : libraryNameToPackageNames.keySet()) 563 { 564 List<String> packageNames = libraryNameToPackageNames.get(folder); 565 566 String packageName = findCommonPackageNameForFolder(folder, packageNames); 567 568 result.put(folder, packageName); 569 } 570 571 return result; 572 } 573 574 static String findCommonPackageNameForFolder(String folder, List<String> packageNames) 575 { 576 String packageName = findCommonPackageName(packageNames); 577 578 if (packageName == null) 579 throw new RuntimeException( 580 String.format( 581 "Package names for library folder '%s' (%s) can not be reduced to a common base package (of at least one term).", 582 folder, InternalUtils.joinSorted(packageNames))); 583 return packageName; 584 } 585 586 static String findCommonPackageName(List<String> packageNames) 587 { 588 // BTW, this is what reduce is for in Clojure ... 589 590 String commonPackageName = packageNames.get(0); 591 592 for (int i = 1; i < packageNames.size(); i++) 593 { 594 commonPackageName = findCommonPackageName(commonPackageName, packageNames.get(i)); 595 596 if (commonPackageName == null) 597 break; 598 } 599 600 return commonPackageName; 601 } 602 603 static String findCommonPackageName(String commonPackageName, String packageName) 604 { 605 String[] commonExploded = explode(commonPackageName); 606 String[] exploded = explode(packageName); 607 608 int count = Math.min(commonExploded.length, exploded.length); 609 610 int commonLength = 0; 611 int commonTerms = 0; 612 613 for (int i = 0; i < count; i++) 614 { 615 if (exploded[i].equals(commonExploded[i])) 616 { 617 // Keep track of the number of shared characters (including the dot seperators) 618 619 commonLength += exploded[i].length() + (i == 0 ? 0 : 1); 620 commonTerms++; 621 } else 622 { 623 break; 624 } 625 } 626 627 if (commonTerms < 1) 628 return null; 629 630 return commonPackageName.substring(0, commonLength); 631 } 632 633 private static final Pattern DOT = Pattern.compile("\\."); 634 635 private static String[] explode(String packageName) 636 { 637 return DOT.split(packageName); 638 } 639 640 public List<String> getLibraryNames() 641 { 642 return F.flow(libraryNameToPackageNames.keySet()).remove(F.IS_BLANK).sort().toList(); 643 } 644 645 public String getLibraryNameForClass(String className) 646 { 647 assert className != null; 648 649 String current = className; 650 651 while (true) 652 { 653 654 int dotx = current.lastIndexOf('.'); 655 656 if (dotx < 1) 657 { 658 throw new IllegalArgumentException(String.format("Class %s is not inside any package associated with any library.", 659 className)); 660 } 661 662 current = current.substring(0, dotx); 663 664 String libraryName = packageNameToLibraryName.get(current); 665 666 if (libraryName != null) 667 { 668 return libraryName; 669 } 670 } 671 } 672}