View Javadoc

1   /*
2    * Copyright (c) 2007, James Leigh All rights reserved.
3    * 
4    * Redistribution and use in source and binary forms, with or without
5    * modification, are permitted provided that the following conditions are met:
6    * 
7    * - Redistributions of source code must retain the above copyright notice, this
8    *   list of conditions and the following disclaimer.
9    * - Redistributions in binary form must reproduce the above copyright notice,
10   *   this list of conditions and the following disclaimer in the documentation
11   *   and/or other materials provided with the distribution. 
12   * - Neither the name of the openrdf.org nor the names of its contributors may
13   *   be used to endorse or promote products derived from this software without
14   *   specific prior written permission.
15   * 
16   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
20   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26   * POSSIBILITY OF SUCH DAMAGE.
27   * 
28   */
29  package org.openrdf.elmo.impl;
30  
31  import java.beans.PropertyDescriptor;
32  import java.lang.reflect.Method;
33  import java.lang.reflect.ParameterizedType;
34  import java.lang.reflect.Type;
35  import java.util.ArrayList;
36  import java.util.Collection;
37  import java.util.HashSet;
38  import java.util.List;
39  import java.util.Set;
40  
41  import javassist.CannotCompileException;
42  import javassist.NotFoundException;
43  
44  import org.openrdf.elmo.ElmoMapperResolver;
45  import org.openrdf.elmo.ElmoProperty;
46  import org.openrdf.elmo.ElmoPropertyFactory;
47  import org.openrdf.elmo.Entity;
48  import org.openrdf.elmo.Mergeable;
49  import org.openrdf.elmo.Refreshable;
50  import org.openrdf.elmo.annotations.inverseOf;
51  import org.openrdf.elmo.annotations.rdf;
52  import org.openrdf.elmo.dynacode.ClassFactory;
53  import org.openrdf.elmo.dynacode.ClassTemplate;
54  import org.openrdf.elmo.dynacode.CodeBuilder;
55  import org.openrdf.elmo.exceptions.ElmoCompositionException;
56  import org.slf4j.Logger;
57  import org.slf4j.LoggerFactory;
58  
59  /**
60   * Properties that have the rdf or localname annotation are replaced with
61   * getters and setters that access the Sesame Repository directly.
62   * 
63   * @author James Leigh
64   * 
65   */
66  public class ElmoMapperClassFactory implements ElmoMapperResolver {
67  	private static final String PROPERTY_SUFFIX = "Property";
68  
69  	private static final String FACTORY_SUFFIX = "Factory";
70  
71  	private static final String SET_PROPERTY_DESCRIPTOR = "setPropertyDescriptor";
72  
73  	private static final String CREATE_ELMO_PROPERTY = "createElmoProperty";
74  
75  	private static final String CLASS_PREFIX = "elmobeans.mappers.";
76  
77  	private static final String BEAN_FIELD_NAME = "_$elmoBean";
78  
79  	private static final String GET_ALL = "getAll";
80  
81  	private static final String GET_SINGLE = "getSingle";
82  
83  	private static final String SET_ALL = "setAll";
84  
85  	private static final String SET_SINGLE = "setSingle";
86  
87  	private final Logger logger = LoggerFactory
88  			.getLogger(ElmoMapperClassFactory.class);
89  
90  	private ClassFactory cp;
91  
92  	private Class<?> propertyFactoryClass;
93  
94  	public ElmoMapperClassFactory() {
95  	}
96  
97  	public void setClassDefiner(ClassFactory definer) {
98  		this.cp = definer;
99  	}
100 
101 	@SuppressWarnings("unchecked")
102 	public void setElmoPropertyFactoryClass(
103 			Class<? extends ElmoPropertyFactory> propertyFactoryClass) {
104 		this.propertyFactoryClass = propertyFactoryClass;
105 	}
106 
107 	public Collection<Class<?>> findMappers(Collection<Class<?>> interfaces) {
108 		try {
109 			Set<Class<?>> faces = new HashSet<Class<?>>();
110 			for (Class<?> i : interfaces) {
111 				faces.add(i);
112 				faces = getInterfaces(i, faces);
113 			}
114 			List<Class<?>> mappers = new ArrayList<Class<?>>();
115 			for (Class<?> concept : faces) {
116 				if (isRdfPropertyPresent(concept)) {
117 					mappers.add(findBehaviour(concept));
118 				}
119 			}
120 			return mappers;
121 		} catch (ElmoCompositionException e) {
122 			throw e;
123 		} catch (Exception e) {
124 			throw new ElmoCompositionException(e);
125 		}
126 	}
127 
128 	private Class<?> findBehaviour(Class<?> concept) throws Exception {
129 		String className = getJavaClassName(concept);
130 		try {
131 			return Class.forName(className, true, cp);
132 		} catch (ClassNotFoundException e1) {
133 			synchronized (cp) {
134 				try {
135 					return Class.forName(className, true, cp);
136 				} catch (ClassNotFoundException e2) {
137 					return implement(className, concept);
138 				}
139 			}
140 		}
141 	}
142 
143 	private boolean isRdfPropertyPresent(Class<?> concept) {
144 		for (Method m : concept.getDeclaredMethods()) {
145 			if (m.isAnnotationPresent(rdf.class))
146 				return true;
147 		}
148 		return false;
149 	}
150 
151 	private String getJavaClassName(Class<?> concept) {
152 		String cn = concept.getName();
153 		return CLASS_PREFIX + cn + "Mapper";
154 	}
155 
156 	private Class<?> implement(String className, Class<?> concept)
157 			throws Exception {
158 		ClassTemplate cc;
159 		HashSet<Class<?>> interfaces = new HashSet<Class<?>>();
160 		assert concept.isInterface();
161 		interfaces.add(concept);
162 		cc = cp.createClassTemplate(className);
163 		cc.addInterface(Mergeable.class);
164 		cc.addInterface(Refreshable.class);
165 		addNewConstructor(cc);
166 		enhance(cc, concept);
167 		return cp.createClass(cc);
168 	}
169 
170 	private Set<Class<?>> getInterfaces(Class<?> concept,
171 			Set<Class<?>> interfaces) throws NotFoundException {
172 		for (Class<?> face : concept.getInterfaces()) {
173 			if (!interfaces.contains(face)) {
174 				interfaces.add(face);
175 				getInterfaces(face, interfaces);
176 			}
177 		}
178 		return interfaces;
179 	}
180 
181 	private void addNewConstructor(ClassTemplate cc) throws NotFoundException,
182 			CannotCompileException {
183 		cc.createField(Entity.class, BEAN_FIELD_NAME);
184 		cc.addConstructor(new Class<?>[] { Entity.class }, BEAN_FIELD_NAME + " = $1;");
185 	}
186 
187 	private void enhance(ClassTemplate cc, Class<?> concept) throws Exception {
188 		for (Method method : concept.getDeclaredMethods()) {
189 			if (method.isAnnotationPresent(rdf.class)
190 					|| method.isAnnotationPresent(inverseOf.class)) {
191 				if (method.getParameterTypes().length == 0) {
192 					overrideMethod(method, cc);
193 				} else {
194 					String msg = "@rdf and @inverseOf annotations should only be on getter methods: {}#{}";
195 					logger.warn(msg, concept.getSimpleName(), method.getName());
196 				}
197 			}
198 		}
199 		overrideMergeMethod(cc, concept);
200 		overrideRefreshMethod(cc, concept);
201 	}
202 
203 	private void overrideMergeMethod(ClassTemplate cc, Class<?> concept)
204 			throws Exception {
205 		Method merge = Mergeable.class.getMethod("merge", Object.class);
206 		CodeBuilder sb = cc.overrideMethod(merge);
207 		sb.code("if($1 instanceof ").code(concept.getName());
208 		sb.code("){\n");
209 		for (Method method : concept.getDeclaredMethods()) {
210 			if (method.isAnnotationPresent(rdf.class)) {
211 				String property = getPropertyName(method);
212 				Method setter = getSetterMethod(property, method);
213 				if (setter == null)
214 					continue;
215 				if (method.getReturnType().isPrimitive()) {
216 					sb.code(setter.getName());
217 					sb.code("(").code("((").code(concept.getName());
218 					sb.code(") $1).").code(method.getName());
219 					sb.code("()").code(");\n");
220 				} else {
221 					sb.declareObject(method.getReturnType(), property + "Var");
222 					sb.code("((").code(concept.getName());
223 					sb.code(") $1).").code(method.getName());
224 					sb.code("();\n");
225 					// array != null does not seem to work in javassist
226 					sb.code("if(");
227 					sb.codeInstanceof(property + "Var", method.getReturnType());
228 					sb.code("){");
229 					sb.code(setter.getName());
230 					sb.code("(").code(property).code("Var);}\n");
231 				}
232 			}
233 		}
234 		sb.code("}").end();
235 	}
236 
237 	private void overrideRefreshMethod(ClassTemplate cc, Class<?> concept)
238 			throws Exception {
239 		Method refresh = Refreshable.class.getMethod("refresh");
240 		CodeBuilder sb = cc.overrideMethod(refresh);
241 		for (String field : cc.getDeclaredFieldNames()) {
242 			if (field.endsWith(PROPERTY_SUFFIX)) {
243 				sb.code("if (").code(field).code(" != null) {");
244 				sb.code(field).code(".refresh();}\n");
245 			}
246 		}
247 		sb.end();
248 	}
249 
250 	private void overrideMethod(Method method, ClassTemplate cc) throws Exception {
251 		String property = getPropertyName(method);
252 		Method setter = getSetterMethod(property, method);
253 		Class<?> type = method.getReturnType();
254 		String field = createPropertyField(property, cc);
255 		String factory = createFactoryField(property, method, setter, cc);
256 		CodeBuilder body = cc.overrideMethod(method);
257 		appendNullCheck(body, field, factory, method, cc);
258 		appendGetterMethod(body, field, type, method, cc);
259 		body.end();
260 		if (setter != null) {
261 			body = cc.overrideMethod(setter);
262 			appendNullCheck(body, field, factory, method, cc);
263 			appendSetterMethod(body, field, type, method, cc);
264 			body.end();
265 		}
266 	}
267 
268 	private Method getSetterMethod(String property, Method getter) {
269 		try {
270 			StringBuilder smn = new StringBuilder();
271 			smn.append("set").append(Character.toUpperCase(property.charAt(0)));
272 			smn.append(property, 1, property.length());
273 			Class<?> dc = getter.getDeclaringClass();
274 			Class<?> rt = getter.getReturnType();
275 			return dc.getDeclaredMethod(smn.toString(), rt);
276 		} catch (NoSuchMethodException exc) {
277 			return null;
278 		}
279 	}
280 
281 	private String getPropertyName(Method method) {
282 		String name = method.getName();
283 		for (int i = 0, n = name.length(); i < n; i++) {
284 			char chr = name.charAt(i);
285 			if (Character.isUpperCase(chr)) {
286 				StringBuilder property = new StringBuilder(name.length() - i);
287 				property.append(Character.toLowerCase(name.charAt(i)));
288 				property.append(name, i + 1, name.length());
289 				return property.toString();
290 			}
291 		}
292 		throw new IllegalArgumentException();
293 	}
294 
295 	private String createPropertyField(String property, ClassTemplate cc)
296 			throws Exception {
297 		StringBuilder name = new StringBuilder();
298 		name.append("_$").append(property).append(PROPERTY_SUFFIX);
299 		String fieldName = name.toString();
300 		cc.createField(ElmoProperty.class, fieldName);
301 		return fieldName;
302 	}
303 
304 	private String createFactoryField(String property, Method method,
305 			Method setter, ClassTemplate cc) throws Exception {
306 		String setterName = setter == null? null:setter.getName();
307 		Class<?> declaringClass = method.getDeclaringClass();
308 		String getterName = method.getName();
309 		Class<?> class1 = PropertyDescriptor.class;
310 		Class<?> type = ElmoPropertyFactory.class;
311 		String fieldName = "_$" + property + FACTORY_SUFFIX;
312 		CodeBuilder code = cc.assignStaticField(type, fieldName);
313 		code.construct(propertyFactoryClass);
314 		code.code(".").code(SET_PROPERTY_DESCRIPTOR).code("(");
315 		code.construct(class1, property, declaringClass, getterName, setterName);
316 		code.code(")").end();
317 		return fieldName;
318 	}
319 
320 	private boolean isCollection(Class<?> type, Method method, ClassTemplate cc)
321 			throws Exception {
322 		return Set.class.equals(type);
323 	}
324 
325 	public static Class<?> getClassType(Method method) {
326 		return method.getReturnType();
327 	}
328 
329 	public static Class<?> getContentClassType(Method method) {
330 		Type[] types = getTypeArguments(method);
331 		if (types == null || types.length != 1)
332 			return null;
333 		if (types[0] instanceof Class)
334 			return (Class) types[0];
335 		if (types[0] instanceof ParameterizedType)
336 			return (Class) ((ParameterizedType) types[0]).getRawType();
337 		return null;
338 	}
339 
340 	public static Type[] getTypeArguments(Method method) {
341 		Type type = method.getGenericReturnType();
342 		if (type instanceof ParameterizedType)
343 			return ((ParameterizedType) type).getActualTypeArguments();
344 		return null;
345 	}
346 
347 	public static Type[] getContentTypeArguments(Method method) {
348 		Type[] types = getTypeArguments(method);
349 		if (types == null || types.length != 1)
350 			return null;
351 		if (types[0] instanceof ParameterizedType)
352 			return ((ParameterizedType) types[0]).getActualTypeArguments();
353 		return null;
354 	}
355 
356 	private CodeBuilder appendNullCheck(CodeBuilder body, String field,
357 			String propertyFactory, Method method, ClassTemplate cc)
358 			throws Exception {
359 		body.code("if (").code(field).code(" == null) {");
360 		body.assign(field).code(propertyFactory);
361 		body.code(".").code(CREATE_ELMO_PROPERTY);
362 		body.code("(").code(BEAN_FIELD_NAME).code(");}");
363 		return body;
364 	}
365 
366 	private CodeBuilder appendGetterMethod(CodeBuilder body, String field,
367 			Class<?> type, Method method, ClassTemplate cc) throws Exception {
368 		if (isCollection(type, method, cc)) {
369 			body.code("return ").code(field);
370 			body.code(".").code(GET_ALL);
371 			body.code("();");
372 		} else if (type.isPrimitive()) {
373 			body.declareObject(type, "result");
374 			body.castObject(field, type);
375 			body.code(".").code(GET_SINGLE);
376 			body.code("();");
377 			body.code("if (result != null) ");
378 			body.code("return result.").code(type.getName()).code("Value();");
379 			if (Boolean.TYPE.equals(type)) {
380 				body.code("return false;");
381 			} else {
382 				body.code("return ($r) 0;");
383 			}
384 		} else {
385 			body.code("return ");
386 			body.castObject(field, type);
387 			body.code(".").code(GET_SINGLE);
388 			body.code("();");
389 		}
390 		return body;
391 	}
392 
393 	private CodeBuilder appendSetterMethod(CodeBuilder body, String field,
394 			Class<?> type, Method method, ClassTemplate cc) throws Exception {
395 		body.code(field).code(".");
396 		if (isCollection(type, method, cc)) {
397 			body.code(SET_ALL).code("($1);");
398 		} else {
399 			body.code(SET_SINGLE).code("(").codeObject("$1", type).code(");");
400 		}
401 		return body;
402 	}
403 }