// ========================================================================
// $Id: XmlConfiguration.java,v 1.28 2005/08/13 08:12:14 gregwilkins Exp $
// Copyright 1999-2004 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========================================================================
package org.openqa.jetty.xml;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.openqa.jetty.log.LogFactory;
import org.openqa.jetty.util.InetAddrPort;
import org.openqa.jetty.util.Loader;
import org.openqa.jetty.util.LogSupport;
import org.openqa.jetty.util.Resource;
import org.openqa.jetty.util.TypeUtil;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/* ------------------------------------------------------------ */
/**
* Configure Objects from XML. This class reads an XML file conforming to the configure.dtd DTD and
* uses it to configure and object by calling set, put or other methods on the object.
*
* @version $Id: XmlConfiguration.java,v 1.28 2005/08/13 08:12:14 gregwilkins Exp $
* @author Greg Wilkins (gregw)
*/
public class XmlConfiguration
{
private static Log log = LogFactory.getLog(XmlConfiguration.class);
private static Class[] __primitives = { Boolean.TYPE, Character.TYPE, Byte.TYPE, Short.TYPE,
Integer.TYPE, Long.TYPE, Float.TYPE, Double.TYPE, Void.TYPE};
private static Class[] __primitiveHolders = { Boolean.class, Character.class, Byte.class,
Short.class, Integer.class, Long.class, Float.class, Double.class, Void.class};
/* ------------------------------------------------------------ */
private static XmlParser __parser;
private XmlParser.Node _config;
private Map _idMap = new HashMap();
/* ------------------------------------------------------------ */
private synchronized static void initParser() throws IOException
{
if (__parser != null) return;
__parser = new XmlParser();
URL config13URL = XmlConfiguration.class.getClassLoader().getResource(
"org/mortbay/xml/configure_1_3.dtd");
__parser.redirectEntity("configure.dtd", config13URL);
__parser.redirectEntity("configure_1_3.dtd", config13URL);
__parser.redirectEntity("http://jetty.mortbay.org/configure_1_3.dtd", config13URL);
__parser.redirectEntity("http://jetty.mortbay.org/configure.dtd", config13URL);
__parser.redirectEntity("-//Mort Bay Consulting//DTD Configure 1.3//EN", config13URL);
__parser.redirectEntity("-//Mort Bay Consulting//DTD Configure//EN", config13URL);
URL config12URL = XmlConfiguration.class.getClassLoader().getResource(
"org/mortbay/xml/configure_1_2.dtd");
__parser.redirectEntity("configure_1_2.dtd", config12URL);
__parser.redirectEntity("http://jetty.mortbay.org/configure_1_2.dtd", config12URL);
__parser.redirectEntity("-//Mort Bay Consulting//DTD Configure 1.2//EN", config12URL);
URL config11URL = XmlConfiguration.class.getClassLoader().getResource(
"org/mortbay/xml/configure_1_1.dtd");
__parser.redirectEntity("configure_1_1.dtd", config11URL);
__parser.redirectEntity("http://jetty.mortbay.org/configure_1_1.dtd", config11URL);
__parser.redirectEntity("-//Mort Bay Consulting//DTD Configure 1.1//EN", config11URL);
URL config10URL = XmlConfiguration.class.getClassLoader().getResource(
"org/mortbay/xml/configure_1_0.dtd");
__parser.redirectEntity("configure_1_0.dtd", config10URL);
__parser.redirectEntity("http://jetty.mortbay.org/configure_1_0.dtd", config10URL);
__parser.redirectEntity("-//Mort Bay Consulting//DTD Configure 1.0//EN", config10URL);
}
/* ------------------------------------------------------------ */
/**
* Constructor. Reads the XML configuration file.
*
* @param configuration
*/
public XmlConfiguration(URL configuration) throws SAXException, IOException
{
initParser();
synchronized (__parser)
{
_config = __parser.parse(configuration.toString());
}
}
/* ------------------------------------------------------------ */
/**
* Constructor.
*
* @param configuration String of XML configuration commands excluding the normal XML preamble.
* The String should start with a " <Configure ...." element.
* @exception SAXException
* @exception IOException
*/
public XmlConfiguration(String configuration) throws SAXException, IOException
{
initParser();
configuration = "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n<!DOCTYPE Configure PUBLIC \"-//Mort Bay Consulting//DTD Configure 1.2//EN\" \"http://jetty.mortbay.org/configure_1_2.dtd\">"
+ configuration;
InputSource source = new InputSource(new StringReader(configuration));
synchronized (__parser)
{
_config = __parser.parse(source);
}
}
/* ------------------------------------------------------------ */
/**
* Constructor.
*
* @param configuration An input stream containing a complete e.g. configuration file
* @exception SAXException
* @exception IOException
*/
public XmlConfiguration(InputStream configuration) throws SAXException, IOException
{
initParser();
InputSource source = new InputSource(configuration);
synchronized (__parser)
{
_config = __parser.parse(source);
}
}
/* ------------------------------------------------------------ */
/**
* Configure an object. If the object is of the approprate class, the XML configuration script
* is applied to the object.
*
* @param obj The object to be configured.
* @exception ClassNotFoundException
* @exception NoSuchMethodException
* @exception InvocationTargetException
* @throws IllegalAccessException
* @throws InstantiationException
*/
public void configure(Object obj) throws ClassNotFoundException, NoSuchMethodException,
InvocationTargetException, InstantiationException, IllegalAccessException
{
//Check the class of the object
Class oClass = nodeClass(_config);
if (oClass != null)
{
if (obj != null && !oClass.isInstance(obj))
throw new IllegalArgumentException("Object is not of type " + oClass);
if (obj == null) obj = oClass.newInstance();
}
configure(obj, _config, 0);
}
/* ------------------------------------------------------------ */
/**
* Create a new object and configure it. A new object is created and configured.
*
* @return The newly created configured object.
* @exception ClassNotFoundException
* @exception NoSuchMethodException
* @exception InvocationTargetException
* @exception InstantiationException
* @exception IllegalAccessException
*/
public Object newInstance() throws ClassNotFoundException, NoSuchMethodException,
InvocationTargetException, InstantiationException, IllegalAccessException
{
Class oClass = nodeClass(_config);
Object obj = null;
if (oClass != null) obj = oClass.newInstance();
configure(obj, _config, 0);
return obj;
}
/* ------------------------------------------------------------ */
private Class nodeClass(XmlParser.Node node) throws ClassNotFoundException
{
String className = node.getAttribute("class");
if (className == null) return null;
return Loader.loadClass(XmlConfiguration.class, className);
}
/* ------------------------------------------------------------ */
/*
* Recursive configuration step. This method applies the remaining Set, Put and Call elements to
* the current object. @param obj @param cfg @param i @exception ClassNotFoundException
* @exception NoSuchMethodException @exception InvocationTargetException
*/
private void configure(Object obj, XmlParser.Node cfg, int i) throws ClassNotFoundException,
NoSuchMethodException, InvocationTargetException, IllegalAccessException
{
XmlParser.Node node = null;
try
{
for (; i < cfg.size(); i++)
{
Object o = cfg.get(i);
if (o instanceof String) continue;
node = (XmlParser.Node) o;
String tag = node.getTag();
if ("Set".equals(tag))
set(obj, node);
else if ("Put".equals(tag))
put(obj, node);
else if ("Call".equals(tag))
call(obj, node);
else if ("Get".equals(tag))
get(obj, node);
else if ("New".equals(tag))
newObj(obj, node);
else if ("Ref".equals(tag))
refObj(obj, node);
else
throw new IllegalStateException("Unknown tag: " + tag);
}
}
catch(InvocationTargetException e)
{
log.warn("Exception at "+node.toString(),e.getTargetException());
throw e;
}
catch (Error e)
{
log.debug(node);
throw e;
}
catch (Exception e)
{
log.debug(node);
if (e instanceof NoSuchMethodException) throw (NoSuchMethodException) e;
if (e instanceof InvocationTargetException) throw (InvocationTargetException) e;
if (e instanceof IllegalAccessException) throw (IllegalAccessException) e;
if (e instanceof RuntimeException) throw (RuntimeException) e;
}
}
/* ------------------------------------------------------------ */
/*
* Call a set method. This method makes a best effort to find a matching set method. The type of
* the value is used to find a suitable set method by 1. Trying for a trivial type match. 2.
* Looking for a native type match. 3. Trying all correctly named methods for an auto
* conversion. 4. Attempting to construct a suitable value from original value. @param obj
* @param node
*/
private void set(Object obj, XmlParser.Node node) throws ClassNotFoundException,
NoSuchMethodException, InvocationTargetException, IllegalAccessException
{
String attr = node.getAttribute("name");
String name = "set" + attr.substring(0, 1).toUpperCase() + attr.substring(1);
Object value = value(obj, node);
Object[] arg = { value};
Class oClass = nodeClass(node);
if (oClass != null)
obj = null;
else
oClass = obj.getClass();
Class[] vClass = { Object.class};
if (value != null) vClass[0] = value.getClass();
if (log.isDebugEnabled())
log.debug(obj + "." + name + "(" + vClass[0] + " " + value + ")");
// Try for trivial match
try
{
Method set = oClass.getMethod(name, vClass);
set.invoke(obj, arg);
return;
}
catch (IllegalArgumentException e)
{
LogSupport.ignore(log, e);
}
catch (IllegalAccessException e)
{
LogSupport.ignore(log, e);
}
catch (NoSuchMethodException e)
{
LogSupport.ignore(log, e);
}
// Try for native match
try
{
Field type = vClass[0].getField("TYPE");
vClass[0] = (Class) type.get(null);
Method set = oClass.getMethod(name, vClass);
set.invoke(obj, arg);
return;
}
catch (NoSuchFieldException e)
{
LogSupport.ignore(log, e);
}
catch (IllegalArgumentException e)
{
LogSupport.ignore(log, e);
}
catch (IllegalAccessException e)
{
LogSupport.ignore(log, e);
}
catch (NoSuchMethodException e)
{
LogSupport.ignore(log, e);
}
// Try a field
try
{
Field field = oClass.getField(attr);
if (Modifier.isPublic(field.getModifiers()))
{
field.set(obj, value);
return;
}
}
catch (NoSuchFieldException e)
{
LogSupport.ignore(log, e);
}
// Search for a match by trying all the set methods
Method[] sets = oClass.getMethods();
Method set = null;
for (int s = 0; sets != null && s < sets.length; s++)
{
if (name.equals(sets[s].getName()) && sets[s].getParameterTypes().length == 1)
{
// lets try it
try
{
set = sets[s];
sets[s].invoke(obj, arg);
return;
}
catch (IllegalArgumentException e)
{
LogSupport.ignore(log, e);
}
catch (IllegalAccessException e)
{
LogSupport.ignore(log, e);
}
}
}
// Try converting the arg to the last set found.
if (set != null)
{
try
{
Class sClass = set.getParameterTypes()[0];
if (sClass.isPrimitive())
{
for (int t = 0; t < __primitives.length; t++)
{
if (sClass.equals(__primitives[t]))
{
sClass = __primitiveHolders[t];
break;
}
}
}
Constructor cons = sClass.getConstructor(vClass);
arg[0] = cons.newInstance(arg);
set.invoke(obj, arg);
return;
}
catch (NoSuchMethodException e)
{
LogSupport.ignore(log, e);
}
catch (IllegalAccessException e)
{
LogSupport.ignore(log, e);
}
catch (InstantiationException e)
{
LogSupport.ignore(log, e);
}
}
// No Joy
throw new NoSuchMethodException(oClass + "." + name + "(" + vClass[0] + ")");
}
/* ------------------------------------------------------------ */
/*
* Call a put method.
*
* @param obj @param node
*/
private void put(Object obj, XmlParser.Node node) throws NoSuchMethodException,
ClassNotFoundException, InvocationTargetException, IllegalAccessException
{
if (!(obj instanceof Map))
throw new IllegalArgumentException("Object for put is not a Map: " + obj);
Map map = (Map) obj;
String name = node.getAttribute("name");
Object value = value(obj, node);
map.put(name, value);
if (log.isDebugEnabled()) log.debug(obj + ".put(" + name + "+" + value + ")");
}
/* ------------------------------------------------------------ */
/*
* Call a get method. Any object returned from the call is passed to the configure method to
* consume the remaining elements. @param obj @param node @return @exception
* NoSuchMethodException @exception ClassNotFoundException @exception InvocationTargetException
*/
private Object get(Object obj, XmlParser.Node node) throws NoSuchMethodException,
ClassNotFoundException, InvocationTargetException, IllegalAccessException
{
Class oClass = nodeClass(node);
if (oClass != null)
obj = null;
else
oClass = obj.getClass();
String name = node.getAttribute("name");
String id = node.getAttribute("id");
if (log.isDebugEnabled()) log.debug("get " + name);
try
{
// try calling a getXxx method.
Method method = oClass.getMethod("get" + name.substring(0, 1).toUpperCase()
+ name.substring(1), (java.lang.Class[]) null);
obj = method.invoke(obj, (java.lang.Object[]) null);
configure(obj, node, 0);
}
catch (NoSuchMethodException nsme)
{
try
{
Field field = oClass.getField(name);
obj = field.get(obj);
configure(obj, node, 0);
}
catch (NoSuchFieldException nsfe)
{
throw nsme;
}
}
if (id != null) _idMap.put(id, obj);
return obj;
}
/* ------------------------------------------------------------ */
/*
* Call a method. A method is selected by trying all methods with matching names and number of
* arguments. Any object returned from the call is passed to the configure method to consume the
* remaining elements. Note that if this is a static call we consider only methods declared
* directly in the given class. i.e. we ignore any static methods in superclasses. @param obj
* @param node @return @exception NoSuchMethodException @exception ClassNotFoundException
* @exception InvocationTargetException
*/
private Object call(Object obj, XmlParser.Node node) throws NoSuchMethodException,
ClassNotFoundException, InvocationTargetException, IllegalAccessException
{
String id = node.getAttribute("id");
Class oClass = nodeClass(node);
if (oClass != null)
obj = null;
else if (obj != null) oClass = obj.getClass();
if (oClass == null) throw new IllegalArgumentException(node.toString());
int size = 0;
int argi = node.size();
for (int i = 0; i < node.size(); i++)
{
Object o = node.get(i);
if (o instanceof String) continue;
if (!((XmlParser.Node) o).getTag().equals("Arg"))
{
argi = i;
break;
}
size++;
}
Object[] arg = new Object[size];
for (int i = 0, j = 0; j < size; i++)
{
Object o = node.get(i);
if (o instanceof String) continue;
arg[j++] = value(obj, (XmlParser.Node) o);
}
String method = node.getAttribute("name");
if (log.isDebugEnabled()) log.debug("call " + method);
// Lets just try all methods for now
Method[] methods = oClass.getMethods();
for (int c = 0; methods != null && c < methods.length; c++)
{
if (!methods[c].getName().equals(method)) continue;
if (methods[c].getParameterTypes().length != size) continue;
if (Modifier.isStatic(methods[c].getModifiers()) != (obj == null)) continue;
if ((obj == null) && methods[c].getDeclaringClass() != oClass) continue;
Object n = null;
boolean called = false;
try
{
n = methods[c].invoke(obj, arg);
called = true;
}
catch (IllegalAccessException e)
{
LogSupport.ignore(log, e);
}
catch (IllegalArgumentException e)
{
LogSupport.ignore(log, e);
}
if (called)
{
if (id != null) _idMap.put(id, n);
configure(n, node, argi);
return n;
}
}
throw new IllegalStateException("No Method: " + node + " on " + oClass);
}
/* ------------------------------------------------------------ */
/*
* Create a new value object.
*
* @param obj @param node @return @exception NoSuchMethodException @exception
* ClassNotFoundException @exception InvocationTargetException
*/
private Object newObj(Object obj, XmlParser.Node node) throws NoSuchMethodException,
ClassNotFoundException, InvocationTargetException, IllegalAccessException
{
Class oClass = nodeClass(node);
String id = node.getAttribute("id");
int size = 0;
int argi = node.size();
for (int i = 0; i < node.size(); i++)
{
Object o = node.get(i);
if (o instanceof String) continue;
if (!((XmlParser.Node) o).getTag().equals("Arg"))
{
argi = i;
break;
}
size++;
}
Object[] arg = new Object[size];
for (int i = 0, j = 0; j < size; i++)
{
Object o = node.get(i);
if (o instanceof String) continue;
arg[j++] = value(obj, (XmlParser.Node) o);
}
if (log.isDebugEnabled()) log.debug("new " + oClass);
// Lets just try all constructors for now
Constructor[] constructors = oClass.getConstructors();
for (int c = 0; constructors != null && c < constructors.length; c++)
{
if (constructors[c].getParameterTypes().length != size) continue;
Object n = null;
boolean called = false;
try
{
n = constructors[c].newInstance(arg);
called = true;
}
catch (IllegalAccessException e)
{
LogSupport.ignore(log, e);
}
catch (InstantiationException e)
{
LogSupport.ignore(log, e);
}
catch (IllegalArgumentException e)
{
LogSupport.ignore(log, e);
}
if (called)
{
if (id != null) _idMap.put(id, n);
configure(n, node, argi);
return n;
}
}
throw new IllegalStateException("No Constructor: " + node + " on " + obj);
}
/* ------------------------------------------------------------ */
/*
* Reference an id value object.
*
* @param obj @param node @return @exception NoSuchMethodException @exception
* ClassNotFoundException @exception InvocationTargetException
*/
private Object refObj(Object obj, XmlParser.Node node) throws NoSuchMethodException,
ClassNotFoundException, InvocationTargetException, IllegalAccessException
{
String id = node.getAttribute("id");
obj = _idMap.get(id);
if (obj == null) throw new IllegalStateException("No object for id=" + id);
configure(obj, node, 0);
return obj;
}
/* ------------------------------------------------------------ */
/*
* Create a new array object.
*
* @param obj @param node @return @exception NoSuchMethodException @exception
* ClassNotFoundException @exception InvocationTargetException
*/
private Object newArray(Object obj, XmlParser.Node node) throws NoSuchMethodException,
ClassNotFoundException, InvocationTargetException, IllegalAccessException
{
// Get the type
Class aClass = java.lang.Object.class;
String type = node.getAttribute("type");
String id = node.getAttribute("id");
if (type != null)
{
aClass = TypeUtil.fromName(type);
if (aClass == null)
{
if ("String".equals(type))
aClass = java.lang.String.class;
else if ("URL".equals(type))
aClass = java.net.URL.class;
else if ("InetAddress".equals(type))
aClass = java.net.InetAddress.class;
else if ("InetAddrPort".equals(type))
aClass = org.openqa.jetty.util.InetAddrPort.class;
else
aClass = Loader.loadClass(XmlConfiguration.class, type);
}
}
Object array = Array.newInstance(aClass, node.size());
if (id != null) _idMap.put(id, obj);
for (int i = 0; i < node.size(); i++)
{
Object o = node.get(i);
if (o instanceof String) continue;
XmlParser.Node item = (XmlParser.Node) o;
if (!item.getTag().equals("Item")) throw new IllegalStateException("Not an Item");
id = item.getAttribute("id");
Object v = value(obj, item);
if (v != null) Array.set(array, i, v);
if (id != null) _idMap.put(id, v);
}
return array;
}
/* ------------------------------------------------------------ */
/*
* Get the value of an element. If no value type is specified, then white space is trimmed out
* of the value. If it contains multiple value elements they are added as strings before being
* converted to any specified type. @param node
*/
private Object value(Object obj, XmlParser.Node node) throws NoSuchMethodException,
ClassNotFoundException, InvocationTargetException, IllegalAccessException
{
Object value = null;
// Get the type
String type = node.getAttribute("type");
// Try a ref lookup
String ref = node.getAttribute("ref");
if (ref != null)
{
value = _idMap.get(ref);
}
else
{
// handle trivial case
if (node.size() == 0)
{
if ("String".equals(type)) return "";
return null;
}
// Trim values
int first = 0;
int last = node.size() - 1;
// Handle default trim type
if (type == null || !"String".equals(type))
{
// Skip leading white
Object item = null;
while (first <= last)
{
item = node.get(first);
if (!(item instanceof String)) break;
item = ((String) item).trim();
if (((String) item).length() > 0) break;
first++;
}
// Skip trailing white
while (first < last)
{
item = node.get(last);
if (!(item instanceof String)) break;
item = ((String) item).trim();
if (((String) item).length() > 0) break;
last--;
}
// All white, so return null
if (first > last) return null;
}
if (first == last)
// Single Item value
value = itemValue(obj, node.get(first));
else
{
// Get the multiple items as a single string
StringBuffer buf = new StringBuffer();
synchronized (buf)
{
for (int i = first; i <= last; i++)
{
Object item = node.get(i);
buf.append(itemValue(obj, item));
}
value = buf.toString();
}
}
}
// Untyped or unknown
if (value == null)
{
if ("String".equals(type)) return "";
return null;
}
// Try to type the object
if (type == null)
{
if (value != null && value instanceof String) return ((String) value).trim();
return value;
}
if ("String".equals(type) || "java.lang.String".equals(type)) return value.toString();
Class pClass = TypeUtil.fromName(type);
if (pClass != null) return TypeUtil.valueOf(pClass, value.toString());
if ("URL".equals(type) || "java.net.URL".equals(type))
{
if (value instanceof URL) return value;
try
{
return new URL(value.toString());
}
catch (MalformedURLException e)
{
throw new InvocationTargetException(e);
}
}
if ("InetAddress".equals(type) || "java.net.InetAddress".equals(type))
{
if (value instanceof InetAddress) return value;
try
{
return InetAddress.getByName(value.toString());
}
catch (UnknownHostException e)
{
throw new InvocationTargetException(e);
}
}
if ("InetAddrPort".equals(type) || "org.openqa.jetty.util.InetAddrPort".equals(type))
{
if (value instanceof InetAddrPort) return value;
try
{
return new InetAddrPort(value.toString());
}
catch (UnknownHostException e)
{
throw new InvocationTargetException(e);
}
}
throw new IllegalStateException("Unknown type " + type);
}
/* ------------------------------------------------------------ */
/*
* Get the value of a single element. @param obj @param item @return @exception
* ClassNotFoundException
*/
private Object itemValue(Object obj, Object item) throws NoSuchMethodException,
ClassNotFoundException, InvocationTargetException, IllegalAccessException
{
// String value
if (item instanceof String) return item;
XmlParser.Node node = (XmlParser.Node) item;
String tag = node.getTag();
if ("Call".equals(tag)) return call(obj, node);
if ("Get".equals(tag)) return get(obj, node);
if ("New".equals(tag)) return newObj(obj, node);
if ("Ref".equals(tag)) return refObj(obj, node);
if ("Array".equals(tag)) return newArray(obj, node);
if ("SystemProperty".equals(tag))
{
String name = node.getAttribute("name");
String defaultValue = node.getAttribute("default");
return System.getProperty(name, defaultValue);
}
log.warn("Unknown value tag: " + node, new Throwable());
return null;
}
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
public static void main(String[] arg)
{
try
{
for (int i = 0; i < arg.length; i++)
new XmlConfiguration(Resource.newResource(arg[i]).getURL()).newInstance();
}
catch (Exception e)
{
log.warn(LogSupport.EXCEPTION, e);
}
}
}