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