jasima.core.util.TypeUtil Maven / Gradle / Ivy
/*******************************************************************************
* This file is part of jasima, v1.3, the Java simulator for manufacturing and
* logistics.
*
* Copyright (c) 2015 jasima solutions UG
* Copyright (c) 2010-2015 Torsten Hildebrandt and jasima contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*******************************************************************************/
package jasima.core.util;
import static jasima.core.util.converter.TypeConverterJavaBean.exceptionMessage;
import static jasima.core.util.converter.TypeToStringConverter.convertToString;
import jasima.core.util.converter.TypeToStringConverter;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Scanner;
import java.util.WeakHashMap;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
/**
* This class contains a collection of methods concernde with
* reading/writing/creating/converting Bean properties.
*
* @author Torsten Hildebrandt
*/
public class TypeUtil {
/**
* A {@code TypeConversionException} is thrown, when the conversion between
* types fails.
*/
public static class TypeConversionException extends RuntimeException {
private static final long serialVersionUID = -7073321632899508315L;
public TypeConversionException(String s) {
this(s, null);
}
public TypeConversionException(String s, Throwable cause) {
super(s, cause);
}
}
/**
* Determines the type of a property named with propPath using reflection.
*
* This method interprets propPath the same way as
* {@link #getPropertyValue(Object, String)} does.
*
* @param o
* The Object from which to get a property value.
* @param propPath
* A String containing the path to the required property.
* @return The property's type.
*
* @throws RuntimeException
* If there was a problem getting the value. The cause of this
* exception (of type {@link ReflectiveOperationException})
* gives a more detailed indication why the operation failed.
*/
public static Class getPropertyType(Object o, String propPath)
throws RuntimeException {
try {
String[] segments = propPath.split("\\.");
// call getters until we finally arrive where we can call the
// setter-method
for (int i = 0; i < segments.length; i++) {
PropertyDescriptor match = getPropertyDescriptor(o, segments[i]);
if (match == null)
throw new IllegalArgumentException(String.format(
Util.DEF_LOCALE,
"segment '%s' not found of property path '%s'.",
segments[i], propPath));
if (i == segments.length - 1) {
// return property type
return match.getPropertyType();
} else {
// call getter and continue
o = match.getReadMethod().invoke(o);
}
}
throw new AssertionError(); // should never be reached
} catch (ReflectiveOperationException e1) {
throw new RuntimeException(String.format(Util.DEF_LOCALE,
"Can't determine type of property '%s': %s", propPath,
e1.toString()), e1);
}
}
/**
* Gets the current value of a property named with propPath using
* reflection.
*
* Example: getProperty( obj, "a.b.c" ); is equivalent to a direct call
* obj.getA().getB().getC()
*
* @param o
* The Object from which to get a property value.
* @param propPath
* A String containing the path to the required property.
* @return The property value.
*
* @throws RuntimeException
* If there was a problem getting the value. The cause of this
* exception (of type {@link ReflectiveOperationException})
* gives a more detailed indication why the operation failed.
*/
public static Object getPropertyValue(Object o, String propPath)
throws RuntimeException {
try {
String[] segments = propPath.split("\\.");
// call getters until we finally arrive where we can call the
// final get-method
for (int i = 0; i < segments.length; i++) {
PropertyDescriptor match = getPropertyDescriptor(o, segments[i]);
if (match == null)
throw new IllegalArgumentException(String.format(
Util.DEF_LOCALE,
"segment '%s' not found of property path '%s'.",
segments[i], propPath));
// call getter and continue
Method m = match.getReadMethod();
o = m.invoke(o);
}
return o;
} catch (ReflectiveOperationException e1) {
throw new RuntimeException(String.format(Util.DEF_LOCALE,
"Can't get property '%s'.", propPath), e1);
}
}
/**
* Calls
* {@link #setPropertyValue(Object, String, Object, ClassLoader, String[])}
* using the ClassLoader that was used to load {@code TypeUtil} and the
* default package search path {@link Util#DEF_CLASS_SEARCH_PATH}.
*
* @param o
* The object with a property to set.
* @param propPath
* The property path and name of the property to set.
* @param value
* The value to set the property to.
* @see #setPropertyValue(Object, String, Object, ClassLoader, String[])
*/
public static void setPropertyValue(Object o, String propPath, Object value) {
setPropertyValue(o, propPath, value, TypeUtil.class.getClassLoader(),
Util.DEF_CLASS_SEARCH_PATH);
}
/**
* Sets a property named with propPath to a certain value using reflection.
*
* Example: setProperty( obj, "a.b.c", 5 ); is equivalent to a direct call
* obj.getA().getB().setC(5)
*
* @param o
* The object with a property to set.
* @param propPath
* The property path and name of the property to set.
* @param value
* The value to set the property to.
* @param loader
* The {@link ClassLoader} to use when new classes have to be
* loaded.
* @param packageSearchPath
* A list of package names that are used to complete abbreviated
* class names.
*/
public static void setPropertyValue(Object o, String propPath,
Object value, ClassLoader loader, String[] packageSearchPath) {
String getPart;
String setPart;
int i = propPath.lastIndexOf('.');
if (i >= 0) {
getPart = propPath.substring(0, i);
setPart = propPath.substring(i + 1);
} else {
getPart = "";
setPart = propPath;
}
if (getPart.length() > 0)
o = getPropertyValue(o, getPart);
// 'o' now contains the object for which to call the setter
//
// find property descriptor
PropertyDescriptor desc = getPropertyDescriptor(o, setPart);
if (desc == null)
throw new IllegalArgumentException(String.format(Util.DEF_LOCALE,
"Segment '%s' not found of property path '%s'.", setPart,
propPath));
value = convert(value, desc.getPropertyType(), getPart, loader,
packageSearchPath);
try {
desc.getWriteMethod().invoke(o, value);
} catch (ReflectiveOperationException e1) {
throw new RuntimeException(String.format(
"Can't set property '%s' to value '%s': %s", propPath,
value, exceptionMessage(e1)), e1);
}
}
/**
* Converts an object {@code o} (which usually is a {@code String}) to
* another type {@code requiredType}.
*
* @param o
* The object to convert.
* @param requiredType
* The desired type {@code o} should be converted to.
* @param context
* A String describing the context of {@code o}. This is used to
* produce more meaningful error messages.
* @param l
* The {@link ClassLoader} to use.
* @param packageSearchPath
* Search path when looking up classes.
* @param
* Type of returned object.
* @return {@code o} converted to {@code requiredType}.
*/
public static T convert(Object o, Class requiredType,
String context, ClassLoader l, String[] packageSearchPath)
throws TypeConversionException {
T value;
if (o instanceof String) {
value = TypeToStringConverter.convertFromString((String) o,
requiredType, context, l, packageSearchPath);
} else {
value = basicConversions(o, requiredType);
}
return value;
}
/**
* Computes an array of all super-classes and interfaces of
* {@code requiredType}. This method performs a breadth first traversal of
* the class/interface hierarchy. Consider the following example:
*
*
* interface A extends M, N
* interface B extends O
* class Y implements C, D
* class X extends Y implements A, B
*
*
* This will produce the following result for {@code x} as
* {@code requiredType}:
*
*
* { X, Y, A, B, C, D, M, N, O, Object }
*
*
* @param requiredType
* The class for which to compute the type hierarchy.
* @return A list of super classes/interfaces from most to least specific.
*/
public static Class[] computeClasses(Class requiredType) {
ArrayList> resList = new ArrayList<>();
ArrayDeque> currStage = new ArrayDeque<>();
HashSet> seen = new HashSet>();
currStage.addLast(requiredType);
seen.add(requiredType);
while (!currStage.isEmpty()) {
Class c = currStage.removeFirst();
resList.add(c);
Class s = c.getSuperclass();
if (s != null && !seen.contains(s)) {
currStage.addLast(s);
seen.add(s);
}
for (Class i : c.getInterfaces()) {
if (!seen.contains(i)) {
currStage.addLast(i);
seen.add(i);
}
}
}
// ensure Object is last in list (if present)
boolean hasObject = resList.remove(Object.class);
if (hasObject) {
resList.add(Object.class);
}
return resList.toArray(new Class[resList.size()]);
}
/**
* Finds (bean) properties of {@code o} which have both getter and setter
* methods.
*
* @param o
* An arbitrary object.
* @return An array containing a {@link PropertyDescriptor} for each
* property of {@code o}.
* @see #findWritableProperties(Class)
*/
public static PropertyDescriptor[] findWritableProperties(Object o) {
return findWritableProperties(o.getClass());
}
/**
* Finds (bean) properties of {@code c} which have both getter and setter
* methods. If an {@link IntrospectionException} is raised during when
* executing the method, then this exception is raised again as an unchecked
* exception (wrapped in a {@link RuntimeException}).
*
* @param c
* An arbitrary class.
* @return An array containing a {@link PropertyDescriptor} for each
* property of {@code c}.
*/
public static PropertyDescriptor[] findWritableProperties(Class c) {
try {
BeanInfo bi = Introspector.getBeanInfo(c);
PropertyDescriptor[] pds = bi.getPropertyDescriptors();
ArrayList list = new ArrayList(
pds.length);
for (PropertyDescriptor pd : pds) {
if (pd.getWriteMethod() != null && pd.getReadMethod() != null)
list.add(pd);
}
return list.toArray(new PropertyDescriptor[list.size()]);
} catch (IntrospectionException e) {
throw new RuntimeException(e);
}
}
/**
* Utility method to return a {@code PropertyDescriptor}. Name matching is
* case in-sensitive.
*
* @param o
* The object for which to get the property.
* @param propName
* Name of the bean property.
* @return A {@code PropertyDescriptor} matching {@code propName}, otherwise
* {@code null}.
*/
private static PropertyDescriptor getPropertyDescriptor(Object o,
String propName) {
Map props = writableProperties(o.getClass());
PropertyDescriptor desc = props.get(propName
.toLowerCase(Util.DEF_LOCALE));
return desc;
}
/**
* Attempts trivial type conversion. This methods supports all casting
* conversions (JLS 5.5) and always returns null when the input object is
* null. If the target type is {@link String}, the result is the return
* value of the input object's {@link Object#toString()} method. Any object
* can be converted to {@link Integer}, {@link Double} and {@link Boolean},
* but those conversions can throw exceptions.
*
* @param o
* the object to be converted
* @param klass
* the target type
* @param
* Type of returned object.
* @return the converted object
* @throws TypeConversionException
* if the conversion is not supported
* @throws NumberFormatException
* if the input object is not assignable to {@link Number} and
* the return value of its {@link Object#toString()} method
* can't be converted to the numeric target type
*/
@SuppressWarnings("unchecked")
private static T basicConversions(Object o, Class klass)
throws TypeConversionException, NumberFormatException {
if (o == null)
return null;
assert !(o instanceof String);
if (klass.isAssignableFrom(o.getClass())) {
return (T) o;
}
if (klass == String.class) {
return (T) convertToString(o);
}
if (klass == int.class || klass == Integer.class) {
if (o instanceof Number)
return (T) (Integer) ((Number) o).intValue();
return (T) Integer.valueOf(convertToString(o));
}
if (klass == long.class || klass == Long.class) {
if (o instanceof Number)
return (T) (Long) ((Number) o).longValue();
return (T) Long.valueOf(convertToString(o));
}
if (klass == double.class || klass == Double.class) {
if (o instanceof Number)
return (T) (Double) ((Number) o).doubleValue();
return (T) Double.valueOf(convertToString(o));
}
if (klass == boolean.class || klass == Boolean.class) {
String str = convertToString(o);
if (str.equalsIgnoreCase("true") || str.equalsIgnoreCase("yes")
|| str.equalsIgnoreCase("1"))
return (T) Boolean.TRUE;
if (str.equalsIgnoreCase("false") || str.equalsIgnoreCase("no")
|| str.equalsIgnoreCase("0"))
return (T) Boolean.FALSE;
throw new TypeConversionException(String.format(Util.DEF_LOCALE,
"Can't convert '%s' to bool.", o));
}
if (klass.isEnum()) {
return (T) Enum.valueOf(klass.asSubclass(Enum.class),
convertToString(o));
}
if (klass == byte.class || klass == Byte.class) {
if (o instanceof Number)
return (T) (Byte) ((Number) o).byteValue();
return (T) Byte.valueOf(convertToString(o));
}
if (klass == short.class || klass == Short.class) {
if (o instanceof Number)
return (T) (Short) ((Number) o).shortValue();
return (T) Short.valueOf(convertToString(o));
}
if (klass == float.class || klass == Float.class) {
if (o instanceof Number)
return (T) (Float) ((Number) o).floatValue();
return (T) Float.valueOf(convertToString(o));
}
if (klass == char.class || klass == Character.class) {
if (o instanceof Character)
return (T) (Character) o;
String s = convertToString(o);
if (s.length() == 1)
return (T) new Character(s.charAt(0));
}
throw new TypeConversionException(String.format(Util.DEF_LOCALE,
"Can't convert from '%s' to '%s'.", o.getClass().getName(),
klass.getName()));
}
private static WeakHashMap, Map> propCache = null;
/**
* Returns a map of property descriptors. Keys in this map are the property
* names converted to lower case.
*
* @param c
* The class for which to find the properties.
* @return A map associating a property name (converted to lower case) with
* a {@link PropertyDescriptor}.
*/
public static Map writableProperties(Class c) {
if (propCache == null)
propCache = new WeakHashMap<>();
Map beanProps = propCache.get(c);
if (beanProps == null) {
PropertyDescriptor[] props = findWritableProperties(c);
beanProps = new HashMap<>();
for (PropertyDescriptor p : props) {
beanProps.put(p.getName().toLowerCase(Util.DEF_LOCALE), p);
}
propCache.put(c, beanProps);
}
return beanProps;
}
/**
* This method returns a clone of an object, if this object is cloneable.
* The clone is created by calling clone()
using Java
* reflection, therefore clone()
not necessarily has to be
* public.
*
* @param o
* The object to be cloned.
* @param
* Type of returned object.
* @return A clone of {@code o} if it was {@link Cloneable}, or otherwise
* the original object.
*/
@SuppressWarnings("unchecked")
public static T cloneIfPossible(T o) {
if (o == null)
return null;
// array or normal class?
if (o.getClass().getComponentType() == null) {
// normal class
o = callClone(o);
} else {
o = (T) deepCloneArrayIfPossible((Object[]) o);
}
return o;
}
@SuppressWarnings("unchecked")
private static T callClone(T o) {
// o or an array's components are clonable
try {
Method cloneMethod = o.getClass()
.getMethod("clone", new Class[] {});
return (T) cloneMethod.invoke(o);
} catch (NoSuchMethodException ignore) {
// not cloneable, or no public clone-method, return "o" as is
return o;
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
/**
* Produces a deep clone of {@code array}, i.e., for each element of
* {@code array} creating a clone is attempted using
* {@link #cloneIfPossible(Object)}.
*
* @param array
* The array to be cloned.
* @param
* Component type of the array.
* @return A clone of {@code array} with each element also cloned.
*/
public static T[] deepCloneArrayIfPossible(T[] array) {
if (array == null)
return null;
T[] clone = array.clone();
for (int i = 0; i < clone.length; i++) {
clone[i] = cloneIfPossible(array[i]);
}
return clone;
}
private static final String PROP_JASIMA_EXPERIMENT = "jasima.experiment";
private static final String PROP_SUN_JAVA_COMMAND = "sun.java.command";
/**
* Tries to find the main class of a java run. This is attempted by looking
* up the system properties {@code jasima.experiment} and
* {@code sun.java.command} first. If this does not lead to a valid
* classname (e.g., if started with "-jar" option) an attempt is made to
* interpret the property as the name of a jar file. The manifest of this
* jar is then searched for its entry {@code Main-Class}.
*
* This code is necessary because Java has no virtual static methods and
* therefore there is no equivalent to the keyword {@code this} in a static
* method.
*
* @throws ClassNotFoundException
* If there were problems locating the main class. Should not
* occur.
*/
public static Class getMainClass() throws ClassNotFoundException {
Properties props = System.getProperties();
String main = (String) findEntryCaseInsensitive(props,
PROP_JASIMA_EXPERIMENT);
if (main == null) {
main = (String) findEntryCaseInsensitive(props,
PROP_SUN_JAVA_COMMAND);
}
if (main == null) {
throw new RuntimeException(String.format(Util.DEF_LOCALE,
"Couldn't find properties '%s' or '%s'.",
PROP_SUN_JAVA_COMMAND, PROP_JASIMA_EXPERIMENT));
}
// strip any arguments, if present
String classOrJar;
try (Scanner s = new Scanner(main)) {
classOrJar = s.next();
}
try {
// try to find as class directly
Class klazz = TypeUtil.class.getClassLoader().loadClass(
classOrJar);
return klazz;
} catch (ClassNotFoundException e) {
// try to interpret as jar and load main class name from manifest.mf
try {
return loadFromJar(classOrJar);
} catch (IOException ignore) {
// re-throw e;
throw e;
}
}
}
private static Class loadFromJar(String classOrJar) throws IOException,
ClassNotFoundException {
try (JarFile jar = new JarFile(classOrJar)) {
Map jarEntries = jar.getManifest().getEntries();
// is app using "jar in jar" export from eclipse? in this case
// main-class is JarRsrcLoader
Attributes o = (Attributes) findEntryCaseInsensitive(jarEntries,
"Rsrc-Main-Class");
if (o == null || o.size() == 0) {
// regular main class
o = (Attributes) findEntryCaseInsensitive(jarEntries,
"Main-Class");
}
assert o.size() == 1;
// get first entry from 'o'
String cName = (String) o.values().iterator().next();
// try to load cName
Class klazz = Util.class.getClassLoader().loadClass(cName);
return klazz;
}
}
private static Object findEntryCaseInsensitive(Map jarEntries,
String entry) {
entry = entry.toLowerCase();
for (Object o : jarEntries.keySet()) {
String s = ((String) o).toLowerCase();
if (entry.equals(s)) {
return jarEntries.get(o);
}
}
return null;
}
}