package eu.fbk.knowledgestore.runtime; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nullable; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import com.google.common.reflect.TypeToken; import com.thoughtworks.paranamer.AdaptiveParanamer; import com.thoughtworks.paranamer.BytecodeReadingParanamer; import com.thoughtworks.paranamer.CachingParanamer; import com.thoughtworks.paranamer.DefaultParanamer; import com.thoughtworks.paranamer.Paranamer; import org.openrdf.model.Literal; import org.openrdf.model.Resource; import org.openrdf.model.Statement; import org.openrdf.model.URI; import org.openrdf.model.Value; import org.openrdf.model.vocabulary.RDF; import eu.fbk.knowledgestore.data.Data; import eu.fbk.knowledgestore.data.Record; import eu.fbk.knowledgestore.data.XPath; public final class Factory { private static final String SCHEME = "java:"; private static final Paranamer PARANAMER = new CachingParanamer(new AdaptiveParanamer( new DefaultParanamer(), new BytecodeReadingParanamer())); private static final Pattern PLACEHOLDER = Pattern.compile("\\$\\{[^\\}]+\\}"); public static Map<URI, Object> instantiate(final Iterable<? extends Statement> model, final URI... ids) { final Map<Resource, URI> types = Maps.newLinkedHashMap(); final Multimap<Resource, Statement> stmt = ArrayListMultimap.create(); for (final Statement statement : model) { final Resource s = statement.getSubject(); final URI p = statement.getPredicate(); final Value o = statement.getObject(); if (p.equals(RDF.TYPE) && o instanceof URI && o.stringValue().startsWith(SCHEME)) { types.put(s, (URI) o); } else if (p.stringValue().startsWith(SCHEME)) { stmt.put(s, statement); } } if (ids != null && ids.length > 0) { final Set<Resource> subjs = Sets.<Resource>newHashSet(ids); int size; do { size = subjs.size(); for (final Statement statement : stmt.values()) { final Value o = statement.getObject(); if (o instanceof Resource) { subjs.add((Resource) o); } } } while (subjs.size() > size); types.keySet().retainAll(subjs); } final Map<Resource, Object> map = Maps.newHashMap(); while (!types.isEmpty()) { final int size = types.size(); for (final Resource s : Lists.newArrayList(types.keySet())) { final Collection<Statement> statements = stmt.get(s); boolean dependent = false; for (final Statement statement : statements) { final Value o = statement.getObject(); if (o instanceof Resource && types.keySet().contains(o)) { dependent = true; break; } } if (!dependent) { final URI implementation = types.get(s); final Multimap<String, Object> properties = ArrayListMultimap.create(); for (final Statement statement : statements) { final URI p = statement.getPredicate(); final Value o = statement.getObject(); final Object obj = map.get(o); properties.put(p.stringValue().substring(SCHEME.length()), obj != null ? obj : o); } Preconditions.checkArgument(implementation != null, "No implementation specified for %s", s); map.put(s, instantiate(properties.asMap(), implementation, Object.class)); types.remove(s); } } Preconditions.checkArgument(types.size() < size, "Cannot instantiate " + stmt.keySet() + " - detected circular dependencies"); } final ImmutableMap.Builder<URI, Object> builder = ImmutableMap.builder(); for (final Map.Entry<Resource, Object> entry : map.entrySet()) { final Resource s = entry.getKey(); final Object obj = entry.getValue(); if (s instanceof URI && (ids == null || ids.length == 0 || Arrays.asList(ids).contains(s))) { builder.put((URI) s, obj); } } return builder.build(); } public static <T> T instantiate(final Iterable<? extends Statement> model, final URI id, final Class<T> type) { return type.cast(instantiate(model, id).get(id)); } public static <T> T instantiate(final Map<String, ? extends Object> properties, final URI implementation, final Class<T> type) { final String uriString = implementation.stringValue(); Preconditions.checkArgument(uriString.startsWith("java:")); final int index = uriString.indexOf("#"); final String className = uriString.substring(5, index > 0 ? index : uriString.length()); final String methodName = index < 0 ? null : uriString.substring(index + 1); final Class<?> clazz; try { clazz = Class.forName(className); } catch (final ClassNotFoundException ex) { throw new IllegalArgumentException("No class for " + implementation); } // Transform input property names to lower case final Map<String, Object> props = Maps.newLinkedHashMap(); for (final Map.Entry<String, ? extends Object> entry : properties.entrySet()) { props.put(entry.getKey().toLowerCase(), entry.getValue()); } if (clazz == Record.class) { Preconditions.checkArgument(type.isAssignableFrom(Record.class)); final Record record = Record.create(); for (final Map.Entry<String, ? extends Object> entry : properties.entrySet()) { record.set(Data.getValueFactory().createURI("java:" + entry.getKey()), entry.getValue()); } return type.cast(record); } final Map<String, Method> setters = Maps.newHashMap(); for (final Method m : clazz.getMethods()) { final String name = m.getName(); if (!Modifier.isStatic(m.getModifiers()) && m.getParameterTypes().length == 1 && name.startsWith("set")) { setters.put(name.substring(3).toLowerCase(), m); } } final Set<String> constructionProps = Sets.newHashSet(props.keySet()); constructionProps.removeAll(setters.keySet()); if (methodName == null) { for (final Constructor<?> c : clazz.getConstructors()) { final Set<String> args = Sets.newHashSet(signature(c)); boolean acceptMap = false; for (int i = 0; i < c.getParameterTypes().length; ++i) { acceptMap = acceptMap || c.getParameterTypes()[i].isAssignableFrom(Map.class) || c.getParameterTypes()[i].isAssignableFrom(Properties.class); } if (args.containsAll(constructionProps) || acceptMap) { return type.cast(callSetters(callConstructor(c, props), setters, props)); } } throw new IllegalArgumentException("No suitable constructor for " + implementation + " supporting properties " + Joiner.on(", ").join(props.keySet())); } for (final Method m : clazz.getMethods()) { if (!m.getName().equals(methodName) || !Modifier.isStatic(m.getModifiers())) { continue; } final Set<String> args = Sets.newHashSet(signature(m)); if (methodName.equals("builder")) { final Class<?> builderClazz = m.getReturnType(); Method build = null; final Map<String, Method> builderSetters = Maps.newHashMap(); for (final Method setter : builderClazz.getMethods()) { String name = setter.getName(); if (name.equals("build")) { build = setter; } if (!Modifier.isStatic(setter.getModifiers()) && setter.getReturnType() == builderClazz && setter.getParameterTypes().length == 1) { if (name.startsWith("set")) { name = name.substring(3); } else if (name.startsWith("with")) { name = name.substring(4); } builderSetters.put(name.toLowerCase(), setter); } } args.addAll(builderSetters.keySet()); args.addAll(Arrays.asList(signature(build))); if (build != null && args.containsAll(props.keySet())) { return type.cast(callSetters(callBuilder(m, builderSetters, build, props), setters, props)); } } else if (args.containsAll(props.keySet())) { return type.cast(callSetters(callMethod(m, null, props), setters, props)); } } throw new IllegalArgumentException("No suitable '" + methodName + "' method for " + implementation + " supporting properties " + Joiner.on(", ").join(props.keySet())); } private static String[] signature(final AccessibleObject member) { String[] names = PARANAMER.lookupParameterNames(member, false); if (names != null) { names = names.clone(); for (int i = 0; i < names.length; ++i) { names[i] = names[i].trim().toLowerCase(); } } else if (member instanceof Constructor) { names = new String[((Constructor<?>) member).getParameterTypes().length]; } else if (member instanceof Method) { names = new String[((Method) member).getParameterTypes().length]; } else { throw new Error("Unexpected member " + member); } return names; } private static Object callConstructor(final Constructor<?> constructor, final Map<String, Object> properties) { // Prepare the argument list final Type[] argTypes = constructor.getGenericParameterTypes(); final String[] argNames = signature(constructor); final Object[] argValues = convertArgs(properties, argNames, argTypes); try { // Invoke the constructor, reporting detailed error information on failure return constructor.newInstance(argValues); } catch (final Throwable ex) { throw new RuntimeException("Invocation of constructor '" + constructor + "' with parameters " + Arrays.asList(argValues) + " failed", ex); } } private static Object callMethod(final Method method, final Object object, final Map<String, Object> properties) { // Prepare the argument list final Type[] argTypes = method.getGenericParameterTypes(); final String[] argNames = signature(method); final Object[] argValues = convertArgs(properties, argNames, argTypes); try { // Invoke the method, reporting detailed error information on failure return method.invoke(object, argValues); } catch (final Throwable ex) { throw new RuntimeException("Invocation of method '" + method + "' on object " + object + " with parameters " + Arrays.asList(argValues) + " failed", ex); } } private static Object callBuilder(final Method builder, final Map<String, Method> setters, final Method build, final Map<String, Object> properties) { // Instantiate the builder object final Object obj = callMethod(builder, null, properties); // Set properties on the builder callSetters(obj, setters, properties); // Instantiate the object return callMethod(build, obj, properties); } private static Object callSetters(final Object object, final Map<String, Method> setters, final Map<String, Object> properties) { for (final Map.Entry<String, Method> entry : setters.entrySet()) { final String name = entry.getKey(); final Method setter = entry.getValue(); final Object value = properties.get(name); if (value != null || properties.containsKey(name)) { callMethod(setter, object, ImmutableMap.of(name, value)); } } return object; // for call chaining } @SuppressWarnings({ "unchecked", "rawtypes" }) private static Object[] convertArgs(final Map<String, Object> properties, final String[] names, final Type[] types) { assert names.length == types.length; final int length = names.length; final Object[] result = new Object[length]; for (int i = 0; i < length; ++i) { final String name = names[i]; final Type type = types[i]; final TypeToken token = TypeToken.of(type); if (token.getRawType().isAssignableFrom(Map.class)) { final Type valueType = ((ParameterizedType) token.getSupertype(Map.class) .getType()).getActualTypeArguments()[1]; final Map<String, Object> map = Maps.newHashMapWithExpectedSize(properties.size()); for (final Map.Entry<String, Object> entry : properties.entrySet()) { map.put(entry.getKey(), convertMany(entry.getValue(), valueType)); } result[i] = map; } else if (token.getRawType().isAssignableFrom(Properties.class)) { final Properties props = new Properties(); for (final Map.Entry<String, Object> entry : properties.entrySet()) { try { props.setProperty(entry.getKey(), (String) convertMany(entry.getValue(), String.class)); } catch (final Throwable ex) { // ignore conversion error } } result[i] = props; } else { result[i] = convertMany(properties.get(name), type); } } return result; } @SuppressWarnings({ "rawtypes", "unchecked" }) private static Object convertMany(@Nullable final Object value, final Type type) { final TypeToken token = TypeToken.of(type); final Class<?> clazz = token.getRawType(); Type elementType = type; if (Iterable.class.isAssignableFrom(clazz)) { elementType = ((ParameterizedType) token.getSupertype(Iterable.class).getType()) .getActualTypeArguments()[0]; } else if (clazz.isArray()) { elementType = token.getComponentType().getType(); } final Class<?> elementClass = TypeToken.of(elementType).getRawType(); final List<Object> values = Lists.newArrayList(); if (value != null) { if (value instanceof Iterable<?>) { for (final Object v : (Iterable<?>) value) { values.add(convertSingle(expand(v), elementClass)); } } else if (value.getClass().isArray()) { final int length = Array.getLength(value); for (int i = 0; i < length; ++i) { values.add(convertSingle(expand(Array.get(value, i)), elementClass)); } } else { values.add(convertSingle(expand(value), elementClass)); } } if (clazz.isAssignableFrom(List.class)) { return values; } else if (clazz.isAssignableFrom(Set.class)) { return Sets.newHashSet(values); } else if (clazz.isArray()) { return Iterables.toArray(values, (Class<Object>) clazz.getComponentType()); } else if (!values.isEmpty()) { return values.get(0); } else if (clazz == long.class) { return 0L; } else if (clazz == int.class) { return 0; } else if (clazz == short.class) { return (short) 0; } else if (elementClass == byte.class) { return (byte) 0; } else if (elementClass == char.class) { return (char) 0; } else if (elementClass == boolean.class) { return false; } return null; } private static Object convertSingle(final Object object, final Class<?> type) { if (type == XPath.class) { return object instanceof XPath || object == null ? (XPath) object : // XPath.parse((String) convertSingle(object, String.class)); } else { return Data.convert(object, type); } } private static Object expand(final Object object) { String string; if (object instanceof String) { string = (String) object; } else if (object instanceof Literal) { string = ((Literal) object).getLabel(); } else { return object; } final StringBuilder builder = new StringBuilder(); int index = 0; final Matcher matcher = PLACEHOLDER.matcher(string); while (matcher.find()) { builder.append(string.substring(index, matcher.start())); final String property = string.substring(matcher.start() + 2, matcher.end() - 1); String value = System.getProperty(property); if (value != null) { if (property.equals("user.dir") || property.equals("user.home") || property.equals("java.home") || property.equals("java.io.tmpdir") || property.equals("java.ext.dirs")) { value = value.replace('\\', '/'); } builder.append(value); } index = matcher.end(); } builder.append(string.substring(index)); final String expanded = builder.toString(); if (object instanceof String) { return expanded; } else { final Literal l = (Literal) object; final URI dt = l.getDatatype(); final String lang = l.getLanguage(); return dt != null ? Data.getValueFactory().createLiteral(expanded, dt) // : Data.getValueFactory().createLiteral(expanded, lang); } } }