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 }