View Javadoc

1   /*******************************************************************************
2    * Copyright 2013 André Rouél and Dominik Seichter
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    *   http://www.apache.org/licenses/LICENSE-2.0
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   ******************************************************************************/
16  package net.sf.qualitytest.blueprint;
17  
18  import java.lang.reflect.Constructor;
19  import java.lang.reflect.Field;
20  import java.lang.reflect.Method;
21  import java.lang.reflect.Modifier;
22  import java.lang.reflect.Proxy;
23  import java.text.MessageFormat;
24  
25  import javax.annotation.Nonnull;
26  import javax.annotation.Nullable;
27  
28  import net.sf.qualitycheck.Check;
29  import net.sf.qualitycheck.Throws;
30  import net.sf.qualitycheck.exception.IllegalNullArgumentException;
31  import net.sf.qualitytest.ModifierBits;
32  import net.sf.qualitytest.blueprint.configuration.DefaultBlueprintConfiguration;
33  import net.sf.qualitytest.blueprint.configuration.RandomBlueprintConfiguration;
34  import net.sf.qualitytest.exception.BlueprintException;
35  import net.sf.qualitytest.exception.NoPublicConstructorException;
36  
37  /**
38   * Blueprinting is a technique that makes writing test easier. For unit-testing you often need data-objects, where the
39   * actual content of the objects does not matter. {@code Blueprint} creates data-objects filled with random or defined
40   * data automatically based on the "Blue-Print" which is the Class itself.
41   * <p>
42   * Blueprinting makes tests more maintainable as they depend less on test-data. Imagine, you add a new required
43   * attribute to a class. Usually, you have to add this to all tests using this class. With blueprinting you just have to
44   * add it to certain tests where the contents of the logic does actually matter. Most of the time the randomly generated
45   * value by {@code Blueprint} is just fine.
46   * <p>
47   * {@code Blueprint} is similar to C#'s AutoFixture (https://github.com/AutoFixture/AutoFixture#readme).
48   * <p>
49   * A simple example:
50   * 
51   * <pre>
52   * final BlueprintConfiguration config = new RandomBlueprintConfiguration().with(&quot;email&quot;, &quot;mail@example.com&quot;);
53   * final User user = Blueprint.construct(User.class, config);
54   * </pre>
55   * 
56   * or simpler
57   * 
58   * <pre>
59   * final User user = Blueprint.random().with(&quot;email&quot;, &quot;mail@example.com&quot;).construct(User.class);
60   * </pre>
61   * 
62   * {@code Blueprint} offers two custom configurations. A {@code DefaultBlueprintConfiguration} which fills any object
63   * using default, empty or 0 values. The second configuration, {@code RandomBlueprintConfiguration} will always generate
64   * a random value. Both fill child objects using a deep-tree-search.
65   * <p>
66   * Utilities for collections can be found in {@code CollectionBlueprint}.
67   * 
68   * @see DefaultBlueprintConfiguration
69   * @see RandomBlueprintConfiguration
70   * 
71   * @author Dominik Seichter
72   */
73  public final class Blueprint {
74  
75  	private static final BlueprintConfiguration DEFAULT_CONFIG = new DefaultBlueprintConfiguration();
76  
77  	/**
78  	 * Blueprint a Java-Bean.
79  	 * 
80  	 * This method will call the default constructor and fill all setters using blueprints.
81  	 * 
82  	 * @param <T>
83  	 * @param clazz
84  	 *            a class, which must have a default constructor.
85  	 * @param config
86  	 *            a BlueprintConfiguration
87  	 * @param session
88  	 *            A {@code BlueprintSession}
89  	 * @return a blue printed instance of {@code T}
90  	 */
91  	@Throws(IllegalNullArgumentException.class)
92  	private static <T> T bean(@Nonnull final Class<T> clazz, @Nonnull final BlueprintConfiguration config,
93  			@Nonnull final BlueprintSession session) {
94  		Check.notNull(clazz, "clazz");
95  		Check.notNull(config, "config");
96  		Check.notNull(session, "sesion");
97  
98  		final T obj = safeNewInstance(session, clazz);
99  		blueprintPublicMethods(obj, clazz, config, session);
100 		blueprintPublicAttributes(obj, clazz, config, session);
101 		return obj;
102 	}
103 
104 	/**
105 	 * Blueprint all attributes of an object.
106 	 * <p>
107 	 * Static fields are ignored.
108 	 * 
109 	 * @param <T>
110 	 *            type of object
111 	 * @param obj
112 	 *            Instance of the object
113 	 * @param clazz
114 	 *            Class of the object
115 	 * @param config
116 	 *            Configuration to apply
117 	 * @param session
118 	 *            A {@code BlueprintSession}
119 	 */
120 	private static <T> void blueprintAllAttributes(final T obj, final Class<T> clazz, final BlueprintConfiguration config,
121 			final BlueprintSession session) {
122 		for (final Field f : clazz.getDeclaredFields()) {
123 			final boolean isStatic = ModifierBits.isModifierBitSet(f.getModifiers(), Modifier.STATIC);
124 			if (!isStatic) {
125 				blueprintField(obj, f, config, session);
126 			}
127 		}
128 	}
129 
130 	/**
131 	 * Blueprint a field.
132 	 * 
133 	 * @param that
134 	 *            Instance of the object
135 	 * @param field
136 	 *            Accessible field
137 	 * @param config
138 	 *            configuration to use.
139 	 * @param session
140 	 *            A {@code BlueprintSession}
141 	 */
142 	private static void blueprintField(final Object that, final Field field, final BlueprintConfiguration config,
143 			final BlueprintSession session) {
144 		CreationStrategy<?> creator = config.findCreationStrategyForField(field);
145 		if (creator == null) {
146 			creator = config.findCreationStrategyForType(field.getType());
147 		}
148 		final Object value = blueprintObject(field.getType(), config, creator, session);
149 
150 		final String action = MessageFormat.format("Setting field {0} to {1}.", field.getName(), value);
151 		SafeInvoke.invoke(new BlueprintExceptionRunnable<Object>(session, action) {
152 			@Override
153 			public Object runInternal() throws Exception {
154 				field.setAccessible(true);
155 				field.set(that, value);
156 				return null;
157 			}
158 		}, BlueprintException.class);
159 
160 	}
161 
162 	/**
163 	 * Blueprint a method.
164 	 * 
165 	 * @param that
166 	 *            Instance of the object
167 	 * @param m
168 	 *            setter method
169 	 * @param config
170 	 *            configuration to use.
171 	 * @param session
172 	 *            A {@code BlueprintSession}
173 	 */
174 	private static void blueprintMethod(final Object that, final Method m, final BlueprintConfiguration config,
175 			final BlueprintSession session) {
176 		final CreationStrategy<?> creator = config.findCreationStrategyForMethod(m);
177 		if (creator != null) {
178 			final Class<?>[] parameterTypes = m.getParameterTypes();
179 			final Object[] values = new Object[parameterTypes.length];
180 			for (int i = 0; i < parameterTypes.length; i++) {
181 				values[i] = creator.createValue(parameterTypes[i], config, session);
182 			}
183 
184 			final String action = MessageFormat.format("Invoking method {0} with arguments {1}.", m.getName(), values);
185 			SafeInvoke.invoke(new BlueprintExceptionRunnable<Object>(session, action) {
186 				@Override
187 				public Object runInternal() throws Exception {
188 					m.setAccessible(true);
189 					m.invoke(that, values);
190 					return null;
191 				}
192 
193 			}, BlueprintException.class);
194 		}
195 	}
196 
197 	@Nullable
198 	@SuppressWarnings({ "unchecked" })
199 	private static <T> T blueprintObject(@Nonnull final Class<T> clazz, @Nonnull final BlueprintConfiguration config,
200 			@Nullable final CreationStrategy<?> creator, @Nonnull final BlueprintSession session) {
201 		final boolean cycle = session.push(clazz);
202 		final T ret;
203 		if (cycle) {
204 			ret = (T) config.handleCycle(session, clazz);
205 		} else {
206 			if (creator != null) {
207 				ret = (T) creator.createValue(clazz, config, session);
208 			} else if (clazz.isInterface()) {
209 				ret = (T) proxy(clazz, config, session);
210 			} else if (hasPublicDefaultConstructor(clazz)) {
211 				ret = bean(clazz, config, session);
212 			} else {
213 				ret = immutable(clazz, config, session);
214 			}
215 		}
216 		session.pop();
217 		return ret;
218 	}
219 
220 	/**
221 	 * Blueprint all public attributes in an object.
222 	 * 
223 	 * Does nothing if {@code config.isWithPublicAttributes} is false.
224 	 * 
225 	 * Static fields are ignored.
226 	 * 
227 	 * @param <T>
228 	 *            type of object
229 	 * @param obj
230 	 *            Instance of the object
231 	 * @param clazz
232 	 *            Class of the object
233 	 * @param config
234 	 *            Configuration to apply
235 	 * @param session
236 	 *            A {@code BlueprintSession}
237 	 */
238 	private static <T> void blueprintPublicAttributes(final T obj, final Class<T> clazz, final BlueprintConfiguration config,
239 			final BlueprintSession session) {
240 		if (!config.isWithPublicAttributes()) {
241 			return;
242 		}
243 
244 		for (final Field f : clazz.getFields()) {
245 			final boolean isStatic = ModifierBits.isModifierBitSet(f.getModifiers(), Modifier.STATIC);
246 			final boolean isFinal = ModifierBits.isModifierBitSet(f.getModifiers(), Modifier.FINAL);
247 			if (!isStatic && !isFinal) {
248 				blueprintField(obj, f, config, session);
249 			}
250 		}
251 	}
252 
253 	/**
254 	 * Blueprint all non static public method in an object.
255 	 * 
256 	 * @param <T>
257 	 *            type of object
258 	 * @param obj
259 	 *            Instance of the object
260 	 * @param clazz
261 	 *            Class of the object
262 	 * @param config
263 	 *            Configuration to apply
264 	 * @param session
265 	 *            A {@code BlueprintSession}
266 	 */
267 	private static <T> void blueprintPublicMethods(final T obj, final Class<T> clazz, final BlueprintConfiguration config,
268 			final BlueprintSession session) {
269 		for (final Method m : clazz.getMethods()) {
270 			if (isRelevant(m)) {
271 				blueprintMethod(obj, m, config, session);
272 			}
273 		}
274 	}
275 
276 	/**
277 	 * Construct a Java-Object using a class as a blueprint.
278 	 * 
279 	 * If the object has a default constructor, it will be called and all setters will be called. If the object does not
280 	 * have a default constructor the first constructor is called and filled with all parameters. Afterwards all setters
281 	 * will be called.
282 	 * 
283 	 * @param <T>
284 	 * @param clazz
285 	 *            a class
286 	 * @return a blue printed instance of {@code T}
287 	 */
288 	@Nullable
289 	@Throws(IllegalNullArgumentException.class)
290 	public static <T> T construct(@Nonnull final Class<T> clazz) {
291 		Check.notNull(clazz, "clazz");
292 
293 		return Blueprint.construct(clazz, DEFAULT_CONFIG, new BlueprintSession());
294 	}
295 
296 	/**
297 	 * Construct a Java-Object using a class as a blueprint.
298 	 * 
299 	 * If the object has a default constructor, it will be called and all setters will be called. If the object does not
300 	 * have a default constructor the first constructor is called and filled with all parameters. Afterwards all setters
301 	 * will be called.
302 	 * 
303 	 * @param <T>
304 	 * @param clazz
305 	 *            a class
306 	 * @param config
307 	 *            a {@code BlueprintConfiguration}
308 	 * @return a blue printed instance of {@code T}
309 	 */
310 	@Nullable
311 	@Throws(IllegalNullArgumentException.class)
312 	public static <T> T construct(@Nonnull final Class<T> clazz, @Nonnull final BlueprintConfiguration config) {
313 		Check.notNull(clazz, "clazz");
314 		Check.notNull(config, "config");
315 
316 		return Blueprint.construct(clazz, config, new BlueprintSession());
317 	}
318 
319 	/**
320 	 * Construct a Java-Object using a class as a blueprint.
321 	 * 
322 	 * If the object has a default constructor, it will be called and all setters will be called. If the object does not
323 	 * have a default constructor the first constructor is called and filled with all parameters. Afterwards all setters
324 	 * will be called.
325 	 * 
326 	 * @param <T>
327 	 * @param clazz
328 	 *            a class
329 	 * @param config
330 	 *            a {@code BlueprintConfiguration}
331 	 * @param session
332 	 *            a {@code BlueprintSession}
333 	 * @return a blue printed instance of {@code T}
334 	 */
335 	@Nullable
336 	@Throws(IllegalNullArgumentException.class)
337 	public static <T> T construct(@Nonnull final Class<T> clazz, @Nonnull final BlueprintConfiguration config,
338 			@Nonnull final BlueprintSession session) {
339 		Check.notNull(clazz, "clazz");
340 		Check.notNull(config, "config");
341 		Check.notNull(session, "session");
342 
343 		final CreationStrategy<?> creator = config.findCreationStrategyForType(clazz);
344 		return blueprintObject(clazz, config, creator, session);
345 	}
346 
347 	/**
348 	 * Return a new configuration for default blueprinting with zero or empty default values.
349 	 * 
350 	 * @return a new {@code DefaultBlueprintConfiguration}
351 	 */
352 	public static BlueprintConfiguration def() {
353 		return new DefaultBlueprintConfiguration();
354 	}
355 
356 	/**
357 	 * Find the first public constructor in a class.
358 	 * 
359 	 * @param <T>
360 	 *            type parameter of the class and constructor.
361 	 * @param clazz
362 	 *            the class object
363 	 */
364 	private static <T> Constructor<?> findFirstPublicConstructor(final Class<T> clazz) {
365 		final Constructor<?>[] constructors = clazz.getConstructors();
366 		for (final Constructor<?> c : constructors) {
367 			return c;
368 		}
369 		return null;
370 	}
371 
372 	/**
373 	 * Test if a class has a public default constructor (i.e. a public constructor without constructor arguments).
374 	 * 
375 	 * @param clazz
376 	 *            the class object.
377 	 * @return true if the class has a public default constructor.
378 	 */
379 	private static boolean hasPublicDefaultConstructor(final Class<?> clazz) {
380 		final Constructor<?>[] constructors = clazz.getConstructors();
381 		for (final Constructor<?> c : constructors) {
382 			if (c.getParameterTypes().length == 0) {
383 				return true;
384 			}
385 		}
386 		return false;
387 	}
388 
389 	/**
390 	 * Blueprint an immutable class based on the constructor parameters of the first public constructor.
391 	 * 
392 	 * @param <T>
393 	 *            type parameter of the blueprinted class
394 	 * @param clazz
395 	 *            the class object
396 	 * @param config
397 	 *            the configuration
398 	 * @param session
399 	 *            A {@code BlueprintSession}
400 	 * @return a new blueprint of the class with all constructor parameters to filled.
401 	 */
402 	private static <T> T immutable(final Class<T> clazz, final BlueprintConfiguration config, final BlueprintSession session) {
403 		final Constructor<?> constructor = findFirstPublicConstructor(clazz);
404 		if (constructor == null) {
405 			final BlueprintException b = new NoPublicConstructorException(clazz.getSimpleName());
406 			final String action = MessageFormat.format("Finding public constructor in {0}", clazz.getName());
407 			session.setLastAction(action);
408 			b.setSession(session);
409 			throw b;
410 		}
411 
412 		final Class<?>[] parameterTypes = constructor.getParameterTypes();
413 		final Object[] parameters = new Object[parameterTypes.length];
414 		for (int i = 0; i < parameterTypes.length; i++) {
415 			parameters[i] = construct(parameterTypes[i], config, session);
416 		}
417 
418 		@SuppressWarnings("unchecked")
419 		final T obj = (T) safeNewInstance(session, constructor, parameters);
420 		blueprintAllAttributes(obj, clazz, config, session);
421 
422 		return obj;
423 	}
424 
425 	/**
426 	 * Check if a method is setter according to the java
427 	 * 
428 	 * @param m
429 	 *            a method
430 	 * @return true if this is a setter method
431 	 */
432 	protected static boolean isRelevant(final Method m) {
433 		final boolean isNotStatic = !ModifierBits.isModifierBitSet(m.getModifiers(), Modifier.STATIC);
434 		final boolean isPublic = ModifierBits.isModifierBitSet(m.getModifiers(), Modifier.PUBLIC);
435 		return isNotStatic && isPublic;
436 	}
437 
438 	/**
439 	 * Create a proxy for an interface, which does nothing.
440 	 * 
441 	 * @param <T>
442 	 *            Class of the interface
443 	 * @param iface
444 	 *            an interace
445 	 * @param config
446 	 *            {@code BlueprintConfiguration}
447 	 * @param session
448 	 *            {@code BlueprintSession}
449 	 * @return a new dynamic proxy
450 	 */
451 	@SuppressWarnings("unchecked")
452 	private static <T> T proxy(final Class<T> iface, final BlueprintConfiguration config, final BlueprintSession session) {
453 		return (T) Proxy.newProxyInstance(iface.getClassLoader(), new Class[] { iface }, new BlueprintInvocationHandler(config, session));
454 	}
455 
456 	/**
457 	 * Return a new configuration for random blueprinting.
458 	 * 
459 	 * @return a new {@code RandomBlueprintConfiguration}
460 	 */
461 	public static BlueprintConfiguration random() {
462 		return new RandomBlueprintConfiguration();
463 	}
464 
465 	/**
466 	 * Create a new instance of a class without having to care about the checked exceptions. The class must have an
467 	 * accessible default constructor.
468 	 * 
469 	 * @param <T>
470 	 *            type parameter of the class to create
471 	 * @param clazz
472 	 *            class-object
473 	 * @return a new instance of the class
474 	 * 
475 	 * @throws BlueprintException
476 	 *             in case of any error
477 	 */
478 	private static <T> T safeNewInstance(final BlueprintSession session, final Class<T> clazz) {
479 		final String action = MessageFormat.format("Creating clazz {0} with default constructor.", clazz.getName());
480 		return SafeInvoke.invoke(new BlueprintExceptionRunnable<T>(session, action) {
481 
482 			@Override
483 			public T runInternal() throws Exception {
484 				return (T) clazz.newInstance();
485 			}
486 		}, BlueprintException.class);
487 	}
488 
489 	/**
490 	 * Create a new instance of a class without having to care about the checked exceptions using a given constructor.
491 	 * 
492 	 * @param constructor
493 	 *            constructor to call
494 	 * @param parameters
495 	 *            constructor arguments
496 	 * @throws BlueprintException
497 	 *             in case of any error
498 	 */
499 	@SuppressWarnings("unchecked")
500 	private static <T> T safeNewInstance(final BlueprintSession session, final Constructor<?> constructor, final Object[] parameters) {
501 		final String action = MessageFormat.format("Creating clazz {0} with constructor {1}.", constructor.getClass().getName(),
502 				constructor.toGenericString());
503 		return SafeInvoke.invoke(new BlueprintExceptionRunnable<T>(session, action) {
504 
505 			@Override
506 			public T runInternal() throws Exception {
507 				return (T) constructor.newInstance(parameters);
508 			}
509 		}, BlueprintException.class);
510 	}
511 
512 	/**
513 	 * <strong>Attention:</strong> This class is not intended to create objects from it.
514 	 */
515 	private Blueprint() {
516 		// This class is not intended to create objects from it.
517 	}
518 }