immutableByLocationCache = mImmutableByLocationCache.get( locationKey );
if ( immutableByLocationCache == null ) {
immutableByLocationCache = CollectionUtils.newHashMap();
}
configHandler.setImmutableForThisLocationCache( immutableByLocationCache );
try {
// Replay the existing cache...
CachingContentHandler cachingContentHandler = mResourceCache.get( locationKey );
if ( cachingContentHandler != null ) {
cachingContentHandler.replay( configHandler );
}
// ...or cache a new one
else {
LOG.debug( "Reading resource from {0}", locationKey );
cachingContentHandler = new CachingContentHandler( configHandler );
configHandler.setCachingContentHandler( cachingContentHandler );
mFactory.newSAXParser().parse( mResourceResolver.openResource( resource ), cachingContentHandler );
// Only cache if successful
mResourceCache.put( locationKey, cachingContentHandler );
mImmutableByLocationCache.put( locationKey, immutableByLocationCache );
}
return configHandler.getConfigured();
} catch ( Exception e ) {
throw MetawidgetException.newException( e );
}
}
}
/**
* Read configuration from an input stream.
*
* This version of configure
caches any immutable objects (as determined by
* isImmutable
) and reuses them for subsequent calls. This helps ensure there is
* only ever one instance of a, say, Inspector
or WidgetBuilder
.
*
* If the Object to configure is a Class
, this method will create and return an
* instance of that class based on the configuration file. For example, if the configuration
* file is...
*
*
* <metawidget>
* <myInspector config="myConfig">
* <someConfigParameter/>
* </myInspector>
* </metawidget>
*
*
* ...then the code...
*
*
* Inspector myInspector = myConfigReader.configure( stream, Inspector.class );
*
*
* ...will create a MyInspector
configured with someConfigParameter
.
*
* Conversely, if the Object to configure is already an instance, this method will configure the
* instance. For example if the configuration file is...
*
*
* <metawidget>
* <swingMetawidget>
* <opaque><boolean>true</boolean></opaque>
* </swingMetawidget>
* </metawidget>
*
*
* ...then the code...
*
*
* JPanel panel = new JPanel();
* myConfigReader.configure( stream, panel );
*
*
* ...will call setOpaque
on the given JPanel
.
*
* @param stream
* XML input as a stream
* @param toConfigure
* object to configure. Can be a subclass of the one actually in the resource
* @param names
* path to a property within the object. If specified, siblings to this path will be
* ignored. This allows ConfigReader to be used to initialise only a specific part of
* an object
*/
public Object configure( InputStream stream, Object toConfigure, String... names ) {
if ( stream == null ) {
throw MetawidgetException.newException( "No input stream specified" );
}
try {
ConfigHandler configHandler = new ConfigHandler( toConfigure, names );
mFactory.newSAXParser().parse( stream, configHandler );
return configHandler.getConfigured();
} catch ( Exception e ) {
throw MetawidgetException.newException( e );
}
}
public final ResourceResolver getResourceResolver() {
return mResourceResolver;
}
//
// Protected methods
//
/**
* Certain XML tags are supported 'natively' by the reader.
*
* Deciding (ie. isNative
) and creating (ie. createNative
) are
* separated into two phases. The former is called to decide whether to
* SAX.startRecording
. The latter is called after SAX.endRecording
.
*/
protected boolean isNative( String name ) {
if ( "null".equals( name ) ) {
return true;
}
if ( "string".equals( name ) ) {
return true;
}
if ( "class".equals( name ) ) {
return true;
}
if ( "instanceOf".equals( name ) ) {
return true;
}
if ( "pattern".equals( name ) ) {
return true;
}
if ( "format".equals( name ) ) {
return true;
}
if ( "int".equals( name ) ) {
return true;
}
if ( "boolean".equals( name ) ) {
return true;
}
if ( "resource".equals( name ) ) {
return true;
}
if ( "url".equals( name ) ) {
return true;
}
if ( "file".equals( name ) ) {
return true;
}
if ( "bundle".equals( name ) ) {
return true;
}
if ( "constant".equals( name ) ) {
return true;
}
return false;
}
/**
* Certain XML tags are supported 'natively' by the reader.
*
* Certain tags are indicative of a broader type, but it would be too onerous to specify them
* differently for their exact type (ie. array
or enum
). Therefore
* they are lazily resolved at time of method call.
*/
protected boolean isLazyResolvingNative( String name ) {
if ( "enum".equals( name ) ) {
return true;
}
return false;
}
/**
* Create the given native type based on the recorded text (as returned by
* SAX.endRecording
)
*
* @param namespace
* the Class of the object under construction
*/
protected Object createNative( String name, Class> namespace, String recordedText )
throws Exception {
if ( "null".equals( name ) ) {
return null;
}
if ( "string".equals( name ) ) {
return recordedText;
}
if ( "class".equals( name ) ) {
if ( "".equals( recordedText ) ) {
return null;
}
return Class.forName( recordedText );
}
if ( "instanceOf".equals( name ) ) {
if ( "".equals( recordedText ) ) {
return null;
}
return Class.forName( recordedText ).newInstance();
}
if ( "pattern".equals( name ) ) {
Pattern pattern = mPatternCache.get( recordedText );
if ( pattern == null ) {
pattern = Pattern.compile( recordedText );
mPatternCache.put( recordedText, pattern );
}
return pattern;
}
if ( "format".equals( name ) ) {
return new MessageFormat( recordedText );
}
if ( "int".equals( name ) ) {
return Integer.valueOf( recordedText );
}
if ( "boolean".equals( name ) ) {
return Boolean.valueOf( recordedText );
}
if ( "bundle".equals( name ) ) {
return ResourceBundle.getBundle( recordedText );
}
if ( "enum".equals( name ) ) {
return recordedText;
}
if ( "constant".equals( name ) ) {
// External constant
int lastIndexOf = recordedText.lastIndexOf( '.' );
if ( lastIndexOf != -1 ) {
return Class.forName( recordedText.substring( 0, lastIndexOf ) ).getDeclaredField( recordedText.substring( lastIndexOf + 1 ) ).get( null );
}
// Relative to current namespace
return namespace.getDeclaredField( recordedText ).get( null );
}
// These native types can never be equal to each other. This will cause a problem if they
// are used by, say, a PropertyStyle because that PropertyStyle can never be shared
if ( "resource".equals( name ) ) {
return mResourceResolver.openResource( recordedText );
}
if ( "url".equals( name ) ) {
return new URL( recordedText ).openStream();
}
if ( "file".equals( name ) ) {
return new FileInputStream( recordedText );
}
throw MetawidgetException.newException( "Don't know how to convert '" + recordedText + "' to a " + name );
}
/**
* Certain XML tags are supported 'natively' as collections by the reader.
*/
protected Object createNativeCollection( String name ) {
if ( "array".equals( name ) ) {
return new Object[0];
}
if ( "list".equals( name ) ) {
return CollectionUtils.newArrayList();
}
if ( "set".equals( name ) ) {
return CollectionUtils.newHashSet();
}
return null;
}
/**
* Create a native that is 'lazily resolved' based on the method it is being applied to. Most
* natives are explicitly typed (ie. boolean, int etc.) but it is too onerous to do that for
* everything (ie. we support array instead of string-array, int-array etc.)
*
* @param nativeValue
* never null
* @param toResolveTo
* @return the resolved native, or null if no resolution was possible
*/
@SuppressWarnings( { "rawtypes", "unchecked" } )
protected Object createLazyResolvingNative( Object nativeValue, Class> toResolveTo ) {
// Arrays (ie. convert Object[] into String[])
if ( toResolveTo.isArray() && nativeValue.getClass().isArray() ) {
Object[] array = (Object[]) nativeValue;
Object[] compatibleArray = (Object[]) Array.newInstance( toResolveTo.getComponentType(), array.length );
try {
System.arraycopy( array, 0, compatibleArray, 0, array.length );
return compatibleArray;
} catch ( ArrayStoreException e ) {
// Could not be converted, is not compatible
return null;
}
}
// Enums
else if ( toResolveTo.isEnum() && nativeValue instanceof String ) {
try {
return Enum.valueOf( (Class extends Enum>) toResolveTo, (String) nativeValue );
} catch ( IllegalArgumentException e ) {
// Could not be converted, is not compatible
return null;
}
}
// Could not be converted
return null;
}
/**
* Lookup a class based on the URI namespace and the local name of the XML tag.
*
* @param uri
* the URI namespace, to be used as the package name
* @param localName
* the name of the tag, to be used as the class name
* @param classLoader
* the classLoader to use to lookup the class. This can be important if the
* ClassLoader gets swapped out on us (e.g. SWT's WindowBuilder)
*/
protected Class> lookupClass( String uri, String localName, ClassLoader classLoader )
throws SAXException {
if ( !uri.startsWith( JAVA_NAMESPACE_PREFIX ) ) {
throw new SAXException( "Namespace '" + uri + "' of element <" + localName + "> must start with " + JAVA_NAMESPACE_PREFIX );
}
String packagePrefix = uri.substring( JAVA_NAMESPACE_PREFIX.length() );
// Try regular class
String uppercasedLocalName = StringUtils.capitalize( localName );
String classToConstruct = packagePrefix + StringUtils.SEPARATOR_DOT_CHAR + uppercasedLocalName;
Class> clazz = lookupClass( classToConstruct, classLoader );
if ( clazz == null ) {
// Try inner class
String innerClassToConstruct = packagePrefix + '$' + uppercasedLocalName;
clazz = lookupClass( innerClassToConstruct, classLoader );
if ( clazz == null ) {
throw MetawidgetException.newException( "No such tag <" + localName + "> or class " + classToConstruct + " (is it on your CLASSPATH?)" );
}
}
// Return it
return clazz;
}
/**
* Lookup a class based on its name.
*
* Subclasses can override this method to lookup a class in other ways (for example using a
* module system).
*/
protected Class> lookupClass( String className, ClassLoader classLoader ) {
return ClassUtils.niceForName( className, classLoader );
}
/**
* Certain classes are immutable. We only ever need one instance of such classes for an entire
* application.
*/
protected boolean isImmutable( Class> clazz ) {
return ( Immutable.class.isAssignableFrom( clazz ) );
}
//
// Inner classes
//
private static enum EncounteredState {
METHOD,
NATIVE_TYPE,
NATIVE_COLLECTION_TYPE,
CONFIGURED_TYPE,
JAVA_OBJECT,
WRONG_TYPE,
WRONG_NAME
}
private static enum ExpectingState {
ROOT,
TO_CONFIGURE,
OBJECT,
METHOD,
CLOSE_OBJECT_WITH_REFID
}
private class ConfigHandler
extends DefaultHandler {
//
// Private statics
//
//
// Private members
//
/**
* Object to configure.
*/
private Object mToConfigure;
/**
* Path within object to configure (if specified, siblings to the path will be ignored).
*/
private String[] mNames;
/**
* Number of elements encountered so far. Used as a simple way to get a unique 'row/column'
* location into the XML tree.
*/
private int mLocationIndex;
/**
* Map of objects that are immutable for this XML document. Keyed by location index.
*/
private Map mImmutableForThisLocationCache;
/**
* Track our depth in the XML tree.
*/
private int mDepth;
/**
* Depth after which to skip type processing, so as to ignore chunks of the XML tree.
*/
private int mIgnoreTypeAfterDepth = -1;
/**
* Depth after which to skip name processing, so as to ignore chunks of the XML tree.
*/
private int mIgnoreNameAfterDepth = -1;
/**
* Depth after which to skip immutable caching, so as to ignore chunks of the XML tree.
*/
private int mIgnoreImmutableAfterDepth = -1;
/**
* Stack of Objects constructed so far.
*/
private Stack mConstructing = CollectionUtils.newStack();
/**
* Next expected state in the XML tree.
*/
private ExpectingState mExpecting = ExpectingState.ROOT;
/**
* Stack of encountered states in the XML tree.
*/
private Stack mEncountered = CollectionUtils.newStack();
private StringBuilder mBuilderValue;
private CachingContentHandler mCachingContentHandler;
//
// Constructor
//
public ConfigHandler( Object toConfigure, String... names ) {
mToConfigure = toConfigure;
mNames = names;
}
//
// Public methods
//
public void setImmutableForThisLocationCache( Map immutableForThisLocationCache ) {
mImmutableForThisLocationCache = immutableForThisLocationCache;
}
public void setCachingContentHandler( CachingContentHandler cachingContentHandler ) {
mCachingContentHandler = cachingContentHandler;
}
public Object getConfigured() {
if ( mConstructing.isEmpty() ) {
if ( mToConfigure instanceof Class ) {
throw MetawidgetException.newException( "No match for " + mToConfigure + " within config" );
}
throw MetawidgetException.newException( "No match for " + mToConfigure.getClass() + " within config" );
}
if ( mConstructing.size() > 1 ) {
throw MetawidgetException.newException( "Config still processing" );
}
return mConstructing.peek();
}
@Override
public void startElement( String uri, String localName, String name, Attributes attributes )
throws SAXException {
mDepth++;
if ( mIgnoreTypeAfterDepth != -1 && mDepth > mIgnoreTypeAfterDepth ) {
return;
}
if ( mIgnoreNameAfterDepth != -1 && mDepth > mIgnoreNameAfterDepth ) {
return;
}
// Allow classnames to start with an uppercase letters, because of the 'second
// character' clause in StringUtils.capitalize
try {
// Note: we rely on our schema-validating parser to enforce the correct
// nesting of elements and/or prescence of attributes, so we don't need to
// re-check that here
switch ( mExpecting ) {
case ROOT:
if ( mToConfigure == null ) {
mExpecting = ExpectingState.OBJECT;
} else {
mExpecting = ExpectingState.TO_CONFIGURE;
}
break;
case TO_CONFIGURE: {
// Initial elements must be at depth == 2
if ( mDepth != 2 ) {
return;
}
Class> toConfigureClass = lookupClass( uri, localName, mToConfigure.getClass().getClassLoader() );
// Match by Class...
if ( mToConfigure instanceof Class> ) {
if ( !( (Class>) mToConfigure ).isAssignableFrom( toConfigureClass ) ) {
mEncountered.push( EncounteredState.WRONG_TYPE );
mIgnoreTypeAfterDepth = 2;
// Pause caching (if any)
if ( mCachingContentHandler != null ) {
mCachingContentHandler.pause( false );
}
return;
}
if ( !mConstructing.isEmpty() ) {
throw MetawidgetException.newException( "Already configured a " + mConstructing.peek().getClass() + ", ambiguous match with " + toConfigureClass );
}
handleNonNativeObject( uri, localName, attributes );
}
// ...or instance of Object
else {
// If already constructed Object of correct type, or if this element has
// wrong @type...
if ( !mConstructing.isEmpty() || !toConfigureClass.isAssignableFrom( mToConfigure.getClass() ) ) {
// ...ignore it
mEncountered.push( EncounteredState.WRONG_TYPE );
mIgnoreTypeAfterDepth = 2;
// Pause caching (if any)
if ( mCachingContentHandler != null ) {
mCachingContentHandler.pause( false );
}
return;
}
mConstructing.push( mToConfigure );
mEncountered.push( EncounteredState.JAVA_OBJECT );
}
mExpecting = ExpectingState.METHOD;
break;
}
case OBJECT: {
if ( mCachingContentHandler == null || !mCachingContentHandler.isPaused() ) {
mLocationIndex++;
}
// Native types
if ( isNative( localName ) || isLazyResolvingNative( localName ) ) {
mEncountered.push( EncounteredState.NATIVE_TYPE );
startRecording();
mExpecting = ExpectingState.METHOD;
return;
}
// Native collection types
Object collection = createNativeCollection( localName );
if ( collection != null ) {
mConstructing.push( collection );
mEncountered.push( EncounteredState.NATIVE_COLLECTION_TYPE );
mExpecting = ExpectingState.OBJECT;
return;
}
mExpecting = handleNonNativeObject( uri, localName, attributes );
break;
}
case METHOD: {
// Screen names
if ( mNames != null ) {
// Initial elements are at depth == 2
int nameIndex = mDepth - 3;
if ( nameIndex < mNames.length ) {
String expectingName = mNames[nameIndex];
// Skip wrong names
if ( !localName.equals( expectingName ) ) {
mEncountered.push( EncounteredState.WRONG_NAME );
mIgnoreNameAfterDepth = mDepth;
// Pause caching (if any)
if ( mCachingContentHandler != null ) {
mCachingContentHandler.pause( false );
}
return;
}
}
}
// Process method
mConstructing.push( new ArrayList() );
mEncountered.push( EncounteredState.METHOD );
mExpecting = ExpectingState.OBJECT;
break;
}
case CLOSE_OBJECT_WITH_REFID: {
throw InspectorException.newException( "<" + name + "> not expected here. Elements with a 'refId' must have an empty body" );
}
}
} catch ( RuntimeException e ) {
throw e;
} catch ( Exception e ) {
throw new SAXException( e );
}
}
public void startRecording() {
mBuilderValue = new StringBuilder();
}
@Override
public void characters( char[] characters, int start, int length ) {
if ( mBuilderValue == null ) {
return;
}
mBuilderValue.append( characters, start, length );
}
public String endRecording() {
String value = mBuilderValue.toString();
mBuilderValue = null;
return value;
}
@Override
public void endElement( String uri, String localName, String name )
throws SAXException {
mDepth--;
if ( mIgnoreTypeAfterDepth != -1 ) {
if ( mDepth >= mIgnoreTypeAfterDepth ) {
return;
}
mIgnoreTypeAfterDepth = -1;
// Unpause caching (if any)
if ( mCachingContentHandler != null ) {
mCachingContentHandler.unpause( false );
}
}
if ( mIgnoreNameAfterDepth != -1 ) {
if ( mDepth >= mIgnoreNameAfterDepth ) {
return;
}
mIgnoreNameAfterDepth = -1;
// Unpause caching (if any)
if ( mCachingContentHandler != null ) {
mCachingContentHandler.unpause( false );
}
}
// All done?
if ( mDepth == 0 ) {
return;
}
// Inside the tree somewhere, but of a different toConfigure?
if ( mConstructing.isEmpty() ) {
return;
}
// Configure based on what was encountered
try {
EncounteredState encountered = mEncountered.pop();
switch ( encountered ) {
case NATIVE_TYPE: {
// Pop/push to peek at namespace
Object methodParameters = mConstructing.pop();
Object constructing = mConstructing.peek();
if ( constructing instanceof ConfigAndId ) {
constructing = ( (ConfigAndId) mConstructing.peek() ).getConfig();
}
mConstructing.push( methodParameters );
// Create native
addToConstructing( createNative( localName, constructing.getClass(), endRecording() ) );
mExpecting = ExpectingState.OBJECT;
return;
}
case NATIVE_COLLECTION_TYPE: {
Object nativeCollectionType = mConstructing.pop();
@SuppressWarnings( "unchecked" )
Collection parameters = (Collection) mConstructing.peek();
parameters.add( nativeCollectionType );
mExpecting = ExpectingState.OBJECT;
return;
}
case CONFIGURED_TYPE:
case JAVA_OBJECT: {
Object object = mConstructing.pop();
if ( encountered == EncounteredState.CONFIGURED_TYPE ) {
Class> classToConstruct = lookupClass( uri, localName, mToConfigure.getClass().getClassLoader() );
String id = ( (ConfigAndId) object ).getId();
object = ( (ConfigAndId) object ).getConfig();
Object configuredObject = null;
// Immutable by class (and config)? Don't re-instantiate
if ( isImmutable( classToConstruct ) ) {
configuredObject = getImmutableByClass( classToConstruct, object );
}
if ( configuredObject == null ) {
try {
Constructor> constructor = classToConstruct.getConstructor( object.getClass() );
configuredObject = constructor.newInstance( object );
} catch ( NoSuchMethodException e ) {
String likelyConfig = getLikelyConfig( classToConstruct );
if ( "".equals( likelyConfig ) ) {
throw MetawidgetException.newException( classToConstruct + " does not have a constructor that takes a " + object.getClass() + ", as specified by your config attribute. It only has a config-less constructor" );
} else if ( likelyConfig != null ) {
throw MetawidgetException.newException( classToConstruct + " does not have a constructor that takes a " + object.getClass() + ", as specified by your config attribute. Did you mean config=\"" + likelyConfig + "\"?" );
}
throw MetawidgetException.newException( classToConstruct + " does not have a constructor that takes a " + object.getClass() + ", as specified by your config attribute" );
}
// Immutable? Cache it going forward
if ( isImmutable( classToConstruct ) ) {
LOG.debug( "\tInstantiated immutable {0} (config hashCode {1})", classToConstruct, object.hashCode() );
Immutable immutable = (Immutable) configuredObject;
putImmutableByClass( immutable, object );
if ( id != null ) {
putImmutableById( id, immutable );
}
}
} else if ( isImmutable( classToConstruct ) ) {
// Unpause caching (if any)
if ( mCachingContentHandler != null && mDepth < mIgnoreImmutableAfterDepth ) {
mCachingContentHandler.unpause( true );
mIgnoreImmutableAfterDepth = -1;
// If the configuredObject was cached by class, it may have come
// from a different 'location' (either a different resource, or
// a different mLocationIndex within this same resource) so we
// still need to cache it at this new location
putImmutableByLocation( (Immutable) configuredObject );
}
}
// Use the configured object (not its config) as the 'object' from now
// on
object = configuredObject;
}
// Back at root? Expect another TO_CONFIGURE
if ( mDepth == 1 ) {
mConstructing.push( object );
mExpecting = ExpectingState.TO_CONFIGURE;
return;
}
addToConstructing( object );
mExpecting = ExpectingState.OBJECT;
return;
}
case METHOD: {
@SuppressWarnings( "unchecked" )
List parameters = (List) mConstructing.pop();
Object constructing = mConstructing.peek();
if ( constructing instanceof ConfigAndId ) {
constructing = ( (ConfigAndId) constructing ).getConfig();
}
Class> constructingClass = constructing.getClass();
String methodName = "set" + StringUtils.capitalize( localName );
try {
Method method = classGetMethod( constructingClass, methodName, parameters );
method.invoke( constructing, parameters.toArray() );
} catch ( NoSuchMethodException e ) {
// Hint for config-based constructors
for ( Constructor> constructor : constructingClass.getConstructors() ) {
Class>[] parameterTypes = constructor.getParameterTypes();
if ( parameterTypes.length != 1 ) {
continue;
}
String parameterClassName = parameterTypes[0].getClass().getSimpleName();
if ( parameterClassName.endsWith( "Config" ) ) {
throw MetawidgetException.newException( "No such method " + methodName + " on " + constructingClass + ". Did you forget config=\"" + parameterClassName + "\"?" );
}
}
throw e;
}
mExpecting = ExpectingState.METHOD;
return;
}
case WRONG_TYPE:
return;
case WRONG_NAME:
return;
}
} catch ( RuntimeException e ) {
throw e;
} catch ( InvocationTargetException e ) {
// Prevent InvocationTargetException 'masking' the error
Throwable t = e.getTargetException();
// getTargetException may return a StackOverflowError
if ( !( t instanceof Exception ) ) {
throw new RuntimeException( t );
}
throw new SAXException( (Exception) t );
} catch ( Exception e ) {
throw new SAXException( e );
}
}
@Override
public void warning( SAXParseException exception ) {
LOG.warn( exception.getMessage() );
}
@Override
public void error( SAXParseException exception ) {
throw MetawidgetException.newException( exception );
}
//
// Private methods
//
/**
* @return what should be expected next
*/
private ExpectingState handleNonNativeObject( String uri, String localName, Attributes attributes )
throws Exception {
String refId = attributes.getValue( "refId" );
// Type with refId
String configClassName = attributes.getValue( "config" );
if ( refId != null ) {
if ( configClassName != null ) {
throw InspectorException.newException( "Elements with 'refId' attributes (refId=\"" + refId + "\") cannot also have 'config' attributes (config=\"" + configClassName + "\")" );
}
Object immutable = getImmutableByRefId( refId );
Class> actualClass = immutable.getClass();
if ( !StringUtils.decapitalize( actualClass.getSimpleName() ).equals( localName ) ) {
throw InspectorException.newException( "refId=\"" + refId + "\" points to an object of " + actualClass + ", not a <" + localName + ">" );
}
mConstructing.push( immutable );
mEncountered.push( EncounteredState.JAVA_OBJECT );
return ExpectingState.CLOSE_OBJECT_WITH_REFID;
}
Object object = null;
Class> classToConstruct = lookupClass( uri, localName, mToConfigure.getClass().getClassLoader() );
// Already cached (by location)?
//
// Note: if it is already cached by location, any child nodes will have been 'paused
// away' by CachingContentHandler, so we don't have to worry about checking the config
// attribute
if ( isImmutable( classToConstruct ) ) {
object = getImmutableByLocation();
}
// Configured types
if ( object == null ) {
if ( configClassName != null ) {
String configToConstruct;
if ( configClassName.indexOf( '.' ) == -1 ) {
configToConstruct = classToConstruct.getPackage().getName() + '.' + configClassName;
} else {
configToConstruct = configClassName;
}
Class> configClass = lookupClass( configToConstruct, mToConfigure.getClass().getClassLoader() );
if ( configClass == null ) {
throw MetawidgetException.newException( "No such configuration class " + configToConstruct );
}
Object config = configClass.newInstance();
if ( config instanceof NeedsResourceResolver ) {
( (NeedsResourceResolver) config ).setResourceResolver( getResourceResolver() );
}
mConstructing.push( new ConfigAndId( config, attributes.getValue( "id" ) ) );
mEncountered.push( EncounteredState.CONFIGURED_TYPE );
// Pause caching (if any)
if ( mIgnoreImmutableAfterDepth == -1 && mCachingContentHandler != null && isImmutable( classToConstruct ) ) {
mCachingContentHandler.pause( true );
mIgnoreImmutableAfterDepth = mDepth;
}
return ExpectingState.METHOD;
}
}
// Already cached (without config)?
if ( object == null && isImmutable( classToConstruct ) ) {
object = getImmutableByClass( classToConstruct, IMMUTABLE_NO_CONFIG );
}
// Java objects (without config)?
if ( object == null ) {
try {
Constructor> defaultConstructor = classToConstruct.getConstructor();
object = defaultConstructor.newInstance();
} catch ( NoSuchMethodException e ) {
String likelyConfig = getLikelyConfig( classToConstruct );
if ( likelyConfig != null ) {
throw MetawidgetException.newException( classToConstruct + " does not have a default constructor. Did you mean config=\"" + likelyConfig + "\"?" );
}
throw MetawidgetException.newException( classToConstruct + " does not have a default constructor" );
}
// Immutable by class (with no config)? Cache for next time
if ( isImmutable( classToConstruct ) ) {
LOG.debug( "\tInstantiated immutable {0} (no config)", classToConstruct );
Immutable immutable = (Immutable) object;
putImmutableByClass( immutable, null );
String id = attributes.getValue( "id" );
if ( id != null ) {
putImmutableById( id, immutable );
}
}
}
mConstructing.push( object );
mEncountered.push( EncounteredState.JAVA_OBJECT );
return ExpectingState.METHOD;
}
private void addToConstructing( Object toAdd ) {
Object parameters = mConstructing.peek();
// Collections
if ( parameters instanceof Collection> ) {
@SuppressWarnings( "unchecked" )
Collection collection = (Collection) parameters;
collection.add( toAdd );
return;
}
// Arrays
if ( parameters.getClass().isArray() ) {
Object[] array = (Object[]) mConstructing.pop();
Object[] newArray = new Object[array.length + 1];
System.arraycopy( array, 0, newArray, 0, array.length );
newArray[array.length] = toAdd;
mConstructing.push( newArray );
return;
}
// Unknown
throw MetawidgetException.newException( "Don't know how to add to a " + parameters.getClass() );
}
private Object getImmutableByLocation() {
// No cache (ie. XML coming from a nameless InputStream)?
if ( mImmutableForThisLocationCache == null ) {
return null;
}
return mImmutableForThisLocationCache.get( mLocationIndex );
}
private void putImmutableByLocation( Immutable immutable ) {
// No cache by (ie. XML coming from a nameless InputStream)?
if ( mImmutableForThisLocationCache == null ) {
return;
}
if ( mImmutableForThisLocationCache.containsKey( mLocationIndex ) ) {
throw InspectorException.newException( "Location " + mLocationIndex + " already cached" );
}
mImmutableForThisLocationCache.put( mLocationIndex, immutable );
}
private Object getImmutableByRefId( String refId ) {
if ( !mImmutableByIdCache.containsKey( refId ) ) {
throw InspectorException.newException( "Attribute refId=\"" + refId + "\" refers to non-existent id" );
}
return mImmutableByIdCache.get( refId );
}
private void putImmutableById( String id, Immutable immutable ) {
if ( mImmutableByIdCache.containsKey( id ) ) {
throw InspectorException.newException( "Attribute id=\"" + id + "\" appears more than once" );
}
mImmutableByIdCache.put( id, immutable );
}
private Object getImmutableByClass( Class> clazz, Object config ) {
Map configs = mImmutableByClassCache.get( clazz );
if ( configs == null ) {
return null;
}
Object configToLookup = config;
if ( configToLookup == null ) {
configToLookup = IMMUTABLE_NO_CONFIG;
}
// Config must have implemented its .hashCode() and .equals() properly for this to work!
return configs.get( configToLookup );
}
private void putImmutableByClass( Immutable immutable, Object config ) {
Class> clazz = immutable.getClass();
Map configs = mImmutableByClassCache.get( clazz );
if ( configs == null ) {
configs = CollectionUtils.newHashMap();
mImmutableByClassCache.put( clazz, configs );
}
Object configToStoreUnder = config;
if ( configToStoreUnder == null ) {
configToStoreUnder = IMMUTABLE_NO_CONFIG;
} else {
// Sanity check. This can be quite expensive, as we will only do it once
try {
Class> configClass = configToStoreUnder.getClass();
// Hard error
// equals
Class> equalsDeclaringClass = configClass.getMethod( "equals", Object.class ).getDeclaringClass();
if ( Object.class.equals( equalsDeclaringClass ) ) {
throw MetawidgetException.newException( configClass + " does not override .equals(), so cannot cache reliably" );
}
// hashCode
//
// Note: tempting to check for System.identityHashCode( configClass ) ==
// configClass.hashCode() here, but that
// could actually be true occasionally, causing hard-to-find bugs in production!
Class> hashCodeDeclaringClass = configClass.getMethod( "hashCode" ).getDeclaringClass();
if ( Object.class.equals( hashCodeDeclaringClass ) ) {
throw MetawidgetException.newException( configClass + " does not override .hashCode(), so cannot cache reliably" );
}
// Soft warning (System.identityHashCode( configClass ) ==
// configClass.hashCode() may be true occasionally, even if properly overridden)
if ( System.identityHashCode( configToStoreUnder ) == configToStoreUnder.hashCode() ) {
LOG.warn( "{0} overrides .hashCode(), but it returns the same as System.identityHashCode, so cannot be cached reliably", configClass );
}
if ( !equalsDeclaringClass.equals( hashCodeDeclaringClass ) ) {
throw MetawidgetException.newException( equalsDeclaringClass + " implements .equals(), but .hashCode() is implemented by " + hashCodeDeclaringClass + ", so cannot cache reliably" );
}
if ( !configClass.equals( equalsDeclaringClass ) ) {
// Soft warning
//
// Note: only show this if the configClass appears to have its own 'state'.
// Base this assumption on whether it declares any methods. We don't want to
// use .getDeclaredFields because that requires a security manager
// check of checkMemberAccess(Member.DECLARED), whereas we may only have
// checkMemberAccess(Member.PUBLIC) permission
//
// This check may seem overkill, but given that we are encouraging people to
// extend their xxxConfigs from BaseObjectInspectorConfig and
// BaseXmlInspectorConfig, it is actually the most likely scenario
outer: for ( Method declaredMethod : configClass.getMethods() ) {
if ( configClass.equals( declaredMethod.getDeclaringClass() ) ) {
// (permit overloaded methods and co-variant return types)
for ( Method equalsDeclaredMethod : equalsDeclaringClass.getMethods() ) {
if ( equalsDeclaredMethod.getName().equals( declaredMethod.getName() ) ) {
break outer;
}
}
LOG.warn( "{0} does not override .equals() (only its super{1} does), so may not be cached reliably", configClass, equalsDeclaringClass );
break;
}
}
// Note: not necessary to do !configClass.equals( hashCodeDeclaringClass ),
// as will already have thrown an Exception from
// !equalsDeclaringClass.equals( hashCodeDeclaringClass ) if that's the case
}
} catch ( Exception e ) {
throw MetawidgetException.newException( e );
}
}
if ( configs.containsKey( configToStoreUnder ) ) {
throw InspectorException.newException( "Config '" + configToStoreUnder + "' already cached" );
}
for ( Method method : clazz.getMethods() ) {
// Do not warn for > 1 parameter, as a lot of WidgetBuilders implement setValue
if ( method.getName().startsWith( ClassUtils.JAVABEAN_SET_PREFIX ) && method.getParameterTypes().length == 1 ) {
LOG.warn( "{0} must be immutable, but appears to have a setter method ({1})", clazz, method );
break;
}
}
configs.put( configToStoreUnder, immutable );
// Unpause caching (if any)
if ( mCachingContentHandler != null && mDepth < mIgnoreImmutableAfterDepth ) {
mCachingContentHandler.unpause( true );
mIgnoreImmutableAfterDepth = -1;
if ( config != null ) {
putImmutableByLocation( immutable );
}
}
}
/**
* Finds a method with the specified parameter types.
*
* Like Class.getMethod
, but works based on isInstance
rather than
* an exact match of parameter types. This is essentially a crude and partial implementation
* of http://java.sun.com/docs/books/jls/second_edition/html/expressions.doc.html#20448. In
* particular, no attempt at 'closest matching' is implemented.
*/
private Method classGetMethod( Class> clazz, String name, List args )
throws NoSuchMethodException {
int numberOfParameterTypes = args.size();
Method likelyMatch = null;
// For each method...
methods: for ( Method method : clazz.getMethods() ) {
// ...with a matching name...
if ( !method.getName().equals( name ) ) {
continue;
}
likelyMatch = method;
// ...and compatible parameters...
Class>[] methodParameterTypes = method.getParameterTypes();
if ( methodParameterTypes.length != numberOfParameterTypes ) {
continue;
}
// Array/enum compatibility handling mangles the args, so take a copy
List compatibleArgs = CollectionUtils.newArrayList( args );
// ...test each parameter for compatibility...
for ( int loop = 0; loop < numberOfParameterTypes; loop++ ) {
Object arg = compatibleArgs.get( loop );
Class> parameterType = methodParameterTypes[loop];
// ...primitives...
if ( parameterType.isPrimitive() ) {
parameterType = ClassUtils.getWrapperClass( parameterType );
} else if ( arg == null ) {
continue;
}
if ( !parameterType.isInstance( arg ) ) {
// ...lazy resolvers...
Object resolvedValue = createLazyResolvingNative( arg, parameterType );
if ( resolvedValue == null ) {
continue methods;
}
compatibleArgs.remove( loop );
compatibleArgs.add( loop, resolvedValue );
}
}
args.clear();
args.addAll( compatibleArgs );
// ...return it. Note we make no attempt to find the 'closest match'
return method;
}
// No such method
if ( likelyMatch != null ) {
throw new NoSuchMethodException( methodToString( clazz, name, args ) + ". Did you mean " + methodToString( likelyMatch ) + "?" );
}
throw new NoSuchMethodException( methodToString( clazz, name, args ) );
}
/**
* @return null if ambiguous match, empty String for default constructor, otherwise name of
* single parameter
*/
private String getLikelyConfig( Class> clazz ) {
Constructor>[] constructors = clazz.getConstructors();
if ( constructors.length != 1 ) {
return null;
}
if ( constructors[0].getParameterTypes().length == 0 ) {
return "";
}
if ( constructors[0].getParameterTypes().length > 1 ) {
return null;
}
Class> likelyConfigClass = constructors[0].getParameterTypes()[0];
if ( likelyConfigClass.getPackage().equals( clazz.getPackage() ) ) {
return likelyConfigClass.getSimpleName();
}
return likelyConfigClass.getName();
}
private String methodToString( Method method ) {
StringBuilder builder = new StringBuilder();
for ( Class> parameterType : method.getParameterTypes() ) {
if ( builder.length() > 0 ) {
builder.append( ", " );
}
if ( parameterType.isArray() ) {
builder.append( parameterType.getComponentType().getSimpleName() );
builder.append( "[]" );
} else {
builder.append( parameterType.getSimpleName() );
}
}
builder.insert( 0, "(" );
builder.insert( 0, method.getName() );
builder.append( ")" );
return builder.toString();
}
private String methodToString( Class> clazz, String methodName, List args ) {
StringBuilder builder = new StringBuilder();
for ( Object obj : args ) {
if ( builder.length() > 0 ) {
builder.append( ", " );
}
if ( obj == null ) {
builder.append( "null" );
} else {
builder.append( obj.getClass().getSimpleName() );
}
}
builder.insert( 0, "(" );
builder.insert( 0, methodName );
builder.insert( 0, StringUtils.SEPARATOR_DOT_CHAR );
builder.insert( 0, clazz );
builder.append( ")" );
return builder.toString();
}
}
private static class ConfigAndId {
//
// Private methods
//
private Object mConfig;
private String mId;
//
// Constructor
//
public ConfigAndId( Object config, String id ) {
mConfig = config;
mId = id;
}
//
// Public methods
//
public Object getConfig() {
return mConfig;
}
public String getId() {
return mId;
}
}
}