org.eclipse.jetty.xml.XmlConfiguration Maven / Gradle / Ivy
Show all versions of driver-cql-shaded Show documentation
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.xml;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.AccessController;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Queue;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jetty.util.LazyList;
import org.eclipse.jetty.util.Loader;
import org.eclipse.jetty.util.MultiException;
import org.eclipse.jetty.util.Pool;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.annotation.Name;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.resource.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;
/**
* Configures 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.
* The actual XML file format may be changed (eg to spring XML) by implementing the
* {@link ConfigurationProcessorFactory} interface to be found by the
* {@link ServiceLoader} by using the DTD and first tag element in the file.
* Note that DTD will be null if validation is off.
* The configuration can be parameterised with properties that are looked up via the
* Property XML element and set on the configuration via the map returned from
* {@link #getProperties()}
* The configuration can create and lookup beans by ID. If multiple configurations are used, then it
* is good practise to copy the entries from the {@link #getIdMap()} of a configuration to the next
* configuration so that they can share an ID space for beans.
*/
public class XmlConfiguration
{
private static final Logger LOG = LoggerFactory.getLogger(XmlConfiguration.class);
private static final Class>[] PRIMITIVES =
{
Boolean.TYPE, Character.TYPE, Byte.TYPE, Short.TYPE, Integer.TYPE, Long.TYPE, Float.TYPE, Double.TYPE, Void.TYPE
};
private static final Class>[] BOXED_PRIMITIVES =
{
Boolean.class, Character.class, Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class,
Void.class
};
private static final Class>[] SUPPORTED_COLLECTIONS =
{
ArrayList.class, HashSet.class, Queue.class, List.class, Set.class, Collection.class
};
private static final List PROCESSOR_FACTORIES = TypeUtil.serviceProviderStream(ServiceLoader.load(ConfigurationProcessorFactory.class))
.flatMap(p -> Stream.of(p.get()))
.collect(Collectors.toList());
private static final Pool __parsers =
new Pool<>(Pool.StrategyType.THREAD_ID, Math.min(8, Runtime.getRuntime().availableProcessors()));
public static final Comparator EXECUTABLE_COMPARATOR = (e1, e2) ->
{
// Favour methods with less parameters
int count = e1.getParameterCount();
int compare = Integer.compare(count, e2.getParameterCount());
if (compare == 0 && count > 0)
{
Parameter[] p1 = e1.getParameters();
Parameter[] p2 = e2.getParameters();
// Favour methods without varargs
compare = Boolean.compare(p1[count - 1].isVarArgs(), p2[count - 1].isVarArgs());
if (compare == 0)
{
// Rank by differences in the parameters
for (int i = 0; i < count; i++)
{
Class> t1 = p1[i].getType();
Class> t2 = p2[i].getType();
if (t1 != t2)
{
// Favour derived type over base type
compare = Boolean.compare(t1.isAssignableFrom(t2), t2.isAssignableFrom(t1));
if (compare == 0)
{
// favour primitive type over reference
compare = Boolean.compare(!t1.isPrimitive(), !t2.isPrimitive());
if (compare == 0)
// Use name to avoid non determinant sorting
compare = t1.getName().compareTo(t2.getName());
}
// break on the first different parameter (should always be true)
if (compare != 0)
break;
}
}
}
compare = Math.min(1, Math.max(compare, -1));
}
return compare;
};
/**
* Set the standard IDs and properties expected in a jetty XML file:
*
* - RefId Server
* - Property jetty.home
* - Property jetty.home.uri
* - Property jetty.base
* - Property jetty.base.uri
* - Property jetty.webapps
* - Property jetty.webapps.uri
*
*
* @param server The Server object to set
* @param webapp The webapps Resource
*/
public void setJettyStandardIdsAndProperties(Object server, Resource webapp)
{
try
{
if (server != null)
getIdMap().put("Server", server);
Path home = Paths.get(System.getProperty("jetty.home", "."));
getProperties().put("jetty.home", home.toString());
getProperties().put("jetty.home.uri", normalizeURI(home.toUri().toASCIIString()));
Path base = Paths.get(System.getProperty("jetty.base", home.toString()));
getProperties().put("jetty.base", base.toString());
getProperties().put("jetty.base.uri", normalizeURI(base.toUri().toASCIIString()));
if (webapp != null)
{
Path webappPath = webapp.getFile().toPath().toAbsolutePath();
getProperties().put("jetty.webapp", webappPath.toString());
getProperties().put("jetty.webapps", webappPath.getParent().toString());
getProperties().put("jetty.webapps.uri", normalizeURI(webappPath.getParent().toUri().toString()));
}
}
catch (Exception e)
{
LOG.warn("Unable to get webapp file reference", e);
}
}
public static String normalizeURI(String uri)
{
if (uri.endsWith("/"))
return uri.substring(0, uri.length() - 1);
return uri;
}
private final Map _idMap = new HashMap<>();
private final Map _propertyMap = new HashMap<>();
private final Resource _location;
private final String _dtd;
private ConfigurationProcessor _processor;
ConfigurationParser getParser()
{
Pool.Entry entry = __parsers.acquire(ConfigurationParser::new);
if (entry == null)
return new ConfigurationParser(null);
return entry.getPooled();
}
/**
* Reads and parses the XML configuration file.
*
* @param resource the Resource to the XML configuration
* @throws IOException if the configuration could not be read
* @throws SAXException if the configuration could not be parsed
*/
public XmlConfiguration(Resource resource) throws SAXException, IOException
{
try (ConfigurationParser parser = getParser(); InputStream inputStream = resource.getInputStream())
{
_location = resource;
setConfig(parser.parse(inputStream));
_dtd = parser.getDTD();
}
}
@Override
public String toString()
{
if (_location == null)
{
return "UNKNOWN-LOCATION";
}
return _location.toString();
}
private void setConfig(XmlParser.Node config)
{
if ("Configure".equals(config.getTag()))
{
_processor = new JettyXmlConfiguration();
}
else if (PROCESSOR_FACTORIES != null)
{
for (ConfigurationProcessorFactory factory : PROCESSOR_FACTORIES)
{
_processor = factory.getConfigurationProcessor(_dtd, config.getTag());
if (_processor != null)
break;
}
if (_processor == null)
throw new IllegalStateException("Unknown configuration type: " + config.getTag() + " in " + this);
}
else
{
throw new IllegalArgumentException("Unknown XML tag:" + config.getTag());
}
_processor.init(_location, config, this);
}
/**
* Get the map of ID String to Objects that is used to hold
* and lookup any objects by ID.
*
* A New, Get or Call XML element may have an
* id attribute which will cause the resulting object to be placed into
* this map. A Ref XML element will lookup an object from this map.
*
* When chaining configuration files, it is good practise to copy the
* ID entries from the ID map to the map of the next configuration, so
* that they may share an ID space
*
*
* @return A modifiable map of ID strings to Objects
*/
public Map getIdMap()
{
return _idMap;
}
/**
* Get the map of properties used by the Property XML element
* to parametrize configuration.
*
* @return A modifiable map of properties.
*/
public Map getProperties()
{
return _propertyMap;
}
/**
* Applies the XML configuration script to the given object.
*
* @param obj The object to be configured, which must be of a type or super type
* of the class attribute of the <Configure> element.
* @return the configured object
* @throws Exception if the configuration fails
*/
public Object configure(Object obj) throws Exception
{
return _processor.configure(obj);
}
/**
* Applies the XML configuration script.
* If the root element of the configuration has an ID, an object is looked up by ID and its type checked
* against the root element's type.
* Otherwise a new object of the type specified by the root element is created.
*
* @return The newly created configured object.
* @throws Exception if the configuration fails
*/
public Object configure() throws Exception
{
if (LOG.isDebugEnabled())
LOG.debug("Configure {}", _location);
return _processor.configure();
}
/**
* Initialize a new Object defaults.
* This method must be called by any {@link ConfigurationProcessor} when it
* creates a new instance of an object before configuring it, so that a derived
* XmlConfiguration class may inject default values.
*
* @param object the object to initialize defaults on
*/
public void initializeDefaults(Object object)
{
}
private static class JettyXmlConfiguration implements ConfigurationProcessor
{
XmlParser.Node _root;
XmlConfiguration _configuration;
@Override
public void init(Resource resource, XmlParser.Node root, XmlConfiguration configuration)
{
_root = root;
_configuration = configuration;
}
@Override
public Object configure(Object obj) throws Exception
{
// Check the class of the object
Class> oClass = nodeClass(_root);
if (oClass != null && !oClass.isInstance(obj))
{
String loaders = (oClass.getClassLoader() == obj.getClass().getClassLoader()) ? "" : "Object Class and type Class are from different loaders.";
throw new IllegalArgumentException("Object of class '" + obj.getClass().getCanonicalName() + "' is not of type '" + oClass.getCanonicalName() + "'. " + loaders + " in " + _configuration);
}
String id = _root.getAttribute("id");
if (id != null)
_configuration.getIdMap().put(id, obj);
AttrOrElementNode aoeNode = new AttrOrElementNode(obj, _root, "Id", "Class", "Arg");
// The Object already existed, if it has nodes, warn about them not being used.
aoeNode.getNodes("Arg")
.forEach((node) -> LOG.warn("Ignored arg {} in {}", node, this._configuration._location));
configure(obj, _root, aoeNode.getNext());
return obj;
}
@Override
public Object configure() throws Exception
{
AttrOrElementNode aoeNode = new AttrOrElementNode(_root, "Id", "Class", "Arg");
String id = aoeNode.getString("Id");
String clazz = aoeNode.getString("Class");
Object obj = id == null ? null : _configuration.getIdMap().get(id);
Class> oClass = clazz != null ? Loader.loadClass(clazz) : obj == null ? null : obj.getClass();
if (LOG.isDebugEnabled())
LOG.debug("Configure {} {}", oClass, obj);
if (obj == null && oClass != null)
{
try
{
obj = construct(oClass, new Args(null, oClass, aoeNode.getNodes("Arg")));
if (id != null)
_configuration.getIdMap().put(id, obj);
}
catch (NoSuchMethodException x)
{
throw new IllegalStateException(String.format("No matching constructor %s in %s", oClass, _configuration));
}
}
else
{
// The Object already existed, if it has nodes, warn about them not being used.
aoeNode.getNodes("Arg")
.forEach((node) -> LOG.warn("Ignored arg {} in {}", node, this._configuration._location));
}
_configuration.initializeDefaults(obj);
configure(obj, _root, aoeNode.getNext());
return obj;
}
private static Class> nodeClass(XmlParser.Node node) throws ClassNotFoundException
{
String className = node.getAttribute("class");
if (className == null)
return null;
return Loader.loadClass(className);
}
/**
* Recursive configuration routine.
* This method applies the nested Set, Put, Call, etc. elements to the given object.
*
* @param obj the object to configure
* @param cfg the XML nodes of the configuration
* @param i the index of the XML nodes
* @throws Exception if the configuration fails
*/
public void configure(Object obj, XmlParser.Node cfg, int i) throws Exception
{
// Process real arguments
for (; i < cfg.size(); i++)
{
Object o = cfg.get(i);
if (o instanceof String)
continue;
XmlParser.Node node = (XmlParser.Node)o;
try
{
String tag = node.getTag();
switch (tag)
{
case "Arg":
case "Class":
case "Id":
throw new IllegalStateException("Element '" + tag + "' not skipped");
case "Set":
set(obj, node);
break;
case "Put":
put(obj, node);
break;
case "Call":
call(obj, node);
break;
case "Get":
get(obj, node);
break;
case "New":
newObj(obj, node);
break;
case "Array":
newArray(obj, node);
break;
case "Map":
newMap(obj, node);
break;
case "Ref":
refObj(node);
break;
case "Property":
propertyObj(node);
break;
case "SystemProperty":
systemPropertyObj(node);
break;
case "Env":
envObj(node);
break;
default:
throw new IllegalStateException("Unknown tag: " + tag + " in " + _configuration);
}
}
catch (Exception e)
{
LOG.warn("Config error {} at {} in {}", e.toString(), node, _configuration);
throw e;
}
}
}
/**
* Call a setter 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:
*
* - Trying for a trivial type match
* - Looking for a native type match
* - Trying all correctly named methods for an auto conversion
* - Attempting to construct a suitable value from original value
*
*
* @param obj the enclosing object
* @param node the <Set> XML node
*/
private void set(Object obj, XmlParser.Node node) throws Exception
{
String attr = node.getAttribute("name");
String id = node.getAttribute("id");
String property = node.getAttribute("property");
String propertyValue = null;
// Look for a property value
if (property != null)
{
Map properties = _configuration.getProperties();
propertyValue = properties.get(property);
// If no property value, then do not set
if (propertyValue == null)
return;
}
String name = "set" + attr.substring(0, 1).toUpperCase(Locale.ENGLISH) + attr.substring(1);
Object value = value(obj, node);
if (value == null)
value = propertyValue;
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("XML {}.{} ({})", (obj != null ? obj.toString() : oClass.getName()), name, value);
MultiException me = new MultiException();
String types = null;
Object setValue = value;
try
{
// Try for trivial match
try
{
Method set = oClass.getMethod(name, vClass);
invokeMethod(set, obj, arg);
return;
}
catch (IllegalArgumentException | IllegalAccessException | NoSuchMethodException e)
{
LOG.trace("IGNORED", e);
me.add(e);
}
// Try for native match
try
{
Field type = vClass[0].getField("TYPE");
vClass[0] = (Class>)type.get(null);
Method set = oClass.getMethod(name, vClass);
invokeMethod(set, obj, arg);
return;
}
catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException | NoSuchMethodException e)
{
LOG.trace("IGNORED", e);
me.add(e);
}
// Try a field
try
{
Field field = oClass.getField(attr);
if (Modifier.isPublic(field.getModifiers()))
{
try
{
setField(field, obj, value);
return;
}
catch (IllegalArgumentException e)
{
// try to convert String value to field value
if (value instanceof String)
{
try
{
value = TypeUtil.valueOf(field.getType(), ((String)value).trim());
setField(field, obj, value);
return;
}
catch (Exception e2)
{
e.addSuppressed(e2);
throw e;
}
}
}
}
}
catch (NoSuchFieldException e)
{
LOG.trace("IGNORED", e);
me.add(e);
}
// Search for a match by trying all the set methods
Method[] sets = oClass.getMethods();
Method set = null;
for (Method setter : sets)
{
if (setter.getParameterCount() != 1)
continue;
Class>[] paramTypes = setter.getParameterTypes();
if (name.equals(setter.getName()))
{
types = types == null ? paramTypes[0].getName() : (types + "," + paramTypes[0].getName());
// lets try it
try
{
set = setter;
invokeMethod(set, obj, arg);
return;
}
catch (IllegalArgumentException | IllegalAccessException e)
{
LOG.trace("IGNORED", e);
me.add(e);
}
try
{
for (Class> c : SUPPORTED_COLLECTIONS)
{
if (paramTypes[0].isAssignableFrom(c))
{
setValue = convertArrayToCollection(value, c);
invokeMethod(setter, obj, setValue);
return;
}
}
}
catch (IllegalAccessException e)
{
LOG.trace("IGNORED", e);
me.add(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 = BOXED_PRIMITIVES[t];
break;
}
}
}
Constructor> cons = sClass.getConstructor(vClass);
arg[0] = cons.newInstance(arg);
_configuration.initializeDefaults(arg[0]);
invokeMethod(set, obj, arg);
setValue = arg[0];
return;
}
catch (NoSuchMethodException | IllegalAccessException | InstantiationException e)
{
LOG.trace("IGNORED", e);
me.add(e);
}
}
setValue = null;
}
finally
{
if (id != null && setValue != null)
_configuration.getIdMap().put(id, setValue);
}
// No Joy
String message = oClass + "." + name + "(" + vClass[0] + ")";
if (types != null)
message += ". Found setters for " + types;
NoSuchMethodException failure = new NoSuchMethodException(message);
for (int i = 0; i < me.size(); i++)
{
failure.addSuppressed(me.getThrowable(i));
}
throw failure;
}
private Object invokeConstructor(Constructor> constructor, Object... args) throws IllegalAccessException, InvocationTargetException, InstantiationException
{
Object result = constructor.newInstance(args);
if (constructor.getAnnotation(Deprecated.class) != null)
LOG.warn("Deprecated constructor {} in {}", constructor, _configuration);
return result;
}
private Object invokeMethod(Method method, Object obj, Object... args) throws IllegalAccessException, InvocationTargetException
{
Object result = method.invoke(obj, args);
if (method.getAnnotation(Deprecated.class) != null)
LOG.warn("Deprecated method {} in {}", method, _configuration);
return result;
}
private Object getField(Field field, Object object) throws IllegalAccessException
{
Object result = field.get(object);
if (field.getAnnotation(Deprecated.class) != null)
LOG.warn("Deprecated field {} in {}", field, _configuration);
return result;
}
private void setField(Field field, Object obj, Object arg) throws IllegalAccessException
{
field.set(obj, arg);
if (field.getAnnotation(Deprecated.class) != null)
LOG.warn("Deprecated field {} in {}", field, _configuration);
}
/**
* @param array the array to convert
* @param collectionType the desired collection type
* @return a collection of the desired type if the array can be converted
*/
private static Collection> convertArrayToCollection(Object array, Class> collectionType)
{
if (array == null)
return null;
Collection> collection = null;
if (array.getClass().isArray())
{
if (collectionType.isAssignableFrom(ArrayList.class))
collection = convertArrayToArrayList(array);
else if (collectionType.isAssignableFrom(HashSet.class))
collection = new HashSet<>(convertArrayToArrayList(array));
}
if (collection == null)
throw new IllegalArgumentException("Can't convert \"" + array.getClass() + "\" to " + collectionType);
return collection;
}
private static ArrayList