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
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
261 final Type[] argTypes = constructor.getGenericParameterTypes();
262 final String[] argNames = signature(constructor);
263 final Object[] argValues = convertArgs(properties, argNames, argTypes);
264
265 try {
266
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
278 final Type[] argTypes = method.getGenericParameterTypes();
279 final String[] argNames = signature(method);
280 final Object[] argValues = convertArgs(properties, argNames, argTypes);
281
282 try {
283
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
295 final Object obj = callMethod(builder, null, properties);
296
297
298 callSetters(obj, setters, properties);
299
300
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;
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
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 }