org.eclipse.jetty.xml.XmlConfiguration Maven / Gradle / Ivy
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.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.Closeable;
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.URI;
import java.net.URL;
import java.net.UnknownHostException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
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.ConcurrentPool;
import org.eclipse.jetty.util.ExceptionUtil;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.LazyList;
import org.eclipse.jetty.util.Loader;
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.ContainerLifeCycle;
import org.eclipse.jetty.util.component.Environment;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
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()))
.toList();
private static final Pool __parsers =
new ConcurrentPool<>(ConcurrentPool.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)
{
// prefer primitives
compare = Boolean.compare(t2.isPrimitive(), t1.isPrimitive());
if (compare == 0)
{
// prefer interfaces
compare = Boolean.compare(t2.isInterface(), t1.isInterface());
if (compare == 0)
{
// prefer most derived
int d1 = calculateDepth(t1);
int d2 = calculateDepth(t2);
compare = Integer.compare(d2, d1);
}
}
}
// break on the first different parameter
if (compare != 0)
break;
}
}
}
// failsafe is to compare on the generic string
if (compare == 0)
compare = e1.toGenericString().compareTo(e2.toGenericString());
// Return normalized to -1, 0, 1
return Integer.compare(compare, 0);
};
private static int calculateDepth(Class> c)
{
int depth = 0;
if (c.isPrimitive())
return Integer.MIN_VALUE;
if (c.isInterface())
{
Set> interfaces = Set.of(c);
while (!interfaces.isEmpty())
{
depth++;
interfaces = interfaces.stream().flatMap(i -> Arrays.stream(i.getInterfaces())).collect(Collectors.toSet());
}
}
else
{
while (c != Object.class && !c.isPrimitive())
{
depth++;
c = c.getSuperclass();
}
}
return depth;
}
/**
* 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, Path 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)
{
getProperties().put("jetty.webapp", webapp.toString());
getProperties().put("jetty.webapps", webapp.getParent().toString());
getProperties().put("jetty.webapps.uri", normalizeURI(webapp.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;
private final Map _propertyMap;
private final Resource _location;
private final String _dtd;
private ConfigurationProcessor _processor;
public XmlParser getXmlParser()
{
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 not thrown anymore (kept for signature backwards compat)
* @throws SAXException not thrown anymore (kept for signature backwards compat)
* @throws XmlConfigurationException if configuration was not able to loaded from XML provided
*/
public XmlConfiguration(Resource resource) throws SAXException, IOException
{
this(resource, null, null);
}
/**
* Reads and parses the XML configuration file.
*
* @param resource the Resource to the XML configuration
* @param idMap Map of objects with IDs
* @param properties Map of properties
* @throws IOException not thrown anymore (kept for signature backwards compat)
* @throws SAXException not thrown anymore (kept for signature backwards compat)
* @throws XmlConfigurationException if configuration was not able to loaded from XML provided
*/
public XmlConfiguration(Resource resource, Map idMap, Map properties) throws SAXException, IOException
{
XmlParser parser = getXmlParser();
try (InputStream inputStream = resource.newInputStream())
{
_location = resource;
setConfig(parser.parse(inputStream));
_dtd = parser.getDTD();
_idMap = idMap == null ? new HashMap<>() : idMap;
_propertyMap = properties == null ? new HashMap<>() : properties;
}
catch (Throwable t)
{
throw new XmlConfigurationException("Bad Jetty XML configuration in " + this, t);
}
finally
{
if (parser instanceof Closeable closeable)
IO.close(closeable);
}
}
@Override
public String toString()
{
return Objects.toString(_location, "UNKNOWN-LOCATION");
}
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());
}
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)
{
}
/**
* Utility method to resolve a provided path against a directory.
*
* @param dir the directory (should be a directory reference, does not have to exist)
* @param destPath the destination path (can be relative or absolute, syntax depends on OS + FileSystem in use,
* and does not need to exist)
* @return String to resolved and normalized path, or null if dir or destPath is empty.
*/
public static String resolvePath(String dir, String destPath)
{
if (StringUtil.isEmpty(dir) || StringUtil.isEmpty(destPath))
return null;
return Paths.get(dir).resolve(destPath).normalize().toString();
}
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);
}
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, _configuration));
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("No matching constructor " + oClass);
}
}
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, _configuration));
}
_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);
}
}
catch (Exception e)
{
LOG.warn("Config error {} at {} in {}", e, 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 name = node.getAttribute("name");
String setter = "set" + name.substring(0, 1).toUpperCase(Locale.ENGLISH) + name.substring(1);
String id = node.getAttribute("id");
String property = node.getAttribute("property");
String propertyValue = null;
Class> oClass = nodeClass(node);
if (oClass == null)
oClass = obj.getClass();
// 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)
{
// check that there is at least one setter or field that could have matched
if (Arrays.stream(oClass.getMethods()).noneMatch(m -> m.getName().equals(setter)) &&
Arrays.stream(oClass.getFields()).filter(f -> Modifier.isPublic(f.getModifiers())).noneMatch(f -> f.getName().equals(name)))
{
NoSuchMethodException e = new NoSuchMethodException(String.format("No method '%s' on %s", setter, oClass.getName()));
e.addSuppressed(new NoSuchFieldException(String.format("No field '%s' on %s", name, oClass.getName())));
throw e;
}
// otherwise it is a noop
return;
}
}
Object value = value(obj, node);
if (value == null)
value = propertyValue;
Object[] arg = {value};
Class> vClass = Object.class;
if (value != null)
vClass = value.getClass();
if (LOG.isDebugEnabled())
LOG.debug("XML {}.{} ({})", (obj != null ? obj.toString() : oClass.getName()), setter, value);
List errors = new ArrayList<>();
String types = null;
Object setValue = value;
try
{
// Try for trivial match
try
{
Method set = oClass.getMethod(setter, vClass);
invokeMethod(set, obj, arg);
return;
}
catch (IllegalArgumentException | IllegalAccessException | NoSuchMethodException e)
{
LOG.trace("IGNORED", e);
errors.add(e);
}
// Try for native match
try
{
Field type = vClass.getField("TYPE");
vClass = (Class>)type.get(null);
Method set = oClass.getMethod(setter, vClass);
invokeMethod(set, obj, arg);
return;
}
catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException | NoSuchMethodException e)
{
LOG.trace("IGNORED", e);
errors.add(e);
}
// Try a field
try
{
Field field = oClass.getField(name);
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);
errors.add(e);
}
// Search for a match by trying all the set methods
Method[] sets = oClass.getMethods();
Method set = null;
for (Method s : sets)
{
if (s.getParameterCount() != 1)
continue;
Class>[] paramTypes = s.getParameterTypes();
if (setter.equals(s.getName()))
{
types = types == null ? paramTypes[0].getName() : (types + "," + paramTypes[0].getName());
// lets try it
try
{
set = s;
invokeMethod(set, obj, arg);
return;
}
catch (IllegalArgumentException | IllegalAccessException e)
{
LOG.trace("IGNORED", e);
errors.add(e);
}
try
{
for (Class> c : SUPPORTED_COLLECTIONS)
{
if (paramTypes[0].isAssignableFrom(c))
{
setValue = convertArrayToCollection(value, c);
invokeMethod(s, obj, setValue);
return;
}
}
}
catch (IllegalAccessException e)
{
LOG.trace("IGNORED", e);
errors.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);
errors.add(e);
}
}
setValue = null;
}
finally
{
if (id != null && setValue != null)
_configuration.getIdMap().put(id, setValue);
}
// No Joy
String message = oClass + "." + setter + "(" + vClass + ")";
if (types != null)
message += ". Found setters for " + types;
throw ExceptionUtil.withSuppressed(new NoSuchMethodException(message), errors);
}
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