blob: 9823af6b7ac4653ab25bd3b35695c7d7b621563a [file] [log] [blame]
// Copyright 2006, 2007, 2008 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package org.apache.tapestry5.internal.services;
import org.apache.tapestry5.internal.InternalConstants;
import org.apache.tapestry5.internal.events.InvalidationListener;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
import org.apache.tapestry5.ioc.internal.util.ConcurrentBarrier;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;
import org.apache.tapestry5.ioc.internal.util.Invokable;
import org.apache.tapestry5.ioc.services.ClassNameLocator;
import org.apache.tapestry5.services.ComponentClassResolver;
import org.apache.tapestry5.services.LibraryMapping;
import org.slf4j.Logger;
import java.util.*;
import java.util.regex.Pattern;
public class ComponentClassResolverImpl implements ComponentClassResolver, InvalidationListener
{
private static final String CORE_LIBRARY_PREFIX = "core/";
private final Logger logger;
private final ComponentInstantiatorSource componentInstantiatorSource;
private final ClassNameLocator classNameLocator;
private final String appRootPackage;
// Map from folder name to a list of root package names.
// The key does not begin or end with a slash.
private final Map<String, List<String>> mappings = CollectionFactory.newCaseInsensitiveMap();
// Flag indicating that the maps have been cleared following an invalidation
// and need to be rebuilt. The flag and the four maps below are not synchronized
// because they are only modified inside a synchronized block. That should be strong enough ...
// and changes made will become "visible" at the end of the synchronized block. Because of the
// structure of Tapestry, there should not be any reader threads while the write thread
// is operating.
private boolean needsRebuild = true;
/**
* Logical page name to class name.
*/
private final Map<String, String> pageToClassName = CollectionFactory.newCaseInsensitiveMap();
/**
* Component type to class name.
*/
private final Map<String, String> componentToClassName = CollectionFactory.newCaseInsensitiveMap();
/**
* Mixing type to class name.
*/
private final Map<String, String> mixinToClassName = CollectionFactory.newCaseInsensitiveMap();
/**
* Page class name to logical name (needed to build URLs). This one is case sensitive, since class names do always
* have a particular case.
*/
private final Map<String, String> pageClassNameToLogicalName = CollectionFactory.newMap();
/**
* Used to convert an logical page name to the canonical form of the page name; this ensures that uniform case for
* page names is used.
*/
private final Map<String, String> pageNameToCanonicalPageName = CollectionFactory.newCaseInsensitiveMap();
private final ConcurrentBarrier barrier = new ConcurrentBarrier();
private static final Pattern SPLIT_PACKAGE_PATTERN = Pattern.compile("\\.");
private static final Pattern SPLIT_FOLDER_PATTERN = Pattern.compile("/");
private static final int LOGICAL_NAME_BUFFER_SIZE = 40;
public ComponentClassResolverImpl(Logger logger,
ComponentInstantiatorSource componentInstantiatorSource,
ClassNameLocator classNameLocator,
@Inject @Symbol(InternalConstants.TAPESTRY_APP_PACKAGE_PARAM)
String appRootPackage,
Collection<LibraryMapping> mappings)
{
this.logger = logger;
this.componentInstantiatorSource = componentInstantiatorSource;
this.classNameLocator = classNameLocator;
this.appRootPackage = appRootPackage;
addPackagesToInstantiatorSource(this.appRootPackage);
for (LibraryMapping mapping : mappings)
{
String prefix = mapping.getPathPrefix();
while (prefix.startsWith("/"))
{
prefix = prefix.substring(1);
}
while (prefix.endsWith("/"))
{
prefix = prefix.substring(0, prefix.length() - 1);
}
String rootPackage = mapping.getRootPackage();
List<String> packages = this.mappings.get(prefix);
if (packages == null)
{
packages = CollectionFactory.newList();
this.mappings.put(prefix, packages);
}
packages.add(rootPackage);
// These packages, which will contain classes subject to class transformation,
// must be registered with the component instantiator (which is responsible
// for transformation).
addPackagesToInstantiatorSource(rootPackage);
}
}
private void addPackagesToInstantiatorSource(String rootPackage)
{
componentInstantiatorSource.addPackage(rootPackage + "." + InternalConstants.PAGES_SUBPACKAGE);
componentInstantiatorSource.addPackage(rootPackage + "." + InternalConstants.COMPONENTS_SUBPACKAGE);
componentInstantiatorSource.addPackage(rootPackage + "." + InternalConstants.MIXINS_SUBPACKAGE);
componentInstantiatorSource.addPackage(rootPackage + "." + InternalConstants.BASE_SUBPACKAGE);
}
/**
* When the class loader is invalidated, clear any cached page names or component types.
*/
public synchronized void objectWasInvalidated()
{
barrier.withWrite(new Runnable()
{
public void run()
{
needsRebuild = true;
}
});
}
/**
* Invoked from within a withRead() block, checks to see if a rebuild is needed, and then performs the rebuild
* within a withWrite() block.
*/
private void rebuild()
{
if (!needsRebuild) return;
barrier.withWrite(new Runnable()
{
public void run()
{
performRebuild();
}
});
}
private void performRebuild()
{
Map<String, String> savedPages = CollectionFactory.newMap(pageToClassName);
Map<String, String> savedComponents = CollectionFactory.newMap(componentToClassName);
Map<String, String> savedMixins = CollectionFactory.newMap(mixinToClassName);
pageToClassName.clear();
componentToClassName.clear();
mixinToClassName.clear();
pageClassNameToLogicalName.clear();
pageNameToCanonicalPageName.clear();
rebuild("", appRootPackage);
for (String prefix : mappings.keySet())
{
List<String> packages = mappings.get(prefix);
String folder = prefix + "/";
for (String packageName : packages)
rebuild(folder, packageName);
}
showChanges("pages", savedPages, pageToClassName);
showChanges("components", savedComponents, componentToClassName);
showChanges("mixins", savedMixins, mixinToClassName);
needsRebuild = false;
}
private void showChanges(String title, Map<String, String> savedMap, Map<String, String> newMap)
{
if (savedMap.equals(newMap)) return;
Map<String, String> core = CollectionFactory.newMap();
Map<String, String> nonCore = CollectionFactory.newMap();
int maxLength = 0;
// Pass # 1: Get all the stuff in the core library
for (String name : newMap.keySet())
{
if (name.startsWith(CORE_LIBRARY_PREFIX))
{
// Strip off the "core/" prefix.
String key = name.substring(CORE_LIBRARY_PREFIX.length());
maxLength = Math.max(maxLength, key.length());
core.put(key, newMap.get(name));
}
else
{
maxLength = Math.max(maxLength, name.length());
nonCore.put(name, newMap.get(name));
}
}
// Merge the non-core mappings into the core mappings. Where there are conflicts on name, it
// means the application overrode a core page/component/mixin and that's ok ... the
// merged core map will reflect the application's mapping.
core.putAll(nonCore);
StringBuilder builder = new StringBuilder(2000);
Formatter f = new Formatter(builder);
f.format("Available %s:\n", title);
String formatString = "%" + maxLength + "s: %s\n";
List<String> sorted = InternalUtils.sortedKeys(core);
for (String name : sorted)
{
String className = core.get(name);
if (name.equals("")) name = "(blank)";
f.format(formatString, name, className);
}
logger.info(builder.toString());
}
private void rebuild(String pathPrefix, String rootPackage)
{
fillNameToClassNameMap(pathPrefix, rootPackage, InternalConstants.PAGES_SUBPACKAGE, pageToClassName);
fillNameToClassNameMap(pathPrefix, rootPackage, InternalConstants.COMPONENTS_SUBPACKAGE, componentToClassName);
fillNameToClassNameMap(pathPrefix, rootPackage, InternalConstants.MIXINS_SUBPACKAGE, mixinToClassName);
}
private void fillNameToClassNameMap(String pathPrefix, String rootPackage, String subPackage,
Map<String, String> logicalNameToClassName)
{
String searchPackage = rootPackage + "." + subPackage;
boolean isPage = subPackage.equals(InternalConstants.PAGES_SUBPACKAGE);
Collection<String> classNames = classNameLocator.locateClassNames(searchPackage);
int startPos = searchPackage.length() + 1;
for (String name : classNames)
{
String logicalName = toLogicalName(name, pathPrefix, startPos, true);
String unstrippedName = toLogicalName(name, pathPrefix, startPos, false);
if (isPage)
{
int lastSlashx = logicalName.lastIndexOf("/");
String lastTerm = lastSlashx < 0 ? logicalName : logicalName.substring(lastSlashx + 1);
if (lastTerm.equalsIgnoreCase("index"))
{
String reducedName = lastSlashx < 0 ? "" : logicalName.substring(0, lastSlashx);
// Make the super-stripped name another alias to the class.
logicalNameToClassName.put(reducedName, name);
pageNameToCanonicalPageName.put(reducedName, logicalName);
}
pageClassNameToLogicalName.put(name, logicalName);
pageNameToCanonicalPageName.put(logicalName, logicalName);
pageNameToCanonicalPageName.put(unstrippedName, logicalName);
}
logicalNameToClassName.put(logicalName, name);
logicalNameToClassName.put(unstrippedName, name);
}
}
/**
* Converts a fully qualified class name to a logical name
*
* @param className fully qualified class name
* @param pathPrefix prefix to be placed on the logical name (to identify the library from in which the class
* lives)
* @param startPos start position within the class name to extract the logical name (i.e., after the final '.' in
* "rootpackage.pages.").
* @param stripTerms
* @return a short logical name in folder format ('.' replaced with '/')
*/
private String toLogicalName(String className, String pathPrefix, int startPos, boolean stripTerms)
{
List<String> terms = CollectionFactory.newList();
addAll(terms, SPLIT_FOLDER_PATTERN, pathPrefix);
addAll(terms, SPLIT_PACKAGE_PATTERN, className.substring(startPos));
StringBuilder builder = new StringBuilder(LOGICAL_NAME_BUFFER_SIZE);
String sep = "";
String logicalName = terms.remove(terms.size() - 1);
String unstripped = logicalName;
for (String term : terms)
{
builder.append(sep);
builder.append(term);
sep = "/";
if (stripTerms) logicalName = stripTerm(term, logicalName);
}
if (logicalName.equals("")) logicalName = unstripped;
builder.append(sep);
builder.append(logicalName);
return builder.toString();
}
private void addAll(List<String> terms, Pattern splitter, String input)
{
for (String term : splitter.split(input))
{
if (term.equals("")) continue;
terms.add(term);
}
}
private String stripTerm(String term, String logicalName)
{
if (isCaselessPrefix(term, logicalName))
{
logicalName = logicalName.substring(term.length());
}
if (isCaselessSuffix(term, logicalName))
{
logicalName = logicalName.substring(0, logicalName.length() - term.length());
}
return logicalName;
}
private boolean isCaselessPrefix(String prefix, String string)
{
return string.regionMatches(true, 0, prefix, 0, prefix.length());
}
private boolean isCaselessSuffix(String suffix, String string)
{
return string.regionMatches(true, string.length() - suffix.length(), suffix, 0, suffix
.length());
}
public String resolvePageNameToClassName(final String pageName)
{
return barrier.withRead(new Invokable<String>()
{
public String invoke()
{
String result = locate(pageName, pageToClassName);
if (result == null) throw new IllegalArgumentException(
ServicesMessages.couldNotResolvePageName(pageName, presentableNames(pageToClassName)));
return result;
}
});
}
public boolean isPageName(final String pageName)
{
return barrier.withRead(new Invokable<Boolean>()
{
public Boolean invoke()
{
return locate(pageName, pageToClassName) != null;
}
});
}
public String resolveComponentTypeToClassName(final String componentType)
{
return barrier.withRead(new Invokable<String>()
{
public String invoke()
{
String result = locate(componentType, componentToClassName);
if (result == null) throw new IllegalArgumentException(ServicesMessages
.couldNotResolveComponentType(componentType, presentableNames(componentToClassName)));
return result;
}
});
}
Collection<String> presentableNames(Map<String, ?> map)
{
Set<String> result = CollectionFactory.newSet();
for (String name : map.keySet())
{
if (name.startsWith(CORE_LIBRARY_PREFIX))
{
result.add(name.substring(CORE_LIBRARY_PREFIX.length()));
continue;
}
result.add(name);
}
return result;
}
public String resolveMixinTypeToClassName(final String mixinType)
{
return barrier.withRead(new Invokable<String>()
{
public String invoke()
{
String result = locate(mixinType, mixinToClassName);
if (result == null) throw new IllegalArgumentException(
ServicesMessages.couldNotResolveMixinType(mixinType, presentableNames(mixinToClassName)));
return result;
}
});
}
/**
* Locates a class name within the provided map, given its logical name. If not found naturally, a search inside the
* "core" library is included.
*
* @param logicalName name to search for
* @param logicalNameToClassName mapping from logical name to class name
* @return the located class name or null
*/
private String locate(String logicalName, Map<String, String> logicalNameToClassName)
{
rebuild();
String result = logicalNameToClassName.get(logicalName);
// If not found, see if it exists under the core package. In this way,
// anything in core is "inherited" (but overridable) by the application.
if (result == null) result = logicalNameToClassName.get(CORE_LIBRARY_PREFIX + logicalName);
return result;
}
public String resolvePageClassNameToPageName(final String pageClassName)
{
return barrier.withRead(new Invokable<String>()
{
public String invoke()
{
rebuild();
String result = pageClassNameToLogicalName.get(pageClassName);
if (result == null) throw new IllegalArgumentException(ServicesMessages
.pageNameUnresolved(pageClassName));
return result;
}
});
}
public String canonicalizePageName(final String pageName)
{
return barrier.withRead(new Invokable<String>()
{
public String invoke()
{
String result = locate(pageName, pageNameToCanonicalPageName);
if (result == null) throw new IllegalArgumentException(ServicesMessages
.couldNotCanonicalizePageName(pageName, presentableNames(pageNameToCanonicalPageName)));
return result;
}
});
}
}