All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.nylle.javafixture.InstanceFactory Maven / Gradle / Ivy

Go to download

JavaFixture is the attempt to bring Mark Seemann's AutoFixture for .NET to the Java world. Its purpose is to generate full object graphs for use in test suites with a fluent API for customising the test objects during generation.

There is a newer version: 2.11.0
Show newest version
package com.github.nylle.javafixture;

import org.objenesis.Objenesis;
import org.objenesis.ObjenesisStd;
import org.objenesis.instantiator.ObjectInstantiator;

import javassist.util.proxy.ProxyFactory;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.lang.reflect.Proxy;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Queue;
import java.util.Random;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TransferQueue;

import static java.lang.String.format;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toList;

public class InstanceFactory {

    private final SpecimenFactory specimenFactory;
    private final Random random;

    private static final Map, Object> primitiveDefaults = Map.of(
            Boolean.TYPE, false,
            Character.TYPE, '\0',
            Byte.TYPE, (byte) 0,
            Short.TYPE, 0,
            Integer.TYPE, 0,
            Long.TYPE, 0L,
            Float.TYPE, 0.0f,
            Double.TYPE, 0.0d
    );

    public InstanceFactory(SpecimenFactory specimenFactory) {
        this.specimenFactory = specimenFactory;
        this.random = new Random();
    }

    public  T construct(final SpecimenType type, CustomizationContext customizationContext) {
        var constructors = type.getDeclaredConstructors()
                .stream()
                .filter(x -> Modifier.isPublic(x.getModifiers()))
                .collect(toList());

        if (constructors.isEmpty()) {
            return manufacture(type, customizationContext);
        }

        return construct(type, constructors.get(random.nextInt(constructors.size())), customizationContext);
    }

    public  T manufacture(final SpecimenType type, CustomizationContext customizationContext) {
        var factoryMethods = type.getFactoryMethods();
        Collections.shuffle(factoryMethods);
        var results = factoryMethods
                .stream()
                .filter(method -> Modifier.isPublic(method.getModifiers()))
                .filter(method -> !hasSpecimenTypeAsParameter(method, type))
                .map(x -> manufactureOrNull(x, type, customizationContext))
                .filter(x -> x != null)
                .findFirst();

        return results.orElseThrow(() -> new SpecimenException(format("Cannot manufacture %s", type.asClass())));
    }

    private  boolean hasSpecimenTypeAsParameter(Method m, SpecimenType type) {
        return stream(m.getGenericParameterTypes())
                .anyMatch(t -> t.getTypeName().equals(type.asClass().getName()));
    }

    public  T instantiate(final SpecimenType type) {
        try {
            return type.asClass().getDeclaredConstructor().newInstance();
        } catch (Exception e) {
            return (T) ((ObjectInstantiator) ((Objenesis) new ObjenesisStd()).getInstantiatorOf(type.asClass())).newInstance();
        }
    }

    public  Object proxy(final SpecimenType type) {
        return proxy(type, new HashMap<>());
    }

    public  Object proxy(final SpecimenType type, final Map> specimens) {
        if (type.isInterface()) {
            return Proxy.newProxyInstance(
                    type.asClass().getClassLoader(),
                    new Class[]{type.asClass()},
                    new ProxyInvocationHandler(specimenFactory, specimens));
        }

        return createProxyForAbstract(type, specimens);
    }

    public > T createCollection(final SpecimenType type) {
        return type.isInterface() ? createCollectionFromInterfaceType(type.asClass()) : createCollectionFromConcreteType(type);
    }

    private  T construct(final SpecimenType type, final Constructor constructor, CustomizationContext customizationContext) {
        try {
            constructor.setAccessible(true);
            return (T) constructor.newInstance(stream(constructor.getParameters())
                    .map(p -> createParameter(p, customizationContext))
                    .toArray());
        } catch (Exception e) {
            return manufacture(type, customizationContext);
        }
    }

    private static Object defaultValue(Class type) {
        if (type.isPrimitive()) {
            return primitiveDefaults.get(type);
        } else {
            return null;
        }
    }

    private Object createParameter(Parameter parameter, CustomizationContext customizationContext) {
        if (customizationContext.getIgnoredFields().contains(parameter.getName())) {
            return defaultValue(parameter.getType());
        }
        if (customizationContext.getCustomFields().containsKey(parameter.getName())) {
            return customizationContext.getCustomFields().get(parameter.getName());
        }
        var specimen = specimenFactory.build(SpecimenType.fromClass(parameter.getParameterizedType()));
        return specimen.create(new CustomizationContext(List.of(), Map.of(), customizationContext.useRandomConstructor()), new Annotation[0]);
    }

    private  T createProxyForAbstract(final SpecimenType type, final Map> specimens) {
        try {
            var factory = new ProxyFactory();
            factory.setSuperclass(type.asClass());
            return (T) factory.create(new Class[0], new Object[0], new ProxyInvocationHandler(specimenFactory, specimens));
        } catch (Exception e) {
            throw new SpecimenException(format("Unable to construct abstract class %s: %s", type.asClass(), e.getMessage()), e);
        }
    }

    private  T manufactureOrNull(final Method method, SpecimenType type, CustomizationContext customizationContext) {
        try {
            List arguments = new ArrayList<>();
            for (int i = 0; i < method.getParameterCount(); i++) {
                var genericParameterType = method.getGenericParameterTypes()[i];
                var specimen = specimenFactory.build(type.isParameterized()
                        ? SpecimenType.fromClass(type.getGenericTypeArgument(i))
                        : SpecimenType.fromClass(genericParameterType));
                var o = specimen.create(customizationContext, new Annotation[0]);
                arguments.add(o);
            }
            return (T) method.invoke(null, arguments.toArray());
        } catch (Exception ex) {
            return null;
        }
    }

    private > T createCollectionFromInterfaceType(final Class type) {

        if (List.class.isAssignableFrom(type)) {
            return (T) new ArrayList();
        }

        if (NavigableSet.class.isAssignableFrom(type)) {
            return (T) new TreeSet();
        }

        if (SortedSet.class.isAssignableFrom(type)) {
            return (T) new TreeSet();
        }

        if (Set.class.isAssignableFrom(type)) {
            return (T) new HashSet();
        }

        if (BlockingDeque.class.isAssignableFrom(type)) {
            return (T) new LinkedBlockingDeque();
        }

        if (Deque.class.isAssignableFrom(type)) {
            return (T) new ArrayDeque();
        }

        if (TransferQueue.class.isAssignableFrom(type)) {
            return (T) new LinkedTransferQueue();
        }

        if (BlockingQueue.class.isAssignableFrom(type)) {
            return (T) new LinkedBlockingQueue();
        }

        if (Queue.class.isAssignableFrom(type)) {
            return (T) new LinkedList();
        }

        if (Collection.class.isAssignableFrom(type)) {
            return (T) new ArrayList();
        }

        throw new SpecimenException("Unsupported type: " + type);
    }

    private > T createCollectionFromConcreteType(final SpecimenType type) {
        try {
            return type.asClass().getDeclaredConstructor().newInstance();
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException |
                 NoSuchMethodException e) {
            throw new SpecimenException("Unable to create collection of type " + type.getName(), e);
        }
    }
}