All Downloads are FREE. Search and download functionalities are using the official Maven repository.

aQute.lib.converter.Converter Maven / Gradle / Ivy

Go to download

A main program (executable JAR) that will listen to port 29998. At first, it can only answer that it is an Envoy (a limited agent). The only function it supports is installing a -runpath. It will then create a framework + agent and transfer the connection to the just installed agent who will then install the bundles. This JAR is a main command for JPM called bndremote. In JPM, it will start up with debug enabled. This JAR does some highly complicated class loading wizardy to ensure that it does not enforce any constraints on the -runpath.

The newest version!
package aQute.lib.converter;

import static java.lang.invoke.MethodHandles.publicLookup;
import static java.lang.invoke.MethodType.methodType;

import java.io.File;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.RecordComponent;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Stack;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.UUID;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import aQute.lib.base64.Base64;
import aQute.lib.fileset.FileSet;
import aQute.lib.io.IO;

/**
 * General Java type converter from an object to any type. Supports number
 * conversion
 */
@SuppressWarnings({
	"unchecked", "rawtypes"
})
public class Converter {

	public interface Hook {
		Object convert(Type dest, Object o) throws Exception;
	}

	boolean			fatal	= true;
	Map	hooks;
	List		allHooks;
	File			base	= IO.work;

	public  T convert(Class type, Object o) throws Exception {
		// Is it a compatible type?
		if (o != null && type.isAssignableFrom(o.getClass()))
			return (T) o;
		return (T) convertT(type, o);
	}

	public  T convert(TypeReference type, Object o) throws Exception {
		return (T) convert(type.getType(), o);
	}

	public Object convert(Type type, Object o) throws Exception {
		return convertT(type, o);
	}

	Object convertT(Type type, Object o) throws Exception {
		Class resultType = getRawClass(type);

		if (resultType == Optional.class) {
			if (o == null)
				return Optional.empty();

			Object oo = convert(((ParameterizedType) type).getActualTypeArguments()[0], o);
			return Optional.ofNullable(oo);
		}

		if (o == null) {
			if (resultType.isPrimitive()) {

				if (resultType == void.class)
					return null;

				if (resultType == boolean.class)
					return false;
				if (resultType == char.class)
					return '\u0000';
				return convert(type, 0);
			}

			return null; // compatible with any
		}

		if (allHooks != null) {
			for (Hook hook : allHooks) {
				Object r = hook.convert(type, o);
				if (r != null)
					return r;
			}
		}

		if (hooks != null) {
			Hook hook = hooks.get(type);
			if (hook != null) {
				Object value = hook.convert(type, o);
				if (value != null)
					return value;
			}
		}

		Class actualType = o.getClass();

		// We can always make a string

		if (resultType == String.class) {
			if (actualType.isArray()) {
				if (actualType == char[].class)
					return new String((char[]) o);
				if (actualType == byte[].class)
					return Base64.encodeBase64((byte[]) o);
				int l = Array.getLength(o);
				StringBuilder sb = new StringBuilder("[");
				String del = "";
				for (int i = 0; i < l; i++) {
					sb.append(del);
					del = ",";
					sb.append(convert(String.class, Array.get(o, i)));
				}
				sb.append("]");
				return sb.toString();
			}
			return o.toString();
		}

		// or make a UUID
		if (resultType == UUID.class) {
			return UUID.fromString(o.toString());
		}

		//
		// In case we have a Dictionary that is not also a map
		// this is kind of opportune in OSGi because of the silly
		// dictionaries we're still having
		//

		if (o instanceof Dictionary dict && !(o instanceof Map)) {
			Map map = new HashMap<>();
			Enumeration e = dict.keys();
			while (e.hasMoreElements()) {
				Object k = e.nextElement();
				Object v = dict.get(k);
				map.put(k, v);
			}
			o = map;
		}

		if (Collection.class.isAssignableFrom(resultType))
			return collection(type, resultType, o);

		if (Map.class.isAssignableFrom(resultType))
			return map(type, resultType, o);

		if (type instanceof GenericArrayType gType) {
			return array(gType.getGenericComponentType(), o);
		}

		if (resultType.isArray()) {
			if (actualType == String.class) {
				String s = (String) o;
				if (byte[].class == resultType)
					return Base64.decodeBase64(s);

				if (char[].class == resultType)
					return s.toCharArray();
			}
			if (byte[].class == resultType) {
				// Sometimes classes implement toByteArray
				try {
					MethodHandle mh = publicLookup().findVirtual(actualType, "toByteArray", methodType(byte[].class));
					return mh.invoke(o);
				} catch (Error e) {
					throw e;
				} catch (Throwable e) {
					// Ignore
				}
			}

			return array(resultType.getComponentType(), o);
		}

		if (resultType.isAssignableFrom(o.getClass()))
			return o;

		if (Map.class.isAssignableFrom(actualType) && resultType.isInterface()) {
			return proxy(resultType, (Map) o);
		}

		if (resultType == File.class && o instanceof String string) {
			return IO.getFile(base, string);
		}

		// Simple type coercion

		if (resultType == boolean.class || resultType == Boolean.class) {
			if (actualType == boolean.class || actualType == Boolean.class)
				return o;
			Number n = number(o);
			if (n != null)
				return n.longValue() == 0 ? false : true;

			resultType = Boolean.class;
		} else if (resultType == byte.class || resultType == Byte.class) {
			Number n = number(o);
			if (n != null)
				return n.byteValue();
			resultType = Byte.class;
		} else if (resultType == char.class || resultType == Character.class) {
			Number n = number(o);
			if (n != null)
				return (char) n.shortValue();
			resultType = Character.class;
		} else if (resultType == short.class || resultType == Short.class) {
			Number n = number(o);
			if (n != null)
				return n.shortValue();

			resultType = Short.class;
		} else if (resultType == int.class || resultType == Integer.class) {
			Number n = number(o);
			if (n != null)
				return n.intValue();

			resultType = Integer.class;
		} else if (resultType == long.class || resultType == Long.class) {
			Number n = number(o);
			if (n != null)
				return n.longValue();

			resultType = Long.class;
		} else if (resultType == float.class || resultType == Float.class) {
			Number n = number(o);
			if (n != null)
				return n.floatValue();

			resultType = Float.class;
		} else if (resultType == double.class || resultType == Double.class) {
			Number n = number(o);
			if (n != null)
				return n.doubleValue();

			resultType = Double.class;
		}

		assert !resultType.isPrimitive();

		if (actualType == String.class) {
			String input = (String) o;
			if (resultType == char[].class)
				return input.toCharArray();

			if (resultType == byte[].class)
				return Base64.decodeBase64(input);

			if (Enum.class.isAssignableFrom(resultType)) {
				try {
					return Enum.valueOf((Class) resultType, input);
				} catch (Exception e) {
					input = input.toUpperCase(Locale.ROOT);
					String input2 = input.replace('_', '.');

					try {
						return Enum.valueOf((Class) resultType, input);
					} catch (Exception ee) {
						Class ec = resultType;

						Enum[] enumConstants = ec.getEnumConstants();
						if (enumConstants != null) {
							for (Enum enm : enumConstants) {
								String s = enm.toString();
								if (s.equalsIgnoreCase(input))
									return enm;
								if (s.equalsIgnoreCase(input2))
									return enm;
							}
							return null;
						}
						return null;
					}
				}
			}
			if (resultType == Pattern.class) {
				return Pattern.compile(input);
			}
			if (resultType == URI.class) {
				return new URI(sanitizeInputForURI(input));
			}
			try {
				MethodHandle mh;
				try {
					mh = publicLookup().findStatic(resultType, "valueOf", methodType(resultType, String.class));
				} catch (NoSuchMethodException | IllegalAccessException e) {
					mh = publicLookup().findConstructor(resultType, methodType(void.class, String.class));
				}
				return mh.invoke(o.toString());
			} catch (Error e) {
				throw e;
			} catch (Throwable t) {}

			if (resultType == Character.class && input.length() == 1)
				return input.charAt(0);
		}
		Number n = number(o);
		if (n != null) {
			if (Enum.class.isAssignableFrom(resultType)) {
				try {
					MethodHandle mh = publicLookup().findStatic(resultType, "values",
						methodType(Array.newInstance(resultType, 0)
							.getClass()));
					Object[] vs = (Object[]) mh.invoke();
					int nn = n.intValue();
					if (nn > 0 && nn < vs.length)
						return vs[nn];
				} catch (Error e) {
					throw e;
				} catch (Throwable e) {
					// Ignore
				}
			}
		}

		// Translate arrays with length 1 by picking the single element
		if (actualType.isArray() && Array.getLength(o) == 1) {
			return convert(type, Array.get(o, 0));
		}

		// Translate collections with size 1 by picking the single element
		if (o instanceof Collection col) {
			if (col.size() == 1)
				return convert(type, col.iterator()
					.next());
		}

		if (o instanceof Map map) {

			String key = null;
			try {
				if (resultType.isRecord()) {
					int length = resultType.getRecordComponents().length;
					Object[] arguments = new Object[length];
					MethodType constructorType = methodType(void.class);
					for (int i = 0; i < length; i++) {
						RecordComponent c = resultType.getRecordComponents()[i];
						Object value = map.get(c.getName());
						arguments[i] = cnv(c.getGenericType(), value);
						constructorType = constructorType.appendParameterTypes(c.getType());
					}
					MethodHandle mh = publicLookup().findConstructor(resultType, constructorType);
					return mh.invokeWithArguments(arguments);
				} else {
					MethodHandle mh = publicLookup().findConstructor(resultType, methodType(void.class));
					Object instance = mh.invoke();
					for (Map.Entry e : map.entrySet()) {
						key = (String) e.getKey();
						try {
							Field f = resultType.getField(key);
							Object value = convert(f.getGenericType(), e.getValue());
							mh = publicLookup().unreflectSetter(f);
							if (isStatic(f)) {
								mh.invoke(value);
							} else {
								mh.invoke(instance, value);
							}
						} catch (Exception ee) {
							// We cannot find the key, so try the __extra field
							mh = publicLookup().findGetter(resultType, "__extra", Map.class);
							Map extra = (Map) mh.invoke(instance);
							if (extra == null) {
								extra = new HashMap<>();
								mh = publicLookup().findSetter(resultType, "__extra", Map.class);
								mh.invoke(instance, extra);
							}
							extra.put(key, convert(Object.class, e.getValue()));
						}
					}
					return instance;
				}
			} catch (Error e) {
				throw e;
			} catch (Throwable e) {
				return error(
					"No conversion found for " + o.getClass() + " to " + type + ", error " + e + " on key " + key);
			}
		}

		return error("No conversion found for " + o.getClass() + " to " + type);
	}

	private String sanitizeInputForURI(String input) {
		int newline = input.indexOf("\n");
		if (newline > -1)
			return input.substring(0, newline)
				.trim();
		return input;
	}

	private Number number(Object o) {
		if (o instanceof Number n)
			return n;

		if (o instanceof Boolean b)
			return b.booleanValue() ? 1 : 0;

		if (o instanceof Character c)
			return (int) c.charValue();

		if (o instanceof String s) {
			try {
				return Double.parseDouble(s);
			} catch (Exception e) {
				// Ignore
			}
		}
		return null;
	}

	private Collection collection(Type collectionType, Class rawClass, Object o)
		throws Exception {
		Collection collection;
		if (rawClass.isInterface() || Modifier.isAbstract(rawClass.getModifiers())) {
			if (rawClass.isAssignableFrom(ArrayList.class))
				collection = new ArrayList();
			else if (rawClass.isAssignableFrom(HashSet.class))
				collection = new HashSet();
			else if (rawClass.isAssignableFrom(TreeSet.class))
				collection = new TreeSet();
			else if (rawClass.isAssignableFrom(LinkedList.class))
				collection = new LinkedList();
			else if (rawClass.isAssignableFrom(Vector.class))
				collection = new Vector();
			else if (rawClass.isAssignableFrom(Stack.class))
				collection = new Stack();
			else if (rawClass.isAssignableFrom(ConcurrentLinkedQueue.class))
				collection = new ConcurrentLinkedQueue();
			else
				return (Collection) error("Cannot find a suitable collection for the collection interface " + rawClass);
		} else {
			collection = newInstance(rawClass);
		}

		Type subType = Object.class;
		if (collectionType instanceof ParameterizedType ptype) {
			subType = ptype.getActualTypeArguments()[0];

			if (subType == File.class && o instanceof String string) {
				FileSet tree = new FileSet(base, string);
				return tree.getFiles();
			}
		}

		Collection input = toCollection(o);

		for (Object i : input)
			collection.add(convert(subType, i));

		return collection;
	}

	private static final MethodType defaultConstructor = methodType(void.class);

	private static  T newInstance(Class rawClass) throws Exception {
		try {
			return (T) publicLookup().findConstructor(rawClass, defaultConstructor)
				.invoke();
		} catch (Error | Exception e) {
			throw e;
		} catch (Throwable e) {
			throw new RuntimeException(e);
		}
	}

	private static boolean isStatic(Member m) {
		return Modifier.isStatic(m.getModifiers());
	}

	private Map map(Type mapType, Class> rawClass, Object o) throws Exception {
		Map result;
		if (rawClass.isInterface() || Modifier.isAbstract(rawClass.getModifiers())) {
			if (rawClass.isAssignableFrom(HashMap.class))
				result = new HashMap();
			else if (rawClass.isAssignableFrom(TreeMap.class))
				result = new TreeMap();
			else if (rawClass.isAssignableFrom(ConcurrentHashMap.class))
				result = new ConcurrentHashMap();
			else {
				return (Map) error("Cannot find suitable map for map interface " + rawClass);
			}
		} else {
			result = newInstance(rawClass);
		}

		Map input = toMap(o);

		Type keyType = Object.class;
		Type valueType = Object.class;
		if (mapType instanceof ParameterizedType ptype) {
			keyType = ptype.getActualTypeArguments()[0];
			valueType = ptype.getActualTypeArguments()[1];
		}

		for (Map.Entry entry : input.entrySet()) {
			Object key = convert(keyType, entry.getKey());
			Object value = convert(valueType, entry.getValue());
			if (key == null)
				error("Key for map must not be null: " + input);
			else
				result.put(key, value);
		}

		return result;
	}

	public Object array(Type type, Object o) throws Exception {

		if (type == File.class && o instanceof String string) {
			FileSet tree = new FileSet(base, string);
			return tree.getFiles()
				.toArray(new File[0]);
		}

		Collection input = toCollection(o);
		Class componentClass = getRawClass(type);
		Object array = Array.newInstance(componentClass, input.size());

		int i = 0;
		for (Object next : input) {
			Array.set(array, i++, convert(type, next));
		}
		return array;
	}

	private Class getRawClass(Type type) {
		if (type instanceof Class ctype)
			return ctype;

		if (type instanceof ParameterizedType ptype)
			return (Class) ptype.getRawType();

		if (type instanceof GenericArrayType gtype) {
			Type componentType = gtype.getGenericComponentType();
			return Array.newInstance(getRawClass(componentType), 0)
				.getClass();
		}

		if (type instanceof TypeVariable ttype) {
			Type componentType = ttype.getBounds()[0];
			return Array.newInstance(getRawClass(componentType), 0)
				.getClass();
		}

		if (type instanceof WildcardType wtype) {
			Type componentType = wtype.getUpperBounds()[0];
			return Array.newInstance(getRawClass(componentType), 0)
				.getClass();
		}

		return Object.class;
	}

	public Collection toCollection(Object o) {
		if (o instanceof Collection c)
			return c;

		if (o.getClass()
			.isArray()) {
			if (o.getClass()
				.getComponentType()
				.isPrimitive()) {
				int length = Array.getLength(o);
				List result = new ArrayList<>(length);
				for (int i = 0; i < length; i++) {
					result.add(Array.get(o, i));
				}
				return result;
			}
			return Arrays.asList((Object[]) o);
		}

		return Arrays.asList(o);
	}

	public Map toMap(Object o) throws Exception {
		if (o instanceof Map m)
			return m;
		Map result = new HashMap<>();
		getFields(o.getClass()).forEach(f -> {
			try {
				MethodHandle mh = publicLookup().unreflectGetter(f);
				result.put(f.getName(), mh.invoke(o));
			} catch (Throwable e) {
				throw new RuntimeException(e);
			}
		});
		if (result.isEmpty()) {
			return null;
		}

		return result;
	}

	private static Stream getFields(Class c) {
		return Stream.of(c.getFields())
			.filter(field -> !(field.isEnumConstant() || field.isSynthetic() || isStatic(field)));
	}

	private Object error(String string) {
		if (fatal)
			throw new IllegalArgumentException(string);
		return null;
	}

	public void setFatalIsException(boolean b) {
		fatal = b;
	}

	public Converter hook(Type type, Hook hook) {
		if (type != null) {
			if (hooks == null)
				hooks = new HashMap<>();
			this.hooks.put(type, hook);
		} else {
			if (allHooks == null)
				allHooks = new ArrayList<>();
			allHooks.add(hook);
		}

		return this;
	}

	/**
	 * Convert a map to an interface.
	 *
	 * @param interfc
	 * @param properties
	 * @return proxy object for map
	 */
	public  T proxy(Class interfc, final Map properties) {
		return (T) Proxy.newProxyInstance(interfc.getClassLoader(), new Class[] {
			interfc
		}, new InvocationHandler() {
			@Override
			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

				if (method.getName()
					.equals("toString") && method.getParameterCount() == 0)
					return properties + "'";

				if (Object.class == method.getDeclaringClass()) {
					MethodHandle mh = publicLookup().unreflect(method)
						.bindTo(this);
					return mh.invokeWithArguments(args);
				}

				Object o = properties.get(method.getName());
				if (o == null)
					o = properties.get(mangleMethodName(method.getName()));

				if (o == null) {
					if (args != null && args.length == 1) {
						o = args[0];
					} else {
						o = method.getDefaultValue();
					}
				}
				return convert(method.getGenericReturnType(), o);
			}
		});
	}

	public static String mangleMethodName(String id) {
		char[] array = id.toCharArray();
		int out = 0;

		boolean changed = false;
		for (int i = 0; i < array.length; i++) {
			if (match("$$", array, i) || match("__", array, i)) {
				array[out++] = array[i++];
				changed = true;
			} else if (match("$_$", array, i)) {
				array[out++] = '-';
				i += 2;
			} else {
				char c = array[i];
				if (c == '_') {
					array[out++] = '.';
					changed = true;
				} else if (c == '$') {
					changed = true;
				} else {
					array[out++] = c;
				}
			}
		}
		if (id.length() != out || changed)
			return new String(array, 0, out);

		return id;
	}

	private static boolean match(String pattern, char[] array, int i) {
		for (int j = 0; j < pattern.length(); j++, i++) {
			if (i >= array.length)
				return false;

			if (pattern.charAt(j) != array[i])
				return false;
		}
		return true;
	}

	public static  T cnv(TypeReference tr, Object source) throws Exception {
		return new Converter().convert(tr, source);
	}

	public static  T cnv(Class tr, Object source) throws Exception {
		return new Converter().convert(tr, source);
	}

	public static Object cnv(Type tr, Object source) throws Exception {
		return new Converter().convert(tr, source);
	}

	/**
	 * Return if the class's instances can hold multiple values.
	 *
	 * @param c the class to test
	 * @return true if the class's instances can hold multiple values
	 */
	public static boolean isMultiple(Class c) {
		if (c.isArray())
			return true;

		if (Collection.class.isAssignableFrom(c))
			return true;

		if (Map.class.isAssignableFrom(c))
			return true;

		return false;
	}

	/**
	 * Return if the class's instances can hold multiple values.
	 *
	 * @param c the class to test
	 * @return true if the class's instances can hold multiple values
	 */
	public static boolean isMultiple(Type c) {
		if (c instanceof Class ctype)
			return isMultiple(ctype);

		if (c instanceof ParameterizedType ptype) {
			Type rawType = ptype.getRawType();
			if (rawType instanceof Class ctype)
				return isMultiple(ctype);
		}

		return false;
	}

	public void setBase(File base) {
		this.base = base;
	}
}