fr.xebia.extras.selma.codegen.CustomMapperWrapper.java Source code

Java tutorial

Introduction

Here is the source code for fr.xebia.extras.selma.codegen.CustomMapperWrapper.java

Source

/*
 * Copyright 2013  Sven Le Mesle
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package fr.xebia.extras.selma.codegen;

import com.squareup.javawriter.JavaWriter;
import fr.xebia.extras.selma.IoC;

import javax.lang.model.element.*;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import java.io.IOException;
import java.util.*;

import static javax.lang.model.element.Modifier.*;

/**
 * Created by slemesle on 16/09/2014.
 */
public class CustomMapperWrapper {

    public static final String CUSTOM_MAPPER_FIELD_TPL = "customMapper%s";
    public static final String WITH_CUSTOM = "withCustom";
    private final AnnotationWrapper annotationWrapper;
    private final MapperGeneratorContext context;
    private final HashMap<InOutType, String> unusedCustomMappers;
    private final Map<InOutType, MappingBuilder> registryMap;
    private final Map<InOutType, MappingBuilder> interceptorMap;
    private final List<TypeElement> customMapperFields;
    private final HashMap<InOutType, String> unusedInterceptor;
    private final Element annotatedElement;
    private final IoC ioC;
    private CustomMapperWrapper parent;

    public CustomMapperWrapper(AnnotationWrapper mapperAnnotation, MapperGeneratorContext context) {
        this.annotatedElement = mapperAnnotation.getAnnotatedElement();

        this.annotationWrapper = mapperAnnotation;
        this.context = context;
        this.customMapperFields = new LinkedList<TypeElement>();
        this.unusedCustomMappers = new HashMap();
        this.unusedInterceptor = new HashMap<InOutType, String>();
        this.registryMap = new HashMap<InOutType, MappingBuilder>();
        this.interceptorMap = new HashMap<InOutType, MappingBuilder>();
        ioC = IoC.valueOf((annotationWrapper.getAsString(MapperWrapper.WITH_IOC) != null
                ? annotationWrapper.getAsString(MapperWrapper.WITH_IOC)
                : IoC.NO.toString()));
        collectCustomMappers();
    }

    public CustomMapperWrapper(CustomMapperWrapper parent, AnnotationWrapper annotationWrapper,
            MapperGeneratorContext context) {
        this.parent = parent;
        this.annotationWrapper = annotationWrapper;
        this.customMapperFields = new LinkedList<TypeElement>();
        this.unusedCustomMappers = new HashMap();
        this.unusedInterceptor = new HashMap<InOutType, String>();
        this.registryMap = new HashMap<InOutType, MappingBuilder>();
        this.interceptorMap = new HashMap<InOutType, MappingBuilder>();

        this.context = context;
        this.ioC = parent.ioC;

        if (annotationWrapper != null) {
            this.annotatedElement = annotationWrapper.getAnnotatedElement();
            collectCustomMappers();
        } else {
            annotatedElement = null;
        }
    }

    void emitCustomMappersFields(JavaWriter writer, boolean assign) throws IOException {
        for (TypeElement customMapperField : customMapperFields) {
            final String field = String.format(CUSTOM_MAPPER_FIELD_TPL,
                    customMapperField.getSimpleName().toString());
            if (assign) {
                if (customMapperField.getKind() != ElementKind.INTERFACE
                        && !(customMapperField.getKind() == ElementKind.CLASS
                                && customMapperField.getModifiers().contains(ABSTRACT))) {
                    TypeConstructorWrapper constructorWrapper = new TypeConstructorWrapper(context,
                            customMapperField);
                    // assign the customMapper field to a newly created instance passing to it the declared source params
                    writer.emitStatement("this.%s = new %s(%s)", field,
                            customMapperField.getQualifiedName().toString(),
                            constructorWrapper.hasMatchingSourcesConstructor ? context.newParams() : "");
                }
            } else {
                writer.emitEmptyLine();
                writer.emitJavadoc("This field is used for custom Mapping");
                if (ioC == IoC.SPRING) {
                    writer.emitAnnotation("org.springframework.beans.factory.annotation.Autowired");
                }
                writer.emitField(customMapperField.asType().toString(),
                        String.format(CUSTOM_MAPPER_FIELD_TPL, customMapperField.getSimpleName().toString()),
                        EnumSet.of(PRIVATE));

                writer.emitEmptyLine();
                writer.emitJavadoc("Custom Mapper setter for " + field);

                EnumSet<Modifier> modifiers = EnumSet.of(PUBLIC, FINAL);
                if (ioC == IoC.CDI) {
                    modifiers = EnumSet.of(PUBLIC);
                }
                writer.beginMethod("void", "setCustomMapper" + customMapperField.getSimpleName(), modifiers,
                        customMapperField.asType().toString(), "mapper");
                writer.emitStatement("this.%s = mapper", field);
                writer.endMethod();
                writer.emitEmptyLine();
            }
        }
    }

    /**
     * Adds a custom mapping method to the registry for later use at codegen.
     *
     * @param method
     * @param immutable
     * @param ignoreAbstract
     */
    private void pushCustomMapper(final TypeElement element, final MethodWrapper method, Boolean immutable,
            boolean ignoreAbstract) {
        MappingBuilder res = null;
        String customMapperFieldName = ignoreAbstract ? "this" : buildMapperFieldName(element);

        InOutType inOutType = method.inOutType();
        String methodCall = String.format("%s.%s", customMapperFieldName, method.getSimpleName());

        if (immutable == null) {
            res = MappingBuilder.newCustomMapper(inOutType, methodCall);
            unusedCustomMappers.put(inOutType,
                    String.format("%s.%s", element.getQualifiedName(), method.getSimpleName()));
        } else if (immutable) {
            res = MappingBuilder.newCustomMapper(inOutType, methodCall);
            inOutType = new InOutType(inOutType, true);
        } else if (!immutable) {
            res = MappingBuilder.newCustomMapperImmutableForUpdateGraph(inOutType, methodCall);
            inOutType = new InOutType(inOutType, false);
        }

        if (registryMap.containsKey(inOutType)) {
            MappingBuilder mappingBuilder = registryMap.get(inOutType);
            context.error(method.element(), "Conflicting custom mapper method " + "conflicts with '%s' For '%s' ",
                    mappingBuilder.toString(), inOutType.toString());
        }
        registryMap.put(inOutType, res);
    }

    private void pushMappingInterceptor(TypeElement element, MethodWrapper method, boolean ignoreAbstract) {

        String customMapperFieldName = ignoreAbstract ? "this" : buildMapperFieldName(element);
        InOutType inOutType = method.inOutArgs();
        MappingBuilder res = MappingBuilder.newMappingInterceptor(inOutType,
                String.format("%s.%s", customMapperFieldName, method.getSimpleName()));

        // Push IOType for both mutable and immutable mapping
        interceptorMap.put(inOutType, res);
        interceptorMap.put(new InOutType(inOutType.in(), inOutType.out(), false), res);
        unusedInterceptor.put(inOutType,
                String.format("%s.%s", element.getQualifiedName(), method.getSimpleName()));
    }

    private void collectCustomMappers() {

        List<String> customClasses = annotationWrapper.getAsStrings(WITH_CUSTOM);
        if (customClasses.size() > 0) {
            int mappingMethodCount = 0;

            for (String customMapper : customClasses) {

                final TypeElement element = context.elements
                        .getTypeElement(customMapper.replaceAll("\\.class$", ""));

                mappingMethodCount += collectCustomMethods(element, false);

                if (mappingMethodCount == 0) {
                    context.error(element, "No valid mapping method found in custom selma class %s\\n "
                            + "A custom mapping method is public and returns a type not void, it takes one parameter or"
                            + " more if you specified datasource.", customMapper);
                } else {

                    TypeConstructorWrapper constructorWrapper = new TypeConstructorWrapper(context, element);
                    if (!constructorWrapper.hasDefaultConstructor && element.getKind() != ElementKind.INTERFACE) {
                        context.error(element, "No default public constructor found in custom mapping class %s\\n"
                                + " Please add one", customMapper);
                    }

                    // Here we collect the name of the field to create in the Mapper generated class
                    customMapperFields.add(element);
                }

            }
        }

    }

    private int collectCustomMethods(TypeElement element, boolean ignoreAbstract) {
        int mappingMethodCount = 0;
        final List<ExecutableElement> methods = ElementFilter.methodsIn(element.getEnclosedElements());
        final HashMap<CustomMapperKey, CustomMapperEntry> customInOutTypes = new HashMap<CustomMapperKey, CustomMapperEntry>();
        for (ExecutableElement method : methods) {
            MethodWrapper methodWrapper = new MethodWrapper(method, (DeclaredType) element.asType(), context);
            // We should ignore abstract methods if parsing an abstract mapper class
            if (ignoreAbstract && methodWrapper.isAbstract()) {
                continue;
            }
            if (isValidCustomMapping(methodWrapper)) {

                if (methodWrapper.isCustomMapper()) {
                    pushCustomMapper(element, methodWrapper, null, ignoreAbstract);
                    addCustomInOutType(customInOutTypes, methodWrapper);
                } else {
                    pushMappingInterceptor(element, methodWrapper, ignoreAbstract);
                }
                mappingMethodCount++;
            }
        }

        // Create defaults custom mappers if immutable or mutable is missing
        addMissingMappings(customInOutTypes, element, ignoreAbstract);
        return mappingMethodCount;
    }

    private void addMissingMappings(HashMap<CustomMapperKey, CustomMapperEntry> customInOutTypes,
            TypeElement element, boolean ignoreAbstract) {
        for (Map.Entry<CustomMapperKey, CustomMapperEntry> entry : customInOutTypes.entrySet()) {
            if (entry.getValue().updateGraphMethod == null) {
                pushCustomMapper(element, entry.getValue().immutableMethod, Boolean.TRUE, ignoreAbstract);
            } else if (entry.getValue().immutableMethod == null) {
                pushCustomMapper(element, entry.getValue().updateGraphMethod, Boolean.FALSE, ignoreAbstract);
            }
        }
    }

    private void addCustomInOutType(HashMap<CustomMapperKey, CustomMapperEntry> customInOutTypes,
            MethodWrapper methodWrapper) {
        CustomMapperKey key = new CustomMapperKey(methodWrapper.inOutType());
        CustomMapperEntry entry1 = customInOutTypes.get(key);
        if (entry1 == null) {
            CustomMapperEntry entry = new CustomMapperEntry(methodWrapper.inOutType(), methodWrapper);
            customInOutTypes.put(key, entry);
        } else {
            CustomMapperEntry entry = new CustomMapperEntry(entry1, methodWrapper);
            customInOutTypes.put(key, entry);
        }
    }

    private String buildMapperFieldName(TypeElement element) {
        return String.format(CUSTOM_MAPPER_FIELD_TPL, element.getSimpleName());
    }

    private boolean isValidCustomMapping(MethodWrapper methodWrapper) {
        boolean res = true;

        if (MapperProcessor.exclusions.contains(methodWrapper.getSimpleName())) {
            // We skip excluded methods
            return false;
        }

        if (!methodWrapper.element().getModifiers().contains(javax.lang.model.element.Modifier.PUBLIC)) {
            res = false;
        }

        if (methodWrapper.element().getModifiers().contains(Modifier.STATIC)) {
            res = false;
        }

        if (!methodWrapper.isCustomMapper() && !methodWrapper.isMappingInterceptor()) {
            res = false;
        }

        return res;
    }

    public MappingBuilder getMapper(InOutType inOutType) {
        MappingBuilder res = registryMap.get(inOutType);
        if (res != null) {
            unusedCustomMappers.remove(inOutType);
        } else if (parent != null) {
            res = parent.getMapper(inOutType);
        }

        return res;
    }

    public MappingBuilder getMappingInterceptor(InOutType inOutType) {
        MappingBuilder res = interceptorMap.get(inOutType);
        if (res != null) {
            unusedInterceptor.remove(new InOutType(inOutType, true));
        } else if (parent != null) {
            res = parent.getMappingInterceptor(inOutType);
        }
        return res;
    }

    public void reportUnused() {
        for (String field : unusedCustomMappers.values()) {
            context.warn(annotatedElement, "Custom mapping method \"%s\" is never used", field);
        }
        for (String field : unusedInterceptor.values()) {
            context.warn(annotatedElement, "Custom interceptor method \"%s\" is never used", field);
        }
    }

    public List<TypeElement> mapperFields() {
        return customMapperFields;
    }

    public void addFields(List<TypeElement> childFields) {

        for (TypeElement childField : childFields) {
            boolean found = false;
            for (TypeElement maperField : customMapperFields) {

                if (childField.equals(maperField)) {
                    found = true;
                    break;
                }

            }
            if (!found) {
                customMapperFields.add(childField);
            }
        }
    }

    public String toString() {
        return this.annotationWrapper.toString();
    }

    public void addMappersElementMethods(TypeElement mapperInterface) {
        collectCustomMethods(mapperInterface, true);
    }

    class CustomMapperKey {
        final TypeMirror in;
        final TypeMirror out;

        public CustomMapperKey(InOutType inOutType) {
            in = inOutType.in();
            out = inOutType.out();
        }

        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;

            CustomMapperKey key = (CustomMapperKey) o;

            if (!MapperProcessor.types.isSameType(in, key.in))
                return false;
            if (!MapperProcessor.types.isSameType(out, key.out))
                return false;

            return true;
        }

        @Override
        public int hashCode() {
            // Here we use the string representation of types because hashCode is not consistent bw instances
            int result = ("" + in).hashCode();
            result = 31 * result + ("" + out).hashCode();
            return result;
        }
    }

    class CustomMapperEntry {
        final MethodWrapper updateGraphMethod;
        final MethodWrapper immutableMethod;

        public CustomMapperEntry(InOutType inOutType, MethodWrapper customMethod) {
            if (inOutType.isOutPutAsParam()) {
                this.updateGraphMethod = customMethod;
                this.immutableMethod = null;
            } else {
                this.immutableMethod = customMethod;
                this.updateGraphMethod = null;
            }
        }

        public CustomMapperEntry(CustomMapperEntry entry1, MethodWrapper methodWrapper) {
            if (entry1.immutableMethod != null) {
                immutableMethod = entry1.immutableMethod;
                updateGraphMethod = methodWrapper;
            } else {
                updateGraphMethod = entry1.updateGraphMethod;
                immutableMethod = methodWrapper;
            }
        }
    }
}