/*
* Copyright 2006 Ethan Nicholas. All rights reserved.
* Use is subject to license terms.
*/
package jaxx.tags;
import java.io.*;
import java.lang.reflect.*;
import java.net.*;
import java.util.*;
import jaxx.*;
import jaxx.compiler.*;
import jaxx.reflect.*;
/** Manages TagHandlers, including automatically compiling .jaxx files corresponding to class tags. */
public class TagManager {
/** Namespace for JAXX's non-class tags, such as <script;>. The namespace normally does not
* need to be specified but can be used to resolve ambiguities.
*/
public static final String JAXX_NAMESPACE = "http://www.jaxxframework.org/";
/** Maps simple tag names to their default namespaces (package names). */
private static Map/*<String, String>*/ defaultNamespaces = new HashMap/*<String, String>*/();
/** Maps qualified tag names to the TagHandlers responsible for processing them. */
private static Map/*<QName, TagHandler>*/ registeredTags = new HashMap/*<QName, TagHandler>*/();
/** Keeps track of whether or not named classes exist. */
private static Map/*<String, Boolean>*/ classExistenceCache = new HashMap/*<String, Boolean>*/();
/** Maps bean classes to their TagHandler classes. The mapping is to TagHandler classes, rather than to
TagHandler instances, because subclasses of the bean class should be handled by the same TagHandler
(assuming no more specific mappings exist), which requires creating a new instance of the TagHandler. */
private static Map/*<ClassDescriptor, Class>*/ registeredBeans = new ClassMap/*<Class>*/();
// still targeting 1.4, so I can't use javax.xml.namespace.QName
private static class QName {
private String namespaceURI;
private String localPart;
public QName(String namespaceURI, String localPart) {
if (localPart == null)
throw new NullPointerException();
this.namespaceURI = namespaceURI;
this.localPart = localPart;
}
public String getNamespaceURI() {
return namespaceURI;
}
public String getLocalPart() {
return localPart;
}
public boolean equals(Object o) {
if (o == null || !(o instanceof QName))
return false;
QName qname = (QName) o;
return qname.getNamespaceURI().equals(getNamespaceURI()) && qname.getLocalPart().equals(getLocalPart());
}
public int hashCode() {
return (namespaceURI != null ? namespaceURI.hashCode() : 0) ^ getLocalPart().hashCode();
}
}
private TagManager() { /* not instantiable */ }
public static void reset() throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException {
registeredBeans.clear();
registeredTags.clear();
defaultNamespaces.clear();
JAXXCompiler.loadLibraries();
}
/** Maps a class tag to a specific <code>TagHandler</code>. When a tag representing the bean class is
* encountered (either the class' simple name, if it is unambiguous, or its fully-qualified name), the specified
* <code>TagHandler</code> will be invoked to compile it.
*
*@param beanClass the class to associate with a <code>TagHandler</code>
*@param handler the <code>TagHandler</code> class, which must descend from <code>DefaultObjectHandler</code>
*@throws IllegalArgumentException if the handler class does not descend from <code>DefaultObjectHandler</code>
*/
public static void registerBean(ClassDescriptor beanClass, Class handler) {
if (!DefaultObjectHandler.class.isAssignableFrom(handler))
throw new IllegalArgumentException("handler class must be a subclass of DefaultObjectHandler");
registeredBeans.put(beanClass, handler);
String name = beanClass.getName();
int dotPos = name.lastIndexOf(".");
String namespace = name.substring(0, dotPos + 1) + "*";
name = name.substring(dotPos + 1);
registerDefaultNamespace(name, namespace);
}
/** Sets the default namespace for a tag. When the tag is encountered with no namespace specified,
* the specified namespace will be assumed. Mapping the same tag to two or more default namespaces
* removes the mapping and marks the entry as being ambiguous (by putting a <code>null</code>
* value into the map); this causes an error to be thrown if the tag is used without a namespace being
* specified.
* <p>
* Java package names on tags are automatically converted into namespaces (e.g. <javax.swing.JButton/>
* and <JButton xmlns="javax.swing.*"/> are equivalent), so tags with package names are considered
* to have namespaces specified.
*/
public static void registerDefaultNamespace(String tag, String namespace) {
if (defaultNamespaces.containsKey(tag) && !defaultNamespaces.get(tag).equals(namespace))
defaultNamespaces.put(tag, null); // tag name is now ambiguous
else
defaultNamespaces.put(tag, namespace);
}
/** Registers a <code>TagHandler</code> for a tag. When a tag with the given name and namespace
* is encountered, the <code>TagHandler's compileFirstPass</code> and <code>compileSecondPass</code>
* methods will be invoked to handle it.
* <p>
* It is not an error to register an already-registered tag and namespace combination. The new mapping
* will replace the old mapping.
*
*@param namespace the tag's namespace
*@param tag the simple name of the tag
*@param handler the <code>TagHandler</code> which should process the tag
*/
public static void registerTag(String namespace, String tag, TagHandler handler) {
if (namespace == null)
namespace = "*";
registeredTags.put(new QName(namespace, tag), handler);
registerDefaultNamespace(tag, namespace);
}
/** Returns the <code>TagHandler</code> that should be used to process the specified tag.
* If the tag represents the class name of an uncompiled <code>.jaxx</code> file, the
* <code>.jaxx</code> is first compiled.
*
*@param namespace the tag's namespace (may be <code>null</code>)
*@param tag the tag's simple name
*@param compiler the current <code>JAXXCompiler</code>
*@return the <code>TagHandler</code> for the tag
*/
public static TagHandler getTagHandler(String namespace, String tag, JAXXCompiler compiler) throws CompilerException {
return getTagHandler(namespace, tag, false, compiler);
}
private static String getNamespace(ClassDescriptor beanClass) {
String packageName = beanClass.getPackageName();
return packageName != null ? packageName + ".*" : "*";
}
private static String getSimpleName(ClassDescriptor beanClass) {
String packageName = beanClass.getPackageName();
if (packageName != null) {
assert beanClass.getName().startsWith(packageName);
return beanClass.getName().substring(packageName.length() + 1);
}
else
return beanClass.getName();
}
/** Returns the <code>TagHandler</code> that should be used to process the specified class.
* Only <code>TagHandlers</code> previously registered with <code>registerBean</code>
* are considered.
*
*@param beanClass the tag class
*/
public static DefaultObjectHandler getTagHandler(ClassDescriptor beanClass) throws CompilerException {
try {
if (beanClass == null)
throw new NullPointerException();
String namespace = getNamespace(beanClass);
String tag = getSimpleName(beanClass);
DefaultObjectHandler handler = (DefaultObjectHandler) registeredTags.get(new QName(namespace, tag));
if (handler == null) {
Class handlerClass = (Class) registeredBeans.get(beanClass);
if (handlerClass == null)
throw new CompilerException("unable to find handler for " + beanClass);
Constructor constructor = handlerClass.getConstructor(new Class[] { ClassDescriptor.class });
handler = (DefaultObjectHandler) constructor.newInstance(new Object[] { beanClass });
registerTag(namespace, tag, handler);
}
return handler;
}
catch (InstantiationException e) {
throw new RuntimeException(e);
}
catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
}
private static boolean classExists(String className, JAXXCompiler compiler) {
if (classExistenceCache.containsKey(className))
return ((Boolean) classExistenceCache.get(className)).booleanValue();
boolean found = false;
try {
Class.forName(className, true, compiler.getClassLoader());
found = true;
}
catch (ClassNotFoundException e) {
}
catch (NoClassDefFoundError e) { // we get this instead of ClassNotFoundException on case-insensitive file systems when
// looking up a class with the wrong case
}
if (!found) { // couldn't find .class, check for .java
URL javaURL = compiler.getClassLoader().getResource(className.replace('.', '/') + ".java");
found = javaURL != null;
}
if (!found) { // couldn't find .java, check for .jaxx
URL jaxxURL = compiler.getClassLoader().getResource(className.replace('.', '/') + ".jaxx");
found = jaxxURL != null;
}
classExistenceCache.put(className, Boolean.valueOf(found));
return found;
}
private static String determinePackage(String simpleClassName, String defaultPackage, JAXXCompiler compiler) {
String namespace = null;
Set/*<String>*/ classes = compiler.getImportedClasses();
Iterator/*<String>*/ i = classes.iterator();
while (i.hasNext()) { // search class imports (e.g. import java.util.Date;)
String className = (String) i.next();
if (className.equals(simpleClassName) || className.endsWith("." + simpleClassName))
namespace = className.substring(0, className.lastIndexOf(".") + 1) + "*";
}
if (namespace == null) { // search package imports (e.g. import java.util.*;)
Set/*<String>*/ searchList = compiler.getImportedPackages();
if (defaultPackage != null) {
if (!defaultPackage.endsWith("*"))
throw new IllegalArgumentException("defaultPackage must end in '*', found '" + defaultPackage + "'");
if (classExists(defaultPackage.substring(0, defaultPackage.length() - 1) + simpleClassName, compiler))
return defaultPackage;
}
i = searchList.iterator();
while (i.hasNext()) {
String currentPackage = (String) i.next();
String className = currentPackage + simpleClassName;
if (classExists(className, compiler)) {
if (namespace != null) { // we've already found the same name in another package
compiler.reportError("symbol '" + simpleClassName + "' is ambiguous, found matching classes " + namespace.substring(0, namespace.length() - 1) + simpleClassName + " and " + currentPackage + simpleClassName + ". Use fully-qualified name to disambiguate.");
return null;
}
if (namespace == null)
namespace = currentPackage + "*";
}
}
}
return namespace;
}
/** Returns the <code>TagHandler</code> that should be used to process the specified tag.
* <p>
* The <code>namespacePrefix</code> parameter is used only for error checking, as it is an
* error to specify conflicting package names using both a fully-qualified tag name and a
* namespace prefix, but it is not an error to specify conflicting package names using a
* fully-qualified tag name and a <i>default</i> namespace (i.e. <awt:javax.swing.JButton xmlns:awt='java.awt.*'/>
* is an error, whereas <javax.swing.JButton xmlns='java.awt.*'/> is not).
*
*@param namespace the tag's namespace (may be <code>null</code>)
*@param tag the tag's simple name (which can include fully-qualified Java class names)
*@param namespacePrefix <code>true</code> if the namespace was specified by means of a namespace prefix (as opposed to a default namespace)
*@param compiler the current <code>JAXXCompiler</code>
*@return the <code>TagHandler</code> for the tag
*/
public static TagHandler getTagHandler(String namespace, String tag, boolean namespacePrefix, JAXXCompiler compiler) throws CompilerException {
if (tag == null)
throw new NullPointerException();
if (namespace == null) {
if (defaultNamespaces.containsKey(tag)) {
namespace = (String) defaultNamespaces.get(tag);
if (namespace == null) { // defaultNamespaces map contains a null value, which is put there to indicate ambiguity
compiler.reportError("tag '" + tag + "' is ambiguous; specify fully-qualified name (package and class) to disambiguate");
return null;
}
}
}
TagHandler handler = (TagHandler) registeredTags.get(new QName(namespace, tag));
if (handler == null) {
if (namespace == null || namespace.endsWith("*")) {
String className;
if (namespace != null) {
className = resolveClassName(namespace.substring(0, namespace.length() - 1) + tag, compiler);
if (className == null) {
className = resolveClassName(tag, compiler);
if (namespacePrefix && !className.startsWith(namespace.substring(0, namespace.length() - 1)))
className = null; // namespace was specified, but we found it in a different package - ignore
}
}
else
className = resolveClassName(tag, compiler);
if (className != null) {
int dotPos = className.lastIndexOf(".");
namespace = className.substring(0, dotPos + 1) + "*";
tag = className.substring(dotPos + 1);
handler = (TagHandler) registeredTags.get(new QName(namespace, tag));
if (handler == null) {
try {
handler = getTagHandler(ClassDescriptorLoader.getClassDescriptor(className, compiler.getClassLoader()));
}
catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
}
return handler;
}
/** Resolves a simple class name (like <code>Object</code> or <code>String</code>) to its fully-qualified name. Inner
* classes should be represented as they would appear in Java source code (e.g. JPopupMenu.Separator). Fully-qualified names,
* such as <code>java.lang.Object</code> are legal and will be returned unmodified (and in fact it is generally impossible to
* even know whether a given reference is fully qualified until it has been resolved). Returns <code>null</code> if no matching
* class could be found.
*/
public static String resolveClassName(String name, JAXXCompiler compiler) {
if (name.endsWith("[]"))
return resolveClassName(name.substring(0, name.length() - 2), compiler) + "[]";
if (name.indexOf("<") != -1)
name = name.substring(0, name.indexOf("<")); // strip off generic types
name = name.intern();
if (name == "boolean" || name == "byte" || name == "short" || name == "int" ||
name == "long" || name == "float" || name == "double" || name == "char")
return name;
String result = null;
String originalName = name;
String defaultNamespace = null;
if (defaultNamespaces.containsKey(name)) {
defaultNamespace = (String) defaultNamespaces.get(name);
if (defaultNamespace == null) { // defaultNamespaces map contains a null value, which is put there to indicate ambiguity
compiler.reportError("class '" + name + "' is ambiguous; specify fully-qualified name (package and class) to disambiguate");
return null;
}
}
if (defaultNamespace != null && defaultNamespace.endsWith("*")) {
result = defaultNamespace.substring(0, defaultNamespace.length() - 1) + name;
}
if (result == null) {
// Inner class names (like JPopupMenu.Separator) present a special challenge. The name before the dot might be
// a package name, or it might be a class name. If it's a class name, it might be fully qualified, or it might
// not. And it's also not actually the correct name of the class, as far as the JVM is concerned -- the correct
// name uses a dollar sign instead of a dot (javax.swing.JPopupMenu$Separator). And there could be more than
// one inner class -- it's possible to have com.mycompany.Outer$Inner$Innerer$Innerest.
//
// The basic strategy is to start by treating the part before the last dot as a package name, as that is by far
// the most likely case. If we don't find the class there, change the last dot to a dollar sign and try again.
// Suppose we have the tag <com.mycompany.Outer.Inner.Innerer.Innerest/>, matching the class above. Resolution
// proceeds like this:
// com.mycompany.Outer.Inner.Innerer.* : Innerest
// com.mycompany.Outer.Inner.* : Innerer$Innerest
// com.mycompany.Outer.* : Inner$Innerer$Innerest
// com.mycompany.* : Outer$Inner$Innerer$Innerest
// And at this point we have a match with the class Outer$Inner$Innerer$Innerest in package com.mycompany.
int dotPos = originalName.lastIndexOf('.');
for (;;) {
String namespace = dotPos != -1 ? originalName.substring(0, dotPos) + ".*" : "*";
name = originalName.substring(dotPos + 1).replace('.', '$');
String packageName = determinePackage(name, namespace, compiler);
if (packageName != null) {
assert packageName.endsWith("*");
if (packageName.equals(namespace) || namespace.equals("*")) {
// check for an alias (like javax.swing.JComboBox actually being jaxx.runtime.swing.JAXXComboBox)
TagHandler handler = (TagHandler) registeredTags.get(new QName(namespace, name));
if (handler != null) { // determine alias by looking at handler
ClassDescriptor alias = ((DefaultObjectHandler) handler).getBeanClass();
// make sure the same handler is used for both the aliased and non-aliased names, in order to avoid "no CompiledObject has been registered" error
// the line below doesn't bother to handle the case where the aliased class name doesn't have a package, since it's a pretty safe assumption that
// that will never happen
assert alias.getPackageName() != null && alias.getPackageName().length() > 0 : "aliasing with no package name has not been implemented";
registeredTags.put(new QName(alias.getPackageName() + ".*", alias.getName().substring(alias.getPackageName().length() + 1)), handler);
result = alias.getName();
break;
}
else { // no alias
result = packageName.substring(0, packageName.length() - 1) + name;
break;
}
}
// else we found a class by the same name, but in the wrong package
}
if (dotPos <= 0)
break;
dotPos = originalName.lastIndexOf('.', dotPos - 1);
}
}
if (result != null && !result.equals(originalName))
result = resolveClassName(result, compiler); // check for aliases against the new name as well
return result;
}
public static ClassDescriptor resolveClass(String className, JAXXCompiler compiler) {
try {
className = resolveClassName(className, compiler);
if (className == null)
return null;
return ClassDescriptorLoader.getClassDescriptor(className, compiler.getClassLoader());
}
catch (ClassNotFoundException e) {
return null;
}
}
}
|