Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.metawidget.config.ConfigReader Maven / Gradle / Ivy
// Metawidget
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package org.metawidget.config;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Stack;
import java.util.regex.Pattern;
import javax.xml.parsers.SAXParserFactory;
import org.metawidget.iface.Immutable;
import org.metawidget.iface.MetawidgetException;
import org.metawidget.inspector.iface.InspectorException;
import org.metawidget.util.ArrayUtils;
import org.metawidget.util.ClassUtils;
import org.metawidget.util.CollectionUtils;
import org.metawidget.util.LogUtils;
import org.metawidget.util.LogUtils.Log;
import org.metawidget.util.XmlUtils.CachingContentHandler;
import org.metawidget.util.simple.StringUtils;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
/**
* Helper class for reading metadata.xml
files and configuring Metawidgets.
*
* In spirit, metadata.xml
is a general-purpose mechanism for configuring JavaBeans
* based on XML files. In practice, there are some Metawidget-specific features such as:
*
*
* support for reusing immutable objects (as defined by isImmutable
)
* caching XML input based on resource name (uses XmlUtils.CachingContextHandler
)
* resolving resources from specialized locations, such as under WEB-INF
using
* ServletContext.getResource
(ConfigReader
implements
* ResourceResolver
)
*
*
* This class is not just static methods, because ConfigReaders need to be able to be subclassed
* (eg. ServletConfigReader
)
*
Important
*
* ConfigReader
's support for reusing immutable objects (eg. JpaInspector
)
* that use config objects (eg. JpaInspectorConfig
) is dependant on the config object
* overriding equals
and hashCode
. Failure to override these
* methods may result in your object not being reused, or being reused inappropriately .
*
* @author Richard Kennard
*/
public class ConfigReader
implements ResourceResolver, Immutable {
//
// Package private statics
//
/**
* Dummy config to cache by if immutable has no Config.
*/
/* package private */static final String IMMUTABLE_NO_CONFIG = "no-config";
/* package private */static final Log LOG = LogUtils.getLog( ConfigReader.class );
/* package private */static final String JAVA_NAMESPACE_PREFIX = "java:";
//
// Protected members
//
protected final SAXParserFactory mFactory;
//
// Private members
//
/**
* Cache of resource content based on resource name
*/
/* package private */final Map mResourceCache = CollectionUtils.newHashMap();
/**
* Cache of objects that are immutable, indexed by a unique location (ie. the resource name) and
* element number. This is a broad-grained cache that can prune off large portions of the tree.
* For example, it can cache a CompositeInspector
at the top-level, including all
* child Inspector
s and their various xxxConfig
s.
*/
/* package private */final Map> mImmutableByLocationCache = CollectionUtils.newHashMap();
/**
* Cache of objects that are immutable, indexed by their Class (and within that their Config).
* This is a more fine-grained cache than mImmutableByLocationCache, but is more widely
* applicable. For example, it can cache the same Inspector
between different XMLs
* from different InputStream
s, and the same PropertyStyle
across
* multiple different Inspector
s.
*/
/* package private */final Map, Map> mImmutableByClassCache = CollectionUtils.newWeakHashMap();
/**
* Cache of objects that are immutable, indexed by their id. This is a less automatic cache than
* either mImmutableByLocationCache or mImmutableByClassCache because the developer has to
* specify an id explicitly. But it leads to cleaner metawidget.xml files because developers
* need only specify, say, a PropertyStyle with nested Config options once.
*/
/* package private */final Map mImmutableByIdCache = CollectionUtils.newHashMap();
/**
* Patterns do not cache well, because java.util.regex.Pattern
does not override
* equals
or hashCode
. Therefore we cache them manually to return the
* same instance.
*/
/* package private */final Map mPatternCache = CollectionUtils.newHashMap();
//
// Constructor
//
public ConfigReader() {
mFactory = SAXParserFactory.newInstance();
mFactory.setNamespaceAware( true );
}
//
// Public methods
//
/**
* Read configuration from an application resource.
*
* This is a convenience method for configure( String, Object )
that casts the
* returned Object to an instance of the given toConfigure
class.
*
* @param resource
* resource name that will be looked up using openResource
* @param toConfigure
* class to instantiate. Can be a superclass 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
*/
@SuppressWarnings( "unchecked" )
public T configure( String resource, Class toConfigure, String... names ) {
return (T) configure( resource, (Object) toConfigure, names );
}
/**
* Read configuration from an application resource.
*
* This version of configure
uses openResource
to open the specified
* resource. It assumes the resource name is a unique key, so subsequent calls do not need to
* re-open the resource, or re-parse it, making this verison of configure
much
* faster than configure( InputStream, Object )
.
*
* This version further caches any immutable objects, in the same way as
* configure( InputStream, Object )
(see the JavaDoc for that method).
*
* @param resource
* resource name that will be looked up using openResource
* @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( String resource, Object toConfigure, String... names ) {
ConfigHandler configHandler = new ConfigHandler( toConfigure, names );
// Establish cache
String locationKey = resource + StringUtils.SEPARATOR_FORWARD_SLASH;
if ( toConfigure instanceof Class ) {
locationKey += ( (Class) toConfigure ).getName();
} else if ( toConfigure != null ) {
locationKey += toConfigure.getClass().getName();
}
locationKey += ArrayUtils.toString( names, StringUtils.SEPARATOR_FORWARD_SLASH, true, false );
Map 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 {
synchronized( mImmutableByLocationCache ) {
LOG.debug( "Reading resource from {0}", locationKey );
cachingContentHandler = new CachingContentHandler( configHandler );
configHandler.setCachingContentHandler( cachingContentHandler );
mFactory.newSAXParser().parse( 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 is a convenience method for configure( InputStream, Object )
that casts the
* returned Object to an instance of the given toConfigure
class.
*
* @param stream
* XML input as a stream
* @param toConfigure
* class to instantiate. Can be a superclass 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
*/
@SuppressWarnings( "unchecked" )
public T configure( InputStream stream, Class toConfigure, String... names ) {
return (T) configure( stream, (Object) toConfigure, names );
}
/**
* 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 );
}
}
/**
* Locate the given resource by trying, in order:
*
*
* the current thread's context classloader, if any
* the classloader that loaded ClassUtils
*
*/
public InputStream openResource( String resource ) {
return new SimpleResourceResolver().openResource( resource );
}
//
// 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 ( "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 ( "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 );
}
// (use new Integer, not Integer.valueOf, so that we're 1.4 compatible)
if ( "int".equals( name ) ) {
return new Integer( recordedText );
}
// (use new Boolean, not Boolean.valueOf, so that we're 1.4 compatible)
if ( "boolean".equals( name ) ) {
return new Boolean( 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 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 {
Object enumValue = Enum.valueOf( (Class) toResolveTo, (String) nativeValue );
return enumValue;
} 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
*/
protected Class lookupClass( String uri, String localName )
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.uppercaseFirstLetter( localName );
String classToConstruct = packagePrefix + StringUtils.SEPARATOR_DOT_CHAR + uppercasedLocalName;
Class clazz = ClassUtils.niceForName( classToConstruct );
if ( clazz == null ) {
// Try inner class
String innerClassToConstruct = packagePrefix + '$' + uppercasedLocalName;
clazz = ClassUtils.niceForName( innerClassToConstruct );
if ( clazz == null ) {
throw MetawidgetException.newException( "No such class " + classToConstruct + " or supported tag <" + localName + ">" );
}
}
// Return it
return clazz;
}
/**
* 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 class ConfigHandler
extends DefaultHandler {
//
// Private statics
//
/**
* Possible 'encountered' states.
*
* Note: not using enum, for JDK 1.4 compatibility.
*/
private static final int ENCOUNTERED_METHOD = 0;
private static final int ENCOUNTERED_NATIVE_TYPE = 1;
private static final int ENCOUNTERED_NATIVE_COLLECTION_TYPE = 2;
private static final int ENCOUNTERED_CONFIGURED_TYPE = 3;
private static final int ENCOUNTERED_JAVA_OBJECT = 4;
private static final int ENCOUNTERED_ALREADY_CACHED_IMMUTABLE = 5;
private static final int ENCOUNTERED_WRONG_TYPE = 6;
private static final int ENCOUNTERED_WRONG_NAME = 7;
/**
* Possible 'expecting' states.
*
* Note: not using enum, for JDK 1.4 compatibility.
*/
private static final int EXPECTING_ROOT = 0;
private static final int EXPECTING_TO_CONFIGURE = 1;
private static final int EXPECTING_OBJECT = 2;
private static final int EXPECTING_METHOD = 3;
private static final int EXPECTING_CLOSE_OBJECT_WITH_REFID = 4;
//
// 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 int mExpecting = EXPECTING_ROOT;
/**
* Stack of encountered states in the XML tree.
*/
private Stack mEncountered = CollectionUtils.newStack();
// (use StringBuffer for J2SE 1.4 compatibility)
private StringBuffer mBufferValue;
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() ) {
throw MetawidgetException.newException( "No match for " + mToConfigure + " 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;
}
if ( Character.isUpperCase( localName.charAt( 0 ) ) ) {
throw MetawidgetException.newException( "XML node '" + localName + "' should start with a lowercase letter" );
}
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 EXPECTING_ROOT:
if ( mToConfigure == null ) {
mExpecting = EXPECTING_OBJECT;
} else {
mExpecting = EXPECTING_TO_CONFIGURE;
}
break;
case EXPECTING_TO_CONFIGURE: {
// Initial elements must be at depth == 2
if ( mDepth != 2 ) {
return;
}
Class toConfigureClass = lookupClass( uri, localName );
// Match by Class...
if ( mToConfigure instanceof Class ) {
if ( !( (Class) mToConfigure ).isAssignableFrom( toConfigureClass ) ) {
mEncountered.push( ENCOUNTERED_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 ( !toConfigureClass.isAssignableFrom( mToConfigure.getClass() ) ) {
mEncountered.push( ENCOUNTERED_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 );
}
mConstructing.push( mToConfigure );
mEncountered.push( ENCOUNTERED_JAVA_OBJECT );
}
mExpecting = EXPECTING_METHOD;
break;
}
case EXPECTING_OBJECT: {
if ( mCachingContentHandler == null || !mCachingContentHandler.isPaused() ) {
mLocationIndex++;
}
// Native types
if ( isNative( localName ) || isLazyResolvingNative( localName ) ) {
mEncountered.push( ENCOUNTERED_NATIVE_TYPE );
startRecording();
mExpecting = EXPECTING_METHOD;
return;
}
// Native collection types
Object collection = createNativeCollection( localName );
if ( collection != null ) {
mConstructing.push( collection );
mEncountered.push( ENCOUNTERED_NATIVE_COLLECTION_TYPE );
mExpecting = EXPECTING_OBJECT;
return;
}
// JDK 1.4 hack
if ( isJdk14Hack( uri, localName ) ) {
mEncountered.push( ENCOUNTERED_WRONG_TYPE );
mExpecting = EXPECTING_OBJECT;
return;
}
mExpecting = handleNonNativeObject( uri, localName, attributes );
break;
}
case EXPECTING_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( ENCOUNTERED_WRONG_NAME );
mIgnoreNameAfterDepth = mDepth;
// Pause caching (if any)
if ( mCachingContentHandler != null ) {
mCachingContentHandler.pause( false );
}
return;
}
}
}
// Process method
mConstructing.push( new ArrayList() );
mEncountered.push( ENCOUNTERED_METHOD );
mExpecting = EXPECTING_OBJECT;
break;
}
case EXPECTING_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() {
mBufferValue = new StringBuffer();
}
@Override
public void characters( char[] characters, int start, int length ) {
if ( mBufferValue == null ) {
return;
}
mBufferValue.append( characters, start, length );
}
public String endRecording() {
String value = mBufferValue.toString();
mBufferValue = 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 {
int encountered = mEncountered.pop().intValue();
switch ( encountered ) {
case ENCOUNTERED_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 = EXPECTING_OBJECT;
return;
}
case ENCOUNTERED_NATIVE_COLLECTION_TYPE: {
Object nativeCollectionType = mConstructing.pop();
@SuppressWarnings( "unchecked" )
Collection parameters = (Collection) mConstructing.peek();
parameters.add( nativeCollectionType );
mExpecting = EXPECTING_OBJECT;
return;
}
case ENCOUNTERED_CONFIGURED_TYPE:
case ENCOUNTERED_JAVA_OBJECT:
case ENCOUNTERED_ALREADY_CACHED_IMMUTABLE: {
Object object = mConstructing.pop();
if ( encountered == ENCOUNTERED_CONFIGURED_TYPE ) {
Class classToConstruct = lookupClass( uri, localName );
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 = EXPECTING_TO_CONFIGURE;
return;
}
addToConstructing( object );
mExpecting = EXPECTING_OBJECT;
return;
}
case ENCOUNTERED_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.uppercaseFirstLetter( 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 = ClassUtils.getSimpleName( parameterTypes[0].getClass() );
if ( parameterClassName.endsWith( "Config" ) ) {
throw MetawidgetException.newException( "No such method " + methodName + " on " + constructingClass + ". Did you forget config=\"" + parameterClassName + "\"?" );
}
}
throw e;
}
mExpecting = EXPECTING_METHOD;
return;
}
case ENCOUNTERED_WRONG_TYPE:
return;
case ENCOUNTERED_WRONG_NAME:
return;
}
} catch ( RuntimeException e ) {
throw e;
} catch ( Exception e ) {
// Prevent InvocationTargetException 'masking' the error
if ( e instanceof InvocationTargetException ) {
Throwable t = ( (InvocationTargetException) e ).getTargetException();
// getTargetException may return a StackOverflowError
if ( !( t instanceof Exception ) ) {
throw new RuntimeException( t );
}
e = (Exception) t;
}
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 int 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.lowercaseFirstLetter( actualClass.getSimpleName() ).equals( localName )) {
throw InspectorException.newException( "refId=\"" + refId + "\" points to an object of " + actualClass + ", not a <" + localName + ">" );
}
mConstructing.push( immutable );
mEncountered.push( ENCOUNTERED_JAVA_OBJECT );
return EXPECTING_CLOSE_OBJECT_WITH_REFID;
}
Object object = null;
Class classToConstruct = lookupClass( uri, localName );
// 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 = ClassUtils.niceForName( configToConstruct );
if ( configClass == null ) {
throw MetawidgetException.newException( "No such configuration class " + configToConstruct );
}
Object config = configClass.newInstance();
if ( config instanceof NeedsResourceResolver ) {
( (NeedsResourceResolver) config ).setResourceResolver( ConfigReader.this );
}
mConstructing.push( new ConfigAndId( config, attributes.getValue( "id" ) ));
mEncountered.push( ENCOUNTERED_CONFIGURED_TYPE );
// Pause caching (if any)
if ( mIgnoreImmutableAfterDepth == -1 && mCachingContentHandler != null && isImmutable( classToConstruct ) ) {
mCachingContentHandler.pause( true );
mIgnoreImmutableAfterDepth = mDepth;
}
return EXPECTING_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( ENCOUNTERED_JAVA_OBJECT );
return EXPECTING_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
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" );
}
configs.put( configToStoreUnder, immutable );
// Unpause caching (if any)
if ( mCachingContentHandler != null && mDepth < mIgnoreImmutableAfterDepth ) {
mCachingContentHandler.unpause( true );
mIgnoreImmutableAfterDepth = -1;
if ( config != null ) {
putImmutableByLocation( immutable );
}
}
}
/**
* Hack to support Java 5 versus JDK 1.4 differences.
*
* We want Metawidget to be useful 'out of the box' (ie. without configuring) on Java 5.
* This means we need to include MetawidgetAnnotationInspector
in the default
* config. But this also means we will fail with an
* UnsupportedClassVersionError
on JDK 1.4, and we want to run 'out of the box'
* on JDK 1.4.
*
* Therefore we allow ourselves this hack: we do not fail if we encounter an
* UnsupportedClassVersionError
when trying to instantiate a
* MetawidgetAnnotationInspector
.
*/
protected boolean isJdk14Hack( String uri, String localName ) {
if ( !( JAVA_NAMESPACE_PREFIX + "org.metawidget.inspector.annotation" ).equals( uri ) ) {
return false;
}
if ( !"metawidgetAnnotationInspector".equals( localName ) ) {
return false;
}
try {
ClassUtils.niceForName( "org.metawidget.inspector.annotation.MetawidgetAnnotationInspector" );
return false;
} catch ( UnsupportedClassVersionError e ) {
LOG.debug( "\tNot instantiating org.metawidget.inspector.annotation.MetawidgetAnnotationInspector - wrong Java version" );
return true;
}
}
/**
* 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 ClassUtils.getSimpleName( likelyConfigClass );
}
return likelyConfigClass.getName();
}
private String methodToString( Method method ) {
StringBuffer buffer = new StringBuffer();
for ( Class parameterType : method.getParameterTypes() ) {
if ( buffer.length() > 0 ) {
buffer.append( ", " );
}
if ( parameterType.isArray() ) {
buffer.append( ClassUtils.getSimpleName( parameterType.getComponentType() ) );
buffer.append( "[]" );
} else {
buffer.append( ClassUtils.getSimpleName( parameterType ) );
}
}
buffer.insert( 0, "(" );
buffer.insert( 0, method.getName() );
buffer.append( ")" );
return buffer.toString();
}
private String methodToString( Class clazz, String methodName, List args ) {
StringBuffer buffer = new StringBuffer();
for ( Object obj : args ) {
if ( buffer.length() > 0 ) {
buffer.append( ", " );
}
if ( obj == null ) {
buffer.append( "null" );
} else {
buffer.append( ClassUtils.getSimpleName( obj.getClass() ) );
}
}
buffer.insert( 0, "(" );
buffer.insert( 0, methodName );
buffer.insert( 0, '.' );
buffer.insert( 0, clazz );
buffer.append( ")" );
return buffer.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;
}
}
}