diff --git a/processor/src/main/java/fr/xebia/extras/selma/codegen/CustomMapperWrapper.java b/processor/src/main/java/fr/xebia/extras/selma/codegen/CustomMapperWrapper.java index b2d7766..f36ce6a 100644 --- a/processor/src/main/java/fr/xebia/extras/selma/codegen/CustomMapperWrapper.java +++ b/processor/src/main/java/fr/xebia/extras/selma/codegen/CustomMapperWrapper.java @@ -115,13 +115,13 @@ void emitCustomMappersFields(JavaWriter writer, boolean assign) throws IOExcepti /** * Adds a custom mapping method to the registry for later use at codegen. - * - * @param method + * @param method * @param immutable + * @param ignoreAbstract */ - private void pushCustomMapper(final TypeElement element, final MethodWrapper method, Boolean immutable) { + private void pushCustomMapper(final TypeElement element, final MethodWrapper method, Boolean immutable, boolean ignoreAbstract) { MappingBuilder res = null; - String customMapperFieldName = buildMapperFieldName(element); + String customMapperFieldName = ignoreAbstract ? "this" : buildMapperFieldName(element); InOutType inOutType = method.inOutType(); String methodCall = String.format("%s.%s", customMapperFieldName, method.getSimpleName()); @@ -163,24 +163,7 @@ private void collectCustomMappers() { final TypeElement element = context.elements.getTypeElement(customMapper.replace(".class", "")); - final List methods = ElementFilter.methodsIn(element.getEnclosedElements()); - final HashMap customInOutTypes = new HashMap(); - for (ExecutableElement method : methods) { - MethodWrapper methodWrapper = new MethodWrapper(method, context); - if (isValidCustomMapping(methodWrapper)) { - - if (methodWrapper.isCustomMapper()) { - pushCustomMapper(element, methodWrapper, null); - addCustomInOutType(customInOutTypes, methodWrapper); - } else { - pushMappingInterceptor(element, methodWrapper); - } - mappingMethodCount++; - } - } - - // Create defaults custom mappers if immutable or mutable is missing - addMissingMappings(customInOutTypes, element); + mappingMethodCount += collectCustomMethods(element, false); if (mappingMethodCount == 0) { @@ -202,12 +185,39 @@ private void collectCustomMappers() { } - private void addMissingMappings(HashMap customInOutTypes, TypeElement element) { + private int collectCustomMethods(TypeElement element, boolean ignoreAbstract) { + int mappingMethodCount = 0; + final List methods = ElementFilter.methodsIn(element.getEnclosedElements()); + final HashMap customInOutTypes = new HashMap(); + for (ExecutableElement method : methods) { + MethodWrapper methodWrapper = new MethodWrapper(method, 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); + } + mappingMethodCount++; + } + } + + // Create defaults custom mappers if immutable or mutable is missing + addMissingMappings(customInOutTypes, element, ignoreAbstract); + return mappingMethodCount; + } + + private void addMissingMappings(HashMap customInOutTypes, TypeElement element, boolean ignoreAbstract) { for (Map.Entry entry : customInOutTypes.entrySet()) { if (entry.getValue().updateGraphMethod == null) { - pushCustomMapper(element, entry.getValue().immutableMethod, Boolean.TRUE); + pushCustomMapper(element, entry.getValue().immutableMethod, Boolean.TRUE, ignoreAbstract); } else if (entry.getValue().immutableMethod == null) { - pushCustomMapper(element, entry.getValue().updateGraphMethod, Boolean.FALSE); + pushCustomMapper(element, entry.getValue().updateGraphMethod, Boolean.FALSE, ignoreAbstract); } } } @@ -313,6 +323,10 @@ public void addFields(List childFields) { } } + public void addMappersElementMethods(TypeElement mapperInterface) { + collectCustomMethods(mapperInterface, true); + } + class CustomMapperKey { final TypeMirror in; final TypeMirror out; diff --git a/processor/src/main/java/fr/xebia/extras/selma/codegen/MapperClassGenerator.java b/processor/src/main/java/fr/xebia/extras/selma/codegen/MapperClassGenerator.java index 91725b4..5c25648 100644 --- a/processor/src/main/java/fr/xebia/extras/selma/codegen/MapperClassGenerator.java +++ b/processor/src/main/java/fr/xebia/extras/selma/codegen/MapperClassGenerator.java @@ -110,11 +110,7 @@ public void build() throws IOException { if (mapper.ioC == IoC.SPRING){ writer.emitAnnotation("org.springframework.stereotype.Service"); } - if (mapper.isFinalMappers()) { - writer.beginType(adapterName, "class", EnumSet.of(PUBLIC, FINAL), null, strippedTypeName); - } else { - writer.beginType(adapterName, "class", EnumSet.of(PUBLIC), null, strippedTypeName); - } + openClassBlock(writer, adapterName, strippedTypeName); writer.emitEmptyLine(); firstMethod = false; } @@ -135,6 +131,21 @@ public void build() throws IOException { mapper.reportUnused(); } + private void openClassBlock(JavaWriter writer, String adapterName, String strippedTypeName) throws IOException { + String[] interfaceName = new String[]{strippedTypeName}; + String className = strippedTypeName; + Set modifiers = EnumSet.of(PUBLIC); + if (mapper.isAbstractClass()){ + interfaceName = new String[]{}; + } else { + className = null; + } + if (mapper.isFinalMappers()) { + modifiers = EnumSet.of(PUBLIC, FINAL); + } + writer.beginType(adapterName, "class", modifiers, className, interfaceName); + } + private void buildConstructor(JavaWriter writer, String adapterName) throws IOException { mapper.emitSourceFields(writer); mapper.emitCustomMappersFields(writer, false); diff --git a/processor/src/main/java/fr/xebia/extras/selma/codegen/MapperProcessor.java b/processor/src/main/java/fr/xebia/extras/selma/codegen/MapperProcessor.java index 661259e..219940b 100644 --- a/processor/src/main/java/fr/xebia/extras/selma/codegen/MapperProcessor.java +++ b/processor/src/main/java/fr/xebia/extras/selma/codegen/MapperProcessor.java @@ -17,6 +17,7 @@ package fr.xebia.extras.selma.codegen; import fr.xebia.extras.selma.Mapper; +import fr.xebia.extras.selma.SelmaConstants; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.ProcessingEnvironment; @@ -69,14 +70,15 @@ public boolean process(Set annotations, RoundEnvironment types = processingEnv.getTypeUtils(); populateAllMappers(roundEnv); - try { - generateMappingClassses(); - } catch (IOException e) { - e.printStackTrace(); - StringWriter writer = new StringWriter(); - e.printStackTrace(new PrintWriter(writer)); - error(writer.toString(), null); - } + try { + generateMappingClassses(); + } catch (IOException e) { + e.printStackTrace(); + StringWriter writer = new StringWriter(); + e.printStackTrace(new PrintWriter(writer)); + error(writer.toString(), null); + } + remainingMapperTypes.clear(); } return res; } @@ -93,7 +95,8 @@ private void generateMappingClassses() throws IOException { private void populateAllMappers(RoundEnvironment roundEnv) { for (Element element : roundEnv.getElementsAnnotatedWith(Mapper.class)) { - if (!isValidMapperUse(element)) { + boolean abstractClass = isAbstractClass(element); + if (isSelmaGenerated(element) || !isValidMapperUse(element)) { continue; } else { TypeElement typeElement = (TypeElement) element; @@ -102,7 +105,10 @@ private void populateAllMappers(RoundEnvironment roundEnv) { for (Element method : methods) { ExecutableElement executableElement = (ExecutableElement) method; - + // We should only process abstract method as mapper to implement + if (abstractClass && !isAbstractMethod(executableElement)) { + continue; + } if (isValidMapperMethod(executableElement)) { putMapper(element, executableElement); } @@ -112,6 +118,14 @@ private void populateAllMappers(RoundEnvironment roundEnv) { } } + private boolean isAbstractMethod(ExecutableElement executableElement) { + return executableElement.getModifiers().contains(Modifier.ABSTRACT); + } + + private boolean isSelmaGenerated(Element element) { + return (""+element.getSimpleName()).endsWith(SelmaConstants.MAPPER_CLASS_SUFFIX); + } + private boolean isValidMapperMethod(ExecutableElement executableElement) { if (exclusions.contains(executableElement)) { @@ -165,14 +179,26 @@ private void putMapper(Element element, ExecutableElement executableElement) { private boolean isValidMapperUse(Element element) { boolean res = true; - if (element.getKind() != ElementKind.INTERFACE) { - error(element, "@Mapper can only be used on interface not on %s", element.getKind()); + if (element.getKind() != ElementKind.INTERFACE && !isAbstractClass(element)) { + error(element, "@Mapper can only be used on interface or public abstract class"); res = false; } return res; } + private boolean isAbstractClass(Element element) { + boolean res = false; + if (element.getKind() == ElementKind.CLASS) { + TypeElement typeElement = (TypeElement) element; + res = typeElement.getModifiers().contains(Modifier.ABSTRACT) && + typeElement.getModifiers().contains(Modifier.PUBLIC) && + !typeElement.getModifiers().contains(Modifier.FINAL); + + } + return res; + } + private void error(String msg, Element element) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, msg, element); } diff --git a/processor/src/main/java/fr/xebia/extras/selma/codegen/MapperWrapper.java b/processor/src/main/java/fr/xebia/extras/selma/codegen/MapperWrapper.java index 67ebaf7..4a325ac 100644 --- a/processor/src/main/java/fr/xebia/extras/selma/codegen/MapperWrapper.java +++ b/processor/src/main/java/fr/xebia/extras/selma/codegen/MapperWrapper.java @@ -22,13 +22,13 @@ import fr.xebia.extras.selma.IoC; import fr.xebia.extras.selma.Mapper; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import java.io.IOException; import java.util.List; -import static fr.xebia.extras.selma.IgnoreMissing.ALL; -import static fr.xebia.extras.selma.IgnoreMissing.DEFAULT; -import static fr.xebia.extras.selma.IgnoreMissing.NONE; +import static fr.xebia.extras.selma.IgnoreMissing.*; /** * Class used to wrap the Mapper Annotation @@ -54,6 +54,7 @@ public class MapperWrapper { private final IgnoreMissing ignoreMissing; final IoC ioC; private final CollectionMappingStrategy collectionMappingStrategy; + private final boolean abstractClass; public MapperWrapper(MapperGeneratorContext context, TypeElement mapperInterface) { this.context = context; @@ -72,7 +73,7 @@ public MapperWrapper(MapperGeneratorContext context, TypeElement mapperInterface IgnoreMissing missing = IgnoreMissing.valueOf(mapper.getAsString(WITH_IGNORE_MISSING)); if (missing == DEFAULT) { - if (configuration.isIgnoreMissingProperties()){ + if (configuration.isIgnoreMissingProperties()) { ignoreMissing = ALL; } else { ignoreMissing = NONE; @@ -91,6 +92,9 @@ public MapperWrapper(MapperGeneratorContext context, TypeElement mapperInterface // Here we collect custom mappers customMappers = new CustomMapperWrapper(mapper, context); mappingRegistry.customMappers(customMappers); + if (mapperInterface.getModifiers().contains(Modifier.ABSTRACT)){ + customMappers.addMappersElementMethods(mapperInterface); + } enumMappers = new EnumMappersWrapper(withEnums(), context, mapperInterface); mappingRegistry.enumMappers(enumMappers); @@ -100,6 +104,8 @@ public MapperWrapper(MapperGeneratorContext context, TypeElement mapperInterface source = new SourceWrapper(mapper, context); + abstractClass = mapperInterface.getModifiers().contains(Modifier.ABSTRACT) && + mapperInterface.getKind() == ElementKind.CLASS; } @@ -166,6 +172,7 @@ public CustomMapperWrapper customMappers() { /** * Method used to collect dependencies from mapping methods that need fields and constructor init + * * @param maps maps annotation we want to collect */ public void collectMaps(MapsWrapper maps) { @@ -185,11 +192,15 @@ public void emitSourceAssigns(JavaWriter writer) throws IOException { source.emitAssigns(writer); } - public IgnoreMissing ignoreMissing(){ + public IgnoreMissing ignoreMissing() { return ignoreMissing; } public boolean allowCollectionGetter() { return collectionMappingStrategy == CollectionMappingStrategy.ALLOW_GETTER; } + + public boolean isAbstractClass() { + return abstractClass; + } } diff --git a/processor/src/main/java/fr/xebia/extras/selma/codegen/MethodWrapper.java b/processor/src/main/java/fr/xebia/extras/selma/codegen/MethodWrapper.java index 48d5888..39ded4b 100644 --- a/processor/src/main/java/fr/xebia/extras/selma/codegen/MethodWrapper.java +++ b/processor/src/main/java/fr/xebia/extras/selma/codegen/MethodWrapper.java @@ -203,4 +203,7 @@ public boolean hasFields() { } + public boolean isAbstract() { + return method.getModifiers().contains(Modifier.ABSTRACT); + } } diff --git a/processor/src/test/java/fr/xebia/extras/selma/it/custom/mapper/AbstractMapperWithCustom.java b/processor/src/test/java/fr/xebia/extras/selma/it/custom/mapper/AbstractMapperWithCustom.java new file mode 100644 index 0000000..ccfd4d2 --- /dev/null +++ b/processor/src/test/java/fr/xebia/extras/selma/it/custom/mapper/AbstractMapperWithCustom.java @@ -0,0 +1,58 @@ +/* + * Copyright 2013 Séven 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.it.custom.mapper; + +import fr.xebia.extras.selma.Mapper; +import fr.xebia.extras.selma.Maps; +import fr.xebia.extras.selma.beans.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +/** + * Created by slemesle on 19/11/14. + */ +@Mapper( + withIgnoreFields = {"fr.xebia.extras.selma.beans.PersonIn.male", "fr.xebia.extras.selma.beans.PersonOut.biography"} +) +public abstract class AbstractMapperWithCustom { + + public static final int NUMBER_INCREMENT = 10000; + + public abstract PersonOut asPersonOut(PersonIn in); + + public abstract CityOut asCityOut(CityIn in); + + /** + * Custom mapper inside the Mapper class + */ + public AddressOut mapAddress(AddressIn addressIn){ + AddressOut res= null; + if (addressIn != null){ + res = new AddressOut(); + res.setCity(this.asCityOut(addressIn.getCity())); + res.setExtras(addressIn.getExtras() == null ? null : new ArrayList(addressIn.getExtras())); + res.setNumber(addressIn.getNumber() + NUMBER_INCREMENT); + res.setPrincipal(addressIn.isPrincipal()); + res.setStreet(addressIn.getStreet()); + } + return res; + } + + +} diff --git a/processor/src/test/java/fr/xebia/extras/selma/it/custom/mapper/AbstractMapperWithCustomIT.java b/processor/src/test/java/fr/xebia/extras/selma/it/custom/mapper/AbstractMapperWithCustomIT.java new file mode 100644 index 0000000..22287ed --- /dev/null +++ b/processor/src/test/java/fr/xebia/extras/selma/it/custom/mapper/AbstractMapperWithCustomIT.java @@ -0,0 +1,64 @@ +/* + * Copyright 2013 Séven 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.it.custom.mapper; + +import fr.xebia.extras.selma.Selma; +import fr.xebia.extras.selma.beans.AddressIn; +import fr.xebia.extras.selma.beans.CityIn; +import fr.xebia.extras.selma.beans.PersonIn; +import fr.xebia.extras.selma.beans.PersonOut; +import fr.xebia.extras.selma.it.utils.Compile; +import fr.xebia.extras.selma.it.utils.IntegrationTestBase; +import junit.framework.Assert; +import org.junit.Test; + +/** + * Created by slemesle on 19/11/14. + */ +@Compile(withClasses = {AbstractMapperWithCustom.class}) +public class AbstractMapperWithCustomIT extends IntegrationTestBase { + + @Test + public void should_map_bean_with_embedded_custom_mapper() throws IllegalAccessException, InstantiationException, ClassNotFoundException { + AbstractMapperWithCustom mapper = Selma.getMapper(AbstractMapperWithCustom.class); + + PersonIn personIn = new PersonIn(); + personIn.setAddress(new AddressIn()); + personIn.getAddress().setCity(new CityIn()); + personIn.getAddress().getCity().setCapital(true); + personIn.getAddress().getCity().setName("Paris"); + personIn.getAddress().getCity().setPopulation(3 * 1000 * 1000); + + personIn.getAddress().setPrincipal(true); + personIn.getAddress().setNumber(55); + personIn.getAddress().setStreet("rue de la truanderie"); + + PersonOut res = mapper.asPersonOut(personIn); + + Assert.assertNotNull(res); + + Assert.assertEquals(personIn.getAddress().getStreet(), res.getAddress().getStreet()); + Assert.assertEquals(personIn.getAddress().getNumber() + AbstractMapperWithCustom.NUMBER_INCREMENT, + res.getAddress().getNumber()); + Assert.assertEquals(personIn.getAddress().getExtras(), res.getAddress().getExtras()); + + Assert.assertEquals(personIn.getAddress().getCity().getName(), res.getAddress().getCity().getName()); + Assert.assertEquals(personIn.getAddress().getCity().getPopulation(), res.getAddress().getCity().getPopulation()); + Assert.assertEquals(personIn.getAddress().getCity().isCapital(), res.getAddress().getCity().isCapital()); + } + +}