1   package eu.fbk.knowledgestore.runtime;
2   
3   import java.lang.reflect.AccessibleObject;
4   import java.lang.reflect.Array;
5   import java.lang.reflect.Constructor;
6   import java.lang.reflect.Method;
7   import java.lang.reflect.Modifier;
8   import java.lang.reflect.ParameterizedType;
9   import java.lang.reflect.Type;
10  import java.util.Arrays;
11  import java.util.Collection;
12  import java.util.List;
13  import java.util.Map;
14  import java.util.Properties;
15  import java.util.Set;
16  import java.util.regex.Matcher;
17  import java.util.regex.Pattern;
18  
19  import javax.annotation.Nullable;
20  
21  import com.google.common.base.Joiner;
22  import com.google.common.base.Preconditions;
23  import com.google.common.collect.ArrayListMultimap;
24  import com.google.common.collect.ImmutableMap;
25  import com.google.common.collect.Iterables;
26  import com.google.common.collect.Lists;
27  import com.google.common.collect.Maps;
28  import com.google.common.collect.Multimap;
29  import com.google.common.collect.Sets;
30  import com.google.common.reflect.TypeToken;
31  import com.thoughtworks.paranamer.AdaptiveParanamer;
32  import com.thoughtworks.paranamer.BytecodeReadingParanamer;
33  import com.thoughtworks.paranamer.CachingParanamer;
34  import com.thoughtworks.paranamer.DefaultParanamer;
35  import com.thoughtworks.paranamer.Paranamer;
36  
37  import org.openrdf.model.Literal;
38  import org.openrdf.model.Resource;
39  import org.openrdf.model.Statement;
40  import org.openrdf.model.URI;
41  import org.openrdf.model.Value;
42  import org.openrdf.model.vocabulary.RDF;
43  
44  import eu.fbk.knowledgestore.data.Data;
45  import eu.fbk.knowledgestore.data.Record;
46  import eu.fbk.knowledgestore.data.XPath;
47  
48  public final class Factory {
49  
50      private static final String SCHEME = "java:";
51  
52      private static final Paranamer PARANAMER = new CachingParanamer(new AdaptiveParanamer(
53              new DefaultParanamer(), new BytecodeReadingParanamer()));
54  
55      private static final Pattern PLACEHOLDER = Pattern.compile("\\$\\{[^\\}]+\\}");
56  
57      public static Map<URI, Object> instantiate(final Iterable<? extends Statement> model,
58              final URI... ids) {
59  
60          final Map<Resource, URI> types = Maps.newLinkedHashMap();
61          final Multimap<Resource, Statement> stmt = ArrayListMultimap.create();
62          for (final Statement statement : model) {
63              final Resource s = statement.getSubject();
64              final URI p = statement.getPredicate();
65              final Value o = statement.getObject();
66              if (p.equals(RDF.TYPE) && o instanceof URI && o.stringValue().startsWith(SCHEME)) {
67                  types.put(s, (URI) o);
68              } else if (p.stringValue().startsWith(SCHEME)) {
69                  stmt.put(s, statement);
70              }
71          }
72  
73          if (ids != null && ids.length > 0) {
74              final Set<Resource> subjs = Sets.<Resource>newHashSet(ids);
75              int size;
76              do {
77                  size = subjs.size();
78                  for (final Statement statement : stmt.values()) {
79                      final Value o = statement.getObject();
80                      if (o instanceof Resource) {
81                          subjs.add((Resource) o);
82                      }
83                  }
84              } while (subjs.size() > size);
85              types.keySet().retainAll(subjs);
86          }
87  
88          final Map<Resource, Object> map = Maps.newHashMap();
89          while (!types.isEmpty()) {
90              final int size = types.size();
91              for (final Resource s : Lists.newArrayList(types.keySet())) {
92                  final Collection<Statement> statements = stmt.get(s);
93                  boolean dependent = false;
94                  for (final Statement statement : statements) {
95                      final Value o = statement.getObject();
96                      if (o instanceof Resource && types.keySet().contains(o)) {
97                          dependent = true;
98                          break;
99                      }
100                 }
101                 if (!dependent) {
102                     final URI implementation = types.get(s);
103                     final Multimap<String, Object> properties = ArrayListMultimap.create();
104                     for (final Statement statement : statements) {
105                         final URI p = statement.getPredicate();
106                         final Value o = statement.getObject();
107                         final Object obj = map.get(o);
108                         properties.put(p.stringValue().substring(SCHEME.length()),
109                                 obj != null ? obj : o);
110                     }
111                     Preconditions.checkArgument(implementation != null,
112                             "No implementation specified for %s", s);
113                     map.put(s, instantiate(properties.asMap(), implementation, Object.class));
114                     types.remove(s);
115                 }
116             }
117             Preconditions.checkArgument(types.size() < size, "Cannot instantiate " + stmt.keySet()
118                     + " - detected circular dependencies");
119         }
120 
121         final ImmutableMap.Builder<URI, Object> builder = ImmutableMap.builder();
122         for (final Map.Entry<Resource, Object> entry : map.entrySet()) {
123             final Resource s = entry.getKey();
124             final Object obj = entry.getValue();
125             if (s instanceof URI
126                     && (ids == null || ids.length == 0 || Arrays.asList(ids).contains(s))) {
127                 builder.put((URI) s, obj);
128             }
129         }
130         return builder.build();
131     }
132 
133     public static <T> T instantiate(final Iterable<? extends Statement> model, final URI id,
134             final Class<T> type) {
135         return type.cast(instantiate(model, id).get(id));
136     }
137 
138     public static <T> T instantiate(final Map<String, ? extends Object> properties,
139             final URI implementation, final Class<T> type) {
140 
141         final String uriString = implementation.stringValue();
142         Preconditions.checkArgument(uriString.startsWith("java:"));
143 
144         final int index = uriString.indexOf("#");
145         final String className = uriString.substring(5, index > 0 ? index : uriString.length());
146         final String methodName = index < 0 ? null : uriString.substring(index + 1);
147 
148         final Class<?> clazz;
149         try {
150             clazz = Class.forName(className);
151         } catch (final ClassNotFoundException ex) {
152             throw new IllegalArgumentException("No class for " + implementation);
153         }
154 
155         // Transform input property names to lower case
156         final Map<String, Object> props = Maps.newLinkedHashMap();
157         for (final Map.Entry<String, ? extends Object> entry : properties.entrySet()) {
158             props.put(entry.getKey().toLowerCase(), entry.getValue());
159         }
160 
161         if (clazz == Record.class) {
162             Preconditions.checkArgument(type.isAssignableFrom(Record.class));
163             final Record record = Record.create();
164             for (final Map.Entry<String, ? extends Object> entry : properties.entrySet()) {
165                 record.set(Data.getValueFactory().createURI("java:" + entry.getKey()),
166                         entry.getValue());
167             }
168             return type.cast(record);
169         }
170 
171         final Map<String, Method> setters = Maps.newHashMap();
172         for (final Method m : clazz.getMethods()) {
173             final String name = m.getName();
174             if (!Modifier.isStatic(m.getModifiers()) && m.getParameterTypes().length == 1
175                     && name.startsWith("set")) {
176                 setters.put(name.substring(3).toLowerCase(), m);
177             }
178         }
179 
180         final Set<String> constructionProps = Sets.newHashSet(props.keySet());
181         constructionProps.removeAll(setters.keySet());
182 
183         if (methodName == null) {
184             for (final Constructor<?> c : clazz.getConstructors()) {
185                 final Set<String> args = Sets.newHashSet(signature(c));
186                 boolean acceptMap = false;
187                 for (int i = 0; i < c.getParameterTypes().length; ++i) {
188                     acceptMap = acceptMap || c.getParameterTypes()[i].isAssignableFrom(Map.class)
189                             || c.getParameterTypes()[i].isAssignableFrom(Properties.class);
190                 }
191                 if (args.containsAll(constructionProps) || acceptMap) {
192                     return type.cast(callSetters(callConstructor(c, props), setters, props));
193                 }
194             }
195             throw new IllegalArgumentException("No suitable constructor for " + implementation
196                     + " supporting properties " + Joiner.on(", ").join(props.keySet()));
197         }
198 
199         for (final Method m : clazz.getMethods()) {
200             if (!m.getName().equals(methodName) || !Modifier.isStatic(m.getModifiers())) {
201                 continue;
202             }
203             final Set<String> args = Sets.newHashSet(signature(m));
204             if (methodName.equals("builder")) {
205                 final Class<?> builderClazz = m.getReturnType();
206                 Method build = null;
207                 final Map<String, Method> builderSetters = Maps.newHashMap();
208                 for (final Method setter : builderClazz.getMethods()) {
209                     String name = setter.getName();
210                     if (name.equals("build")) {
211                         build = setter;
212                     }
213                     if (!Modifier.isStatic(setter.getModifiers())
214                             && setter.getReturnType() == builderClazz
215                             && setter.getParameterTypes().length == 1) {
216                         if (name.startsWith("set")) {
217                             name = name.substring(3);
218                         } else if (name.startsWith("with")) {
219                             name = name.substring(4);
220                         }
221                         builderSetters.put(name.toLowerCase(), setter);
222                     }
223                 }
224                 args.addAll(builderSetters.keySet());
225                 args.addAll(Arrays.asList(signature(build)));
226                 if (build != null && args.containsAll(props.keySet())) {
227                     return type.cast(callSetters(callBuilder(m, builderSetters, build, props),
228                             setters, props));
229                 }
230             } else if (args.containsAll(props.keySet())) {
231                 return type.cast(callSetters(callMethod(m, null, props), setters, props));
232             }
233         }
234 
235         throw new IllegalArgumentException("No suitable '" + methodName + "' method for "
236                 + implementation + " supporting properties "
237                 + Joiner.on(", ").join(props.keySet()));
238     }
239 
240     private static String[] signature(final AccessibleObject member) {
241         String[] names = PARANAMER.lookupParameterNames(member, false);
242         if (names != null) {
243             names = names.clone();
244             for (int i = 0; i < names.length; ++i) {
245                 names[i] = names[i].trim().toLowerCase();
246             }
247         } else if (member instanceof Constructor) {
248             names = new String[((Constructor<?>) member).getParameterTypes().length];
249         } else if (member instanceof Method) {
250             names = new String[((Method) member).getParameterTypes().length];
251         } else {
252             throw new Error("Unexpected member " + member);
253         }
254         return names;
255     }
256 
257     private static Object callConstructor(final Constructor<?> constructor,
258             final Map<String, Object> properties) {
259 
260         // Prepare the argument list
261         final Type[] argTypes = constructor.getGenericParameterTypes();
262         final String[] argNames = signature(constructor);
263         final Object[] argValues = convertArgs(properties, argNames, argTypes);
264 
265         try {
266             // Invoke the constructor, reporting detailed error information on failure
267             return constructor.newInstance(argValues);
268         } catch (final Throwable ex) {
269             throw new RuntimeException("Invocation of constructor '" + constructor
270                     + "' with parameters " + Arrays.asList(argValues) + " failed", ex);
271         }
272     }
273 
274     private static Object callMethod(final Method method, final Object object,
275             final Map<String, Object> properties) {
276 
277         // Prepare the argument list
278         final Type[] argTypes = method.getGenericParameterTypes();
279         final String[] argNames = signature(method);
280         final Object[] argValues = convertArgs(properties, argNames, argTypes);
281 
282         try {
283             // Invoke the method, reporting detailed error information on failure
284             return method.invoke(object, argValues);
285         } catch (final Throwable ex) {
286             throw new RuntimeException("Invocation of method '" + method + "' on object " + object
287                     + " with parameters " + Arrays.asList(argValues) + " failed", ex);
288         }
289     }
290 
291     private static Object callBuilder(final Method builder, final Map<String, Method> setters,
292             final Method build, final Map<String, Object> properties) {
293 
294         // Instantiate the builder object
295         final Object obj = callMethod(builder, null, properties);
296 
297         // Set properties on the builder
298         callSetters(obj, setters, properties);
299 
300         // Instantiate the object
301         return callMethod(build, obj, properties);
302     }
303 
304     private static Object callSetters(final Object object, final Map<String, Method> setters,
305             final Map<String, Object> properties) {
306 
307         for (final Map.Entry<String, Method> entry : setters.entrySet()) {
308             final String name = entry.getKey();
309             final Method setter = entry.getValue();
310             final Object value = properties.get(name);
311             if (value != null || properties.containsKey(name)) {
312                 callMethod(setter, object, ImmutableMap.of(name, value));
313             }
314         }
315 
316         return object; // for call chaining
317     }
318 
319     @SuppressWarnings({ "unchecked", "rawtypes" })
320     private static Object[] convertArgs(final Map<String, Object> properties,
321             final String[] names, final Type[] types) {
322         assert names.length == types.length;
323         final int length = names.length;
324         final Object[] result = new Object[length];
325         for (int i = 0; i < length; ++i) {
326             final String name = names[i];
327             final Type type = types[i];
328             final TypeToken token = TypeToken.of(type);
329             if (token.getRawType().isAssignableFrom(Map.class)) {
330                 final Type valueType = ((ParameterizedType) token.getSupertype(Map.class)
331                         .getType()).getActualTypeArguments()[1];
332                 final Map<String, Object> map = Maps.newHashMapWithExpectedSize(properties.size());
333                 for (final Map.Entry<String, Object> entry : properties.entrySet()) {
334                     map.put(entry.getKey(), convertMany(entry.getValue(), valueType));
335                 }
336                 result[i] = map;
337             } else if (token.getRawType().isAssignableFrom(Properties.class)) {
338                 final Properties props = new Properties();
339                 for (final Map.Entry<String, Object> entry : properties.entrySet()) {
340                     try {
341                         props.setProperty(entry.getKey(),
342                                 (String) convertMany(entry.getValue(), String.class));
343                     } catch (final Throwable ex) {
344                         // ignore conversion error
345                     }
346                 }
347                 result[i] = props;
348             } else {
349                 result[i] = convertMany(properties.get(name), type);
350             }
351         }
352         return result;
353     }
354 
355     @SuppressWarnings({ "rawtypes", "unchecked" })
356     private static Object convertMany(@Nullable final Object value, final Type type) {
357 
358         final TypeToken token = TypeToken.of(type);
359         final Class<?> clazz = token.getRawType();
360 
361         Type elementType = type;
362         if (Iterable.class.isAssignableFrom(clazz)) {
363             elementType = ((ParameterizedType) token.getSupertype(Iterable.class).getType())
364                     .getActualTypeArguments()[0];
365         } else if (clazz.isArray()) {
366             elementType = token.getComponentType().getType();
367         }
368         final Class<?> elementClass = TypeToken.of(elementType).getRawType();
369 
370         final List<Object> values = Lists.newArrayList();
371         if (value != null) {
372             if (value instanceof Iterable<?>) {
373                 for (final Object v : (Iterable<?>) value) {
374                     values.add(convertSingle(expand(v), elementClass));
375                 }
376             } else if (value.getClass().isArray()) {
377                 final int length = Array.getLength(value);
378                 for (int i = 0; i < length; ++i) {
379                     values.add(convertSingle(expand(Array.get(value, i)), elementClass));
380                 }
381             } else {
382                 values.add(convertSingle(expand(value), elementClass));
383             }
384         }
385 
386         if (clazz.isAssignableFrom(List.class)) {
387             return values;
388         } else if (clazz.isAssignableFrom(Set.class)) {
389             return Sets.newHashSet(values);
390         } else if (clazz.isArray()) {
391             return Iterables.toArray(values, (Class<Object>) clazz.getComponentType());
392         } else if (!values.isEmpty()) {
393             return values.get(0);
394         } else if (clazz == long.class) {
395             return 0L;
396         } else if (clazz == int.class) {
397             return 0;
398         } else if (clazz == short.class) {
399             return (short) 0;
400         } else if (elementClass == byte.class) {
401             return (byte) 0;
402         } else if (elementClass == char.class) {
403             return (char) 0;
404         } else if (elementClass == boolean.class) {
405             return false;
406         }
407         return null;
408     }
409 
410     private static Object convertSingle(final Object object, final Class<?> type) {
411         if (type == XPath.class) {
412             return object instanceof XPath || object == null ? (XPath) object : //
413                     XPath.parse((String) convertSingle(object, String.class));
414         } else {
415             return Data.convert(object, type);
416         }
417     }
418 
419     private static Object expand(final Object object) {
420 
421         String string;
422         if (object instanceof String) {
423             string = (String) object;
424         } else if (object instanceof Literal) {
425             string = ((Literal) object).getLabel();
426         } else {
427             return object;
428         }
429 
430         final StringBuilder builder = new StringBuilder();
431         int index = 0;
432         final Matcher matcher = PLACEHOLDER.matcher(string);
433         while (matcher.find()) {
434             builder.append(string.substring(index, matcher.start()));
435             final String property = string.substring(matcher.start() + 2, matcher.end() - 1);
436             String value = System.getProperty(property);
437             if (value != null) {
438                 if (property.equals("user.dir") || property.equals("user.home")
439                         || property.equals("java.home") || property.equals("java.io.tmpdir")
440                         || property.equals("java.ext.dirs")) {
441                     value = value.replace('\\', '/');
442                 }
443                 builder.append(value);
444             }
445             index = matcher.end();
446         }
447         builder.append(string.substring(index));
448         final String expanded = builder.toString();
449 
450         if (object instanceof String) {
451             return expanded;
452         } else {
453             final Literal l = (Literal) object;
454             final URI dt = l.getDatatype();
455             final String lang = l.getLanguage();
456             return dt != null ? Data.getValueFactory().createLiteral(expanded, dt) //
457                     : Data.getValueFactory().createLiteral(expanded, lang);
458         }
459     }
460 
461 }