diff --git a/docs/antora.yml b/docs/antora.yml index 85403940d63..a67eef1280e 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -33,6 +33,7 @@ asciidoc: quarkus-version: 3.2.2.Final # replace ${quarkus.version} graalvm-version: 22.3.2 # replace ${graalvm.version} graalvm-docs-version: 22.3 + mapstruct-version: 1.5.5.Final # replace ${mapstruct.version} min-maven-version: 3.8.2 # replace ${min-maven-version} target-maven-version: 3.9.3 # replace ${target-maven-version} @@ -52,7 +53,7 @@ asciidoc: link-quarkus-code-generator: code.quarkus.io link-quarkus-cxf-doc: https://quarkiverse.github.io/quarkiverse-docs/quarkus-cxf/dev link-quarkus-cxf-source: https://github.com/quarkiverse/quarkus-cxf/tree/main - + # Misc javaxOrJakartaPackagePrefix: jakarta # this can be switched to javax in older branches diff --git a/docs/modules/ROOT/examples/components/mapstruct.yml b/docs/modules/ROOT/examples/components/mapstruct.yml index 8efead02118..d35707a121e 100644 --- a/docs/modules/ROOT/examples/components/mapstruct.yml +++ b/docs/modules/ROOT/examples/components/mapstruct.yml @@ -2,11 +2,11 @@ # This file was generated by camel-quarkus-maven-plugin:update-extension-doc-page cqArtifactId: camel-quarkus-mapstruct cqArtifactIdBase: mapstruct -cqNativeSupported: false -cqStatus: Preview +cqNativeSupported: true +cqStatus: Stable cqDeprecated: false cqJvmSince: 3.0.0 -cqNativeSince: n/a +cqNativeSince: 3.0.0 cqCamelPartName: mapstruct cqCamelPartTitle: MapStruct cqCamelPartDescription: Type Conversion using Mapstruct diff --git a/docs/modules/ROOT/pages/reference/extensions/mapstruct.adoc b/docs/modules/ROOT/pages/reference/extensions/mapstruct.adoc index 1aa730b7c3e..1e234bacc75 100644 --- a/docs/modules/ROOT/pages/reference/extensions/mapstruct.adoc +++ b/docs/modules/ROOT/pages/reference/extensions/mapstruct.adoc @@ -4,17 +4,17 @@ = MapStruct :linkattrs: :cq-artifact-id: camel-quarkus-mapstruct -:cq-native-supported: false -:cq-status: Preview -:cq-status-deprecation: Preview +:cq-native-supported: true +:cq-status: Stable +:cq-status-deprecation: Stable :cq-description: Type Conversion using Mapstruct :cq-deprecated: false :cq-jvm-since: 3.0.0 -:cq-native-since: n/a +:cq-native-since: 3.0.0 ifeval::[{doc-show-badges} == true] [.badges] -[.badge-key]##JVM since##[.badge-supported]##3.0.0## [.badge-key]##Native##[.badge-unsupported]##unsupported## +[.badge-key]##JVM since##[.badge-supported]##3.0.0## [.badge-key]##Native since##[.badge-supported]##3.0.0## endif::[] Type Conversion using Mapstruct @@ -29,6 +29,10 @@ Please refer to the above link for usage and configuration details. [id="extensions-mapstruct-maven-coordinates"] == Maven coordinates +https://{link-quarkus-code-generator}/?extension-search=camel-quarkus-mapstruct[Create a new project with this extension on {link-quarkus-code-generator}, window="_blank"] + +Or add the coordinates to your existing project: + [source,xml] ---- @@ -39,3 +43,57 @@ Please refer to the above link for usage and configuration details. ifeval::[{doc-show-user-guide-link} == true] Check the xref:user-guide/index.adoc[User guide] for more information about writing Camel Quarkus applications. endif::[] + +[id="extensions-mapstruct-usage"] +== Usage +[id="extensions-mapstruct-usage-annotation-processor"] +=== Annotation Processor + +To use MapStruct, you must configure your build to use an annotation processor. + +[id="extensions-mapstruct-usage-maven"] +==== Maven + +[source,xml] +---- + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.mapstruct + mapstruct-processor + {mapstruct-version} + + + + + +---- + +[id="extensions-mapstruct-usage-gradle"] +==== Gradle + +[source,gradle] +---- +dependencies { + annotationProcessor 'org.mapstruct:mapstruct-processor:{mapstruct-version}' + testAnnotationProcessor 'org.mapstruct:mapstruct-processor:{mapstruct-version}' +} +---- + +[id="extensions-mapstruct-usage-mapper-definition-discovery"] +=== Mapper definition discovery + +By default, {project-name} will automatically discover the package paths of your `@Mapper` annotated interfaces or abstract classes and +pass them to the Camel MapStruct component. + +If you want finer control over the specific packages that are scanned, then you can set a configuration property in `application.properties`. + +[source,properties] +---- +camel.component.mapstruct.mapper-package-name = com.first.package,org.second.package +---- + diff --git a/extensions-jvm/mapstruct/deployment/src/main/java/org/apache/camel/quarkus/component/mapstruct/deployment/MapstructProcessor.java b/extensions-jvm/mapstruct/deployment/src/main/java/org/apache/camel/quarkus/component/mapstruct/deployment/MapstructProcessor.java deleted file mode 100644 index 3bfaa4cefdc..00000000000 --- a/extensions-jvm/mapstruct/deployment/src/main/java/org/apache/camel/quarkus/component/mapstruct/deployment/MapstructProcessor.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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 org.apache.camel.quarkus.component.mapstruct.deployment; - -import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.annotations.ExecutionTime; -import io.quarkus.deployment.annotations.Record; -import io.quarkus.deployment.builditem.FeatureBuildItem; -import io.quarkus.deployment.pkg.steps.NativeBuild; -import org.apache.camel.quarkus.core.JvmOnlyRecorder; -import org.jboss.logging.Logger; - -class MapstructProcessor { - - private static final Logger LOG = Logger.getLogger(MapstructProcessor.class); - private static final String FEATURE = "camel-mapstruct"; - - @BuildStep - FeatureBuildItem feature() { - return new FeatureBuildItem(FEATURE); - } - - /** - * Remove this once this extension starts supporting the native mode. - */ - @BuildStep(onlyIf = NativeBuild.class) - @Record(value = ExecutionTime.RUNTIME_INIT) - void warnJvmInNative(JvmOnlyRecorder recorder) { - JvmOnlyRecorder.warnJvmInNative(LOG, FEATURE); // warn at build time - recorder.warnJvmInNative(FEATURE); // warn at runtime - } -} diff --git a/extensions-jvm/pom.xml b/extensions-jvm/pom.xml index e16ab783eba..ec56a2f55f2 100644 --- a/extensions-jvm/pom.xml +++ b/extensions-jvm/pom.xml @@ -83,7 +83,6 @@ jt400 ldif lucene - mapstruct mvel printer pulsar diff --git a/extensions-jvm/mapstruct/deployment/pom.xml b/extensions/mapstruct/deployment/pom.xml similarity index 100% rename from extensions-jvm/mapstruct/deployment/pom.xml rename to extensions/mapstruct/deployment/pom.xml diff --git a/extensions/mapstruct/deployment/src/main/java/org/apache/camel/quarkus/component/mapstruct/deployment/ConversionMethodInfoRuntimeValuesBuildItem.java b/extensions/mapstruct/deployment/src/main/java/org/apache/camel/quarkus/component/mapstruct/deployment/ConversionMethodInfoRuntimeValuesBuildItem.java new file mode 100644 index 00000000000..31cc899015d --- /dev/null +++ b/extensions/mapstruct/deployment/src/main/java/org/apache/camel/quarkus/component/mapstruct/deployment/ConversionMethodInfoRuntimeValuesBuildItem.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.quarkus.component.mapstruct.deployment; + +import java.util.Set; + +import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.runtime.RuntimeValue; +import org.apache.camel.quarkus.component.mapstruct.ConversionMethodInfo; + +/** + * Holds info about generated TypeConverter ConversionMethod. + */ +public final class ConversionMethodInfoRuntimeValuesBuildItem extends SimpleBuildItem { + private final Set> conversionMethodInfoRuntimeValues; + + public ConversionMethodInfoRuntimeValuesBuildItem( + Set> conversionMethodInfoRuntimeValues) { + this.conversionMethodInfoRuntimeValues = conversionMethodInfoRuntimeValues; + } + + public Set> getConversionMethodInfoRuntimeValues() { + return conversionMethodInfoRuntimeValues; + } +} diff --git a/extensions/mapstruct/deployment/src/main/java/org/apache/camel/quarkus/component/mapstruct/deployment/MapStructMapperPackagesBuildItem.java b/extensions/mapstruct/deployment/src/main/java/org/apache/camel/quarkus/component/mapstruct/deployment/MapStructMapperPackagesBuildItem.java new file mode 100644 index 00000000000..ac77a04dbfc --- /dev/null +++ b/extensions/mapstruct/deployment/src/main/java/org/apache/camel/quarkus/component/mapstruct/deployment/MapStructMapperPackagesBuildItem.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.quarkus.component.mapstruct.deployment; + +import java.util.Set; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * Holds the set of discovered MapStruct Mapper packages. + */ +public final class MapStructMapperPackagesBuildItem extends SimpleBuildItem { + private final Set mapperPackages; + + public MapStructMapperPackagesBuildItem(Set mapperPackages) { + this.mapperPackages = mapperPackages; + } + + public Set getMapperPackages() { + return mapperPackages; + } +} diff --git a/extensions/mapstruct/deployment/src/main/java/org/apache/camel/quarkus/component/mapstruct/deployment/MapStructProcessor.java b/extensions/mapstruct/deployment/src/main/java/org/apache/camel/quarkus/component/mapstruct/deployment/MapStructProcessor.java new file mode 100644 index 00000000000..d60b75d3c19 --- /dev/null +++ b/extensions/mapstruct/deployment/src/main/java/org/apache/camel/quarkus/component/mapstruct/deployment/MapStructProcessor.java @@ -0,0 +1,374 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.quarkus.component.mapstruct.deployment; + +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; + +import io.quarkus.arc.deployment.BeanContainerBuildItem; +import io.quarkus.arc.deployment.GeneratedBeanBuildItem; +import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; +import io.quarkus.arc.deployment.UnremovableBeanBuildItem; +import io.quarkus.deployment.GeneratedClassGizmoAdaptor; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.GeneratedClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; +import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.ClassOutput; +import io.quarkus.gizmo.FieldCreator; +import io.quarkus.gizmo.FieldDescriptor; +import io.quarkus.gizmo.MethodCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.runtime.RuntimeValue; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; +import org.apache.camel.Exchange; +import org.apache.camel.component.mapstruct.MapstructComponent; +import org.apache.camel.quarkus.component.mapstruct.ConversionMethodInfo; +import org.apache.camel.quarkus.component.mapstruct.MapStructRecorder; +import org.apache.camel.quarkus.core.deployment.spi.CamelBeanBuildItem; +import org.apache.camel.quarkus.core.deployment.spi.CamelTypeConverterRegistryBuildItem; +import org.apache.camel.support.SimpleTypeConverter.ConversionMethod; +import org.apache.camel.util.ObjectHelper; +import org.apache.camel.util.ReflectionHelper; +import org.apache.camel.util.StringHelper; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.microprofile.config.ConfigProvider; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.IndexView; +import org.mapstruct.Mapper; + +class MapStructProcessor { + + private static final String FEATURE = "camel-mapstruct"; + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } + + @BuildStep + MapStructMapperPackagesBuildItem getMapperPackages(CombinedIndexBuildItem combinedIndex) { + final Set mapperPackages = new HashSet<>(); + + Optional mapperPackageName = ConfigProvider.getConfig() + .getOptionalValue("camel.component.mapstruct.mapper-package-name", String.class); + + if (mapperPackageName.isPresent()) { + String packages = StringUtils.deleteWhitespace(mapperPackageName.get()); + mapperPackages.addAll(Arrays.asList(packages.split(","))); + } else { + // Fallback on auto discovery + combinedIndex.getIndex() + .getAnnotations(Mapper.class) + .stream() + .map(AnnotationInstance::target) + .map(AnnotationTarget::asClass) + .map(ClassInfo::name) + .map(DotName::packagePrefix) + .forEach(mapperPackages::add); + } + + return new MapStructMapperPackagesBuildItem(mapperPackages); + } + + @Record(ExecutionTime.STATIC_INIT) + @BuildStep + CamelBeanBuildItem mapStructComponentBean( + MapStructMapperPackagesBuildItem mapperPackages, + ConversionMethodInfoRuntimeValuesBuildItem conversionMethodInfos, + MapStructRecorder recorder) { + return new CamelBeanBuildItem("mapstruct", MapstructComponent.class.getName(), + recorder.createMapStructComponent(mapperPackages.getMapperPackages(), + conversionMethodInfos.getConversionMethodInfoRuntimeValues())); + } + + @Record(ExecutionTime.STATIC_INIT) + @BuildStep + void generateMapStructTypeConverters( + BuildProducer generatedBean, + BuildProducer generatedClass, + BuildProducer unremovableBean, + BuildProducer reflectiveClass, + BuildProducer conversionMethodInfos, + CombinedIndexBuildItem combinedIndex, + MapStructMapperPackagesBuildItem mapperPackages, + MapStructRecorder recorder) { + + // The logic that follows mimics dynamic TypeConverter logic in DefaultMapStructFinder.discoverMappings + Set packages = mapperPackages.getMapperPackages(); + AtomicInteger methodCount = new AtomicInteger(); + Map> conversionMethods = new HashMap<>(); + IndexView index = combinedIndex.getIndex(); + + // Find implementations of Mapper annotated interfaces or abstract classes + index.getAnnotations(Mapper.class) + .stream() + .map(AnnotationInstance::target) + .map(AnnotationTarget::asClass) + .filter(classInfo -> packages.contains(classInfo.name().packagePrefix())) + .filter(classInfo -> classInfo.isInterface() || Modifier.isAbstract(classInfo.flags())) + .flatMap(classInfo -> Stream.concat(index.getAllKnownImplementors(classInfo.name()).stream(), + index.getAllKnownSubclasses(classInfo.name()).stream())) + .forEach(classInfo -> { + AtomicReference> mapperRuntimeValue = new AtomicReference<>(); + String mapperClassName = classInfo.name().toString(); + String mapperDefinitionClassName = getMapperDefinitionClassName(classInfo); + if (ObjectHelper.isEmpty(mapperDefinitionClassName)) { + return; + } + + // Check if there's a static instance field defined for the Mapper + ClassInfo mapperDefinitionClassInfo = index.getClassByName(mapperDefinitionClassName); + Optional mapperInstanceField = mapperDefinitionClassInfo + .fields() + .stream() + .filter(fieldInfo -> Modifier.isStatic(fieldInfo.flags())) + .filter(fieldInfo -> fieldInfo.type().name().toString().equals(mapperDefinitionClassName)) + .findFirst(); + + // Check of the Mapper is a CDI bean with one of the supported MapStruct annotations + boolean mapperBeanExists = classInfo.hasDeclaredAnnotation(ApplicationScoped.class) + || classInfo.hasDeclaredAnnotation(Named.class); + if (mapperInstanceField.isEmpty()) { + if (mapperBeanExists) { + unremovableBean + .produce(new UnremovableBeanBuildItem(beanInfo -> beanInfo.hasType(classInfo.name()))); + } else { + // Create the Mapper ourselves + mapperRuntimeValue.set(recorder.createMapper(mapperClassName)); + } + } + + /* + * Generate SimpleTypeConverter.ConversionMethod implementations for each candidate Mapper method. + * + * ReflectionHelper is used to resolve the mapper methods for simplicity, compared to Jandex where + * we potentially have to iterate over the type hierarchy (E.g for multiple interfaces, + * interface / class inheritance etc). + * + * public final class FooConversionMethod implements ConversionMethod { + * private final FooMapperImpl mapper; + * + * // Generated only if a Mapper instance is a CDI bean + * public FooConversionMethod() { + * } + * + * // Generated only if a Mapper instance was declared on the Mapper interface + * public FooConversionMethod() { + * this(CarMapper.INSTANCE); + * } + * + * public FooConversionMethod(FooMapperImpl mapper) { + * this.mapper = mapper; + * } + * + * @Override + * public Object doConvert(Class type, Exchange exchange, Object value) throws Exception { + * return mapper.stringToInt(value); + * } + * } + */ + ReflectionHelper.doWithMethods(resolveClass(mapperDefinitionClassName), method -> { + Class[] parameterTypes = method.getParameterTypes(); + if (method.getParameterCount() != 1) { + return; + } + + Class fromType = parameterTypes[0]; + Class toType = method.getReturnType(); + if (toType.isPrimitive()) { + return; + } + + String conversionMethodClassName = String.format("%s.%s%dConversionMethod", + classInfo.name().packagePrefixName().toString(), + StringHelper.capitalize(method.getName()), + methodCount.incrementAndGet()); + + ClassOutput output = mapperBeanExists ? new GeneratedBeanGizmoAdaptor(generatedBean) + : new GeneratedClassGizmoAdaptor(generatedClass, true); + + try (ClassCreator classCreator = ClassCreator.builder() + .className(conversionMethodClassName) + .classOutput(output) + .setFinal(true) + .interfaces(ConversionMethod.class) + .superClass(Object.class.getName()) + .build()) { + + // Take advantage of CDI and use injection to get the Mapper instance + if (mapperBeanExists) { + classCreator.addAnnotation(Singleton.class); + unremovableBean.produce(new UnremovableBeanBuildItem( + beanInfo -> beanInfo.hasType(DotName.createSimple(conversionMethodClassName)))); + } + + FieldCreator mapperField = classCreator.getFieldCreator("mapper", mapperClassName) + .setModifiers(Modifier.PRIVATE | Modifier.FINAL); + + if (mapperInstanceField.isPresent() || mapperBeanExists) { + // Create a no-args constructor + try (MethodCreator initMethod = classCreator.getMethodCreator("", void.class)) { + initMethod.setModifiers(Modifier.PUBLIC); + if (mapperBeanExists) { + initMethod.invokeSpecialMethod(MethodDescriptor.ofConstructor(Object.class), + initMethod.getThis()); + } else { + // If we don't have CDI injection, get the Mapper instance from the Mapper interface + FieldInfo fieldInfo = mapperInstanceField.get(); + initMethod.invokeSpecialMethod( + MethodDescriptor.ofConstructor(conversionMethodClassName, + mapperClassName), + initMethod.getThis(), + initMethod.readStaticField(FieldDescriptor.of(mapperClassName, + fieldInfo.name(), fieldInfo.type().toString()))); + } + initMethod.returnNull(); + } + } + + try (MethodCreator initMethod = classCreator.getMethodCreator("", void.class, + mapperClassName)) { + initMethod.setModifiers(Modifier.PUBLIC); + if (mapperBeanExists) { + initMethod.addAnnotation(Inject.class); + } + initMethod.invokeSpecialMethod(MethodDescriptor.ofConstructor(Object.class), + initMethod.getThis()); + initMethod.writeInstanceField(mapperField.getFieldDescriptor(), initMethod.getThis(), + initMethod.getMethodParam(0)); + initMethod.returnNull(); + } + + // doConvert implementation + try (MethodCreator doConvertMethod = classCreator.getMethodCreator("doConvert", + Object.class, Class.class, Exchange.class, Object.class)) { + doConvertMethod.setModifiers(Modifier.PUBLIC); + + MethodDescriptor mapperMethod = MethodDescriptor.ofMethod(mapperClassName, + method.getName(), toType.getName(), fromType.getName()); + + ResultHandle mapper = doConvertMethod + .readInstanceField(mapperField.getFieldDescriptor(), doConvertMethod.getThis()); + + // Invoke the target method on the Mapper with the 'value' method arg from convertTo + ResultHandle mapperResult = doConvertMethod.invokeVirtualMethod(mapperMethod, + mapper, doConvertMethod.getMethodParam(2)); + + doConvertMethod.returnValue(mapperResult); + } + } + + // Register the 'to' type for reflection (See MapstructEndpoint.doBuild()) + reflectiveClass.produce(ReflectiveClassBuildItem.builder(toType).build()); + + // Instantiate the generated ConversionMethod + String key = String.format("%s:%s", fromType.getName(), toType.getName()); + conversionMethods.computeIfAbsent(key, + conversionMethodsKey -> recorder.createConversionMethodInfo(fromType, toType, + mapperBeanExists, + mapperRuntimeValue.get(), conversionMethodClassName)); + }); + }); + + conversionMethodInfos + .produce(new ConversionMethodInfoRuntimeValuesBuildItem(new HashSet<>(conversionMethods.values()))); + } + + @Record(ExecutionTime.STATIC_INIT) + @BuildStep + void registerTypeConverters( + BeanContainerBuildItem beanContainer, + CamelTypeConverterRegistryBuildItem typeConverterRegistry, + ConversionMethodInfoRuntimeValuesBuildItem conversionMethodInfos, + MapStructRecorder recorder) { + // Register the TypeConverter leveraging the generated ConversionMethod + recorder.registerMapStructTypeConverters(typeConverterRegistry.getRegistry(), + conversionMethodInfos.getConversionMethodInfoRuntimeValues(), beanContainer.getValue()); + } + + @BuildStep + void registerMapperServiceProviders( + BuildProducer serviceProvider, + CombinedIndexBuildItem combinedIndex, + MapStructMapperPackagesBuildItem mapperPackages) { + // If a Mapper definition uses a custom implementationName then they may get loaded via the ServiceLoader + Set packages = mapperPackages.getMapperPackages(); + combinedIndex.getIndex() + .getAnnotations(Mapper.class) + .stream() + .filter(annotationInstance -> packages.contains(annotationInstance.target().asClass().name().packagePrefix())) + .forEach(annotation -> { + AnnotationValue value = annotation.value("implementationName"); + if (value != null) { + DotName name = annotation.target().asClass().name(); + AnnotationValue implementationPackage = annotation.value("implementationPackage"); + String packageName = implementationPackage != null ? implementationPackage.asString() + : name.packagePrefix(); + serviceProvider + .produce(new ServiceProviderBuildItem(name.toString(), packageName + "." + value.asString())); + } + }); + } + + /** + * Gets the name of the Mapper interface or abstract class + */ + private String getMapperDefinitionClassName(ClassInfo mapStructMapperImpl) { + List interfaces = mapStructMapperImpl.interfaceNames(); + if (interfaces.isEmpty()) { + String superClassName = mapStructMapperImpl.superClassType().name().toString(); + if (!superClassName.equals(Object.class.getName())) { + return superClassName; + } + return null; + } + return interfaces.get(0).toString(); + } + + private Class resolveClass(String className) { + try { + return Class.forName(className, false, Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } +} diff --git a/extensions-jvm/mapstruct/pom.xml b/extensions/mapstruct/pom.xml similarity index 96% rename from extensions-jvm/mapstruct/pom.xml rename to extensions/mapstruct/pom.xml index 023d4eca551..4f14eb57665 100644 --- a/extensions-jvm/mapstruct/pom.xml +++ b/extensions/mapstruct/pom.xml @@ -23,7 +23,7 @@ 4.0.0 org.apache.camel.quarkus - camel-quarkus-extensions-jvm + camel-quarkus-extensions 3.0.0-SNAPSHOT ../pom.xml diff --git a/extensions-jvm/mapstruct/runtime/pom.xml b/extensions/mapstruct/runtime/pom.xml similarity index 98% rename from extensions-jvm/mapstruct/runtime/pom.xml rename to extensions/mapstruct/runtime/pom.xml index fd1b388885f..171f1686402 100644 --- a/extensions-jvm/mapstruct/runtime/pom.xml +++ b/extensions/mapstruct/runtime/pom.xml @@ -34,6 +34,7 @@ 3.0.0 + 3.0.0 diff --git a/extensions/mapstruct/runtime/src/main/doc/usage.adoc b/extensions/mapstruct/runtime/src/main/doc/usage.adoc new file mode 100644 index 00000000000..dad335f4c90 --- /dev/null +++ b/extensions/mapstruct/runtime/src/main/doc/usage.adoc @@ -0,0 +1,46 @@ +=== Annotation Processor + +To use MapStruct, you must configure your build to use an annotation processor. + +==== Maven + +[source,xml] +---- + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.mapstruct + mapstruct-processor + {mapstruct-version} + + + + + +---- + +==== Gradle + +[source,gradle] +---- +dependencies { + annotationProcessor 'org.mapstruct:mapstruct-processor:{mapstruct-version}' + testAnnotationProcessor 'org.mapstruct:mapstruct-processor:{mapstruct-version}' +} +---- + +=== Mapper definition discovery + +By default, {project-name} will automatically discover the package paths of your `@Mapper` annotated interfaces or abstract classes and +pass them to the Camel MapStruct component. + +If you want finer control over the specific packages that are scanned, then you can set a configuration property in `application.properties`. + +[source,properties] +---- +camel.component.mapstruct.mapper-package-name = com.first.package,org.second.package +---- diff --git a/extensions/mapstruct/runtime/src/main/java/org/apache/camel/quarkus/component/mapstruct/CamelQuarkusMapStructMapperFinder.java b/extensions/mapstruct/runtime/src/main/java/org/apache/camel/quarkus/component/mapstruct/CamelQuarkusMapStructMapperFinder.java new file mode 100644 index 00000000000..e1914f4c513 --- /dev/null +++ b/extensions/mapstruct/runtime/src/main/java/org/apache/camel/quarkus/component/mapstruct/CamelQuarkusMapStructMapperFinder.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.quarkus.component.mapstruct; + +import org.apache.camel.component.mapstruct.MapStructMapperFinder; +import org.apache.camel.support.service.ServiceSupport; +import org.apache.camel.util.ObjectHelper; +import org.jboss.logging.Logger; + +/** + * Custom {@link MapStructMapperFinder} that is effectively a noop implementation, as the work of discovering + * mappings is done at build time. + */ +public class CamelQuarkusMapStructMapperFinder extends ServiceSupport implements MapStructMapperFinder { + private static final Logger LOG = Logger.getLogger(CamelQuarkusMapStructMapperFinder.class); + + private final int mappingsCount; + private String mapperPackageName; + + public CamelQuarkusMapStructMapperFinder(String mapperPackageName, int mappingsCount) { + setMapperPackageName(mapperPackageName); + this.mappingsCount = mappingsCount; + } + + @Override + public void setMapperPackageName(String mapperPackageName) { + this.mapperPackageName = mapperPackageName; + } + + @Override + public String getMapperPackageName() { + return this.mapperPackageName; + } + + @Override + public int discoverMappings(Class clazz) { + // Discovery is done at build time so just return the count + return mappingsCount; + } + + @Override + protected void doInit() throws Exception { + if (ObjectHelper.isNotEmpty(mapperPackageName)) { + LOG.infof("Discovered %d MapStruct type converters during build time augmentation: %s", mappingsCount, + mapperPackageName); + } + } +} diff --git a/extensions/mapstruct/runtime/src/main/java/org/apache/camel/quarkus/component/mapstruct/ConversionMethodInfo.java b/extensions/mapstruct/runtime/src/main/java/org/apache/camel/quarkus/component/mapstruct/ConversionMethodInfo.java new file mode 100644 index 00000000000..dc6d6bb16c1 --- /dev/null +++ b/extensions/mapstruct/runtime/src/main/java/org/apache/camel/quarkus/component/mapstruct/ConversionMethodInfo.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.quarkus.component.mapstruct; + +import java.util.Objects; + +import io.quarkus.runtime.RuntimeValue; + +/** + * Holds configuration for dynamic TypeConverter instantiation and registration. + */ +public class ConversionMethodInfo { + private final Class fromClass; + private final Class toClass; + private final boolean cdiBean; + private final RuntimeValue mapper; + private final String conversionMethodClassName; + + public ConversionMethodInfo( + Class fromClass, + Class toClass, + boolean cdiBean, + RuntimeValue mapper, + String conversionMethodClassName) { + this.fromClass = fromClass; + this.toClass = toClass; + this.cdiBean = cdiBean; + this.mapper = mapper; + this.conversionMethodClassName = conversionMethodClassName; + } + + public Class getFromClass() { + return fromClass; + } + + public Class getToClass() { + return toClass; + } + + public boolean isCdiBean() { + return cdiBean; + } + + public RuntimeValue getMapper() { + return mapper; + } + + public String getConversionMethodClassName() { + return conversionMethodClassName; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ConversionMethodInfo that = (ConversionMethodInfo) o; + return cdiBean == that.cdiBean && Objects.equals(fromClass, that.fromClass) && Objects.equals(toClass, that.toClass) + && Objects.equals(mapper, that.mapper) + && Objects.equals(conversionMethodClassName, that.conversionMethodClassName); + } + + @Override + public int hashCode() { + return Objects.hash(fromClass, toClass, cdiBean, mapper, conversionMethodClassName); + } +} diff --git a/extensions/mapstruct/runtime/src/main/java/org/apache/camel/quarkus/component/mapstruct/MapStructRecorder.java b/extensions/mapstruct/runtime/src/main/java/org/apache/camel/quarkus/component/mapstruct/MapStructRecorder.java new file mode 100644 index 00000000000..fab7e73e96e --- /dev/null +++ b/extensions/mapstruct/runtime/src/main/java/org/apache/camel/quarkus/component/mapstruct/MapStructRecorder.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.quarkus.component.mapstruct; + +import java.lang.reflect.InvocationTargetException; +import java.util.Set; + +import io.quarkus.arc.runtime.BeanContainer; +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; +import org.apache.camel.component.mapstruct.MapstructComponent; +import org.apache.camel.spi.TypeConverterRegistry; +import org.apache.camel.support.SimpleTypeConverter; +import org.apache.camel.support.SimpleTypeConverter.ConversionMethod; + +@Recorder +public class MapStructRecorder { + + public RuntimeValue createMapper(String mapperClassName) { + try { + Object mapper = Thread.currentThread() + .getContextClassLoader() + .loadClass(mapperClassName) + .getDeclaredConstructor() + .newInstance(); + return new RuntimeValue<>(mapper); + } catch (InstantiationException | InvocationTargetException | NoSuchMethodException | ClassNotFoundException + | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + public RuntimeValue createConversionMethodInfo( + Class from, + Class to, + boolean cdiBean, + RuntimeValue mapper, + String conversionMethodClassName) { + return new RuntimeValue<>( + new ConversionMethodInfo(from, to, cdiBean, mapper, conversionMethodClassName)); + } + + public RuntimeValue createMapStructComponent( + Set mapperPackages, + Set> conversionMethodInfos) { + String packages = String.join(",", mapperPackages); + CamelQuarkusMapStructMapperFinder finder = new CamelQuarkusMapStructMapperFinder(packages, + conversionMethodInfos.size()); + MapstructComponent component = new MapstructComponent(); + component.setMapperPackageName(packages); + component.setMapStructConverter(finder); + return new RuntimeValue<>(component); + } + + public void registerMapStructTypeConverters( + RuntimeValue typeConverterRegistryRuntimeValue, + Set> conversionMethods, + BeanContainer container) { + TypeConverterRegistry registry = typeConverterRegistryRuntimeValue.getValue(); + conversionMethods.forEach(c -> { + try { + ConversionMethodInfo info = c.getValue(); + + // Create the ConversionMethod for the SimpleTypeConverter + Object conversionMethod; + Class conversionMethodClass = Thread.currentThread().getContextClassLoader() + .loadClass(info.getConversionMethodClassName()); + + if (info.isCdiBean()) { + conversionMethod = container.beanInstance(conversionMethodClass); + } else if (info.getMapper() != null) { + // Pass the Mapper instance created at build time + Object mapper = info.getMapper().getValue(); + conversionMethod = conversionMethodClass.getDeclaredConstructor(mapper.getClass()).newInstance(mapper); + } else { + // Default no-args constructor uses a Mapper instance declared in the Mapper interface + conversionMethod = conversionMethodClass.getDeclaredConstructor().newInstance(); + } + + registry.addTypeConverter(info.getToClass(), info.getFromClass(), + new SimpleTypeConverter(false, (ConversionMethod) conversionMethod)); + } catch (ClassNotFoundException | InvocationTargetException | InstantiationException | IllegalAccessException + | NoSuchMethodException e) { + throw new RuntimeException(e); + } + }); + } +} diff --git a/extensions-jvm/mapstruct/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/mapstruct/runtime/src/main/resources/META-INF/quarkus-extension.yaml similarity index 100% rename from extensions-jvm/mapstruct/runtime/src/main/resources/META-INF/quarkus-extension.yaml rename to extensions/mapstruct/runtime/src/main/resources/META-INF/quarkus-extension.yaml diff --git a/extensions/pom.xml b/extensions/pom.xml index bf80304b2ed..c8262d67047 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -169,6 +169,7 @@ lzf mail management + mapstruct master micrometer microprofile-fault-tolerance diff --git a/integration-tests-jvm/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructResource.java b/integration-tests-jvm/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructResource.java deleted file mode 100644 index 7bad4fdf110..00000000000 --- a/integration-tests-jvm/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructResource.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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 org.apache.camel.quarkus.component.mapstruct.it; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.Response; -import org.apache.camel.ProducerTemplate; -import org.apache.camel.quarkus.component.mapstruct.it.model.Car; -import org.apache.camel.quarkus.component.mapstruct.it.model.Vehicle; - -@Path("/mapstruct") -@ApplicationScoped -public class MapStructResource { - @Inject - ProducerTemplate producerTemplate; - - @Path("/component") - @POST - @Consumes("text/plain") - @Produces("text/plain") - public Response componentTest(String vehicleString) { - return Response.ok(testMapping("component", vehicleString)).build(); - } - - @Path("/converter") - @POST - @Consumes("text/plain") - @Produces("text/plain") - public Response converterTest(String vehicleString) { - return Response.ok(testMapping("converter", vehicleString)).build(); - } - - private String testMapping(String endpoint, String vehicleString) { - return producerTemplate.requestBody("direct:" + endpoint, Vehicle.fromString(vehicleString), Car.class).toString(); - } -} diff --git a/integration-tests-jvm/mapstruct/src/test/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructTest.java b/integration-tests-jvm/mapstruct/src/test/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructTest.java deleted file mode 100644 index c748bbe4351..00000000000 --- a/integration-tests-jvm/mapstruct/src/test/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructTest.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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 org.apache.camel.quarkus.component.mapstruct.it; - -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; -import org.apache.camel.quarkus.component.mapstruct.it.model.Car; -import org.apache.camel.quarkus.component.mapstruct.it.model.Vehicle; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -@QuarkusTest -public class MapStructTest { - private static final Vehicle VEHICLE = new Vehicle("Volvo", "XC60", "true", 2021); - - @ParameterizedTest - @ValueSource(strings = { "component", "converter" }) - public void testMapping(String value) { - String response = RestAssured.given() - .body(VEHICLE.toString()) - .post("/mapstruct/" + value) - .then() - .statusCode(200) - .extract().body().asString(); - - Car car = Car.fromString(response); - - assertEquals(car.getBrand(), VEHICLE.getCompany()); - assertEquals(car.getModel(), VEHICLE.getName()); - assertEquals(car.getYear(), VEHICLE.getYear()); - assertEquals(car.isElectric(), Boolean.parseBoolean(VEHICLE.getPower())); - } -} diff --git a/integration-tests-jvm/pom.xml b/integration-tests-jvm/pom.xml index 1ba33ea06db..f512624870f 100644 --- a/integration-tests-jvm/pom.xml +++ b/integration-tests-jvm/pom.xml @@ -82,7 +82,6 @@ jt400 ldif lucene - mapstruct mvel printer pulsar diff --git a/integration-tests-jvm/mapstruct/pom.xml b/integration-tests/mapstruct/pom.xml similarity index 83% rename from integration-tests-jvm/mapstruct/pom.xml rename to integration-tests/mapstruct/pom.xml index 0b23d5437e2..b84bb2e1f81 100644 --- a/integration-tests-jvm/mapstruct/pom.xml +++ b/integration-tests/mapstruct/pom.xml @@ -60,6 +60,24 @@ + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + + + + + virtualDependencies @@ -98,23 +116,32 @@ + + native + + + native + + + + native + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + + + + - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - org.mapstruct - mapstruct-processor - ${mapstruct.version} - - - - - - diff --git a/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructResource.java b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructResource.java new file mode 100644 index 00000000000..8d7971bb8bc --- /dev/null +++ b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructResource.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.quarkus.component.mapstruct.it; + +import java.util.Objects; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.apache.camel.CamelContext; +import org.apache.camel.ProducerTemplate; +import org.apache.camel.component.mapstruct.MapStructMapperFinder; +import org.apache.camel.component.mapstruct.MapstructComponent; +import org.apache.camel.quarkus.component.mapstruct.it.model.ModelFactory; +import org.apache.camel.util.StringHelper; +import org.jboss.logging.Logger; + +@Path("/mapstruct") +@ApplicationScoped +public class MapStructResource { + private static final Logger LOG = Logger.getLogger(MapStructResource.class); + + @Inject + ProducerTemplate producerTemplate; + + @Inject + CamelContext context; + + @Path("/component") + @POST + @Consumes("text/plain") + @Produces("text/plain") + public Response componentTest( + @QueryParam("fromTypeName") String fromTypeName, + @QueryParam("toTypeName") String toTypeName, + String pojoString) { + try { + String result = doMapping(fromTypeName, toTypeName, "component", pojoString); + return Response.ok(result).build(); + } catch (Exception e) { + LOG.error("Error occurred during mapping", e); + String message = e.getCause() != null ? e.getCause().getMessage() : e.getMessage(); + return Response.serverError().entity(message).build(); + } + } + + @Path("/converter") + @POST + @Consumes("text/plain") + @Produces("text/plain") + public Response converterTest( + @QueryParam("fromTypeName") String fromTypeName, + @QueryParam("toTypeName") String toTypeName, + String pojoString) { + try { + String result = doMapping(fromTypeName, toTypeName, "converter", pojoString); + return Response.ok(result).build(); + } catch (Exception e) { + LOG.error("Error occurred during mapping", e); + String message = e.getCause() != null ? e.getCause().getMessage() : e.getMessage(); + return Response.serverError().entity(message).build(); + } + } + + @Path("/finder/mapper") + @GET + @Produces(MediaType.TEXT_PLAIN) + public String mapStructMapperFinderImpl() { + MapstructComponent component = context.getComponent("mapstruct", MapstructComponent.class); + MapStructMapperFinder mapStructConverter = component.getMapStructConverter(); + Objects.requireNonNull(mapStructConverter, "mapStructConverter should not be null"); + return mapStructConverter.getClass().getName(); + } + + @Path("/component/packages") + @GET + @Produces(MediaType.TEXT_PLAIN) + public String mapStructComponentPackages() { + MapstructComponent component = context.getComponent("mapstruct", MapstructComponent.class); + return component.getMapperPackageName(); + } + + private String doMapping(String fromType, String toType, String endpoint, String pojoString) { + Object pojo = ModelFactory.getModel(pojoString, fromType); + String toTypeHeader = endpoint.equals("component") ? toType : StringHelper.afterLast(toType.toLowerCase(), "."); + return producerTemplate.requestBodyAndHeader("direct:" + endpoint, pojo, "toType", toTypeHeader).toString(); + } +} diff --git a/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructRoutes.java b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructRoutes.java new file mode 100644 index 00000000000..622ab3c207a --- /dev/null +++ b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructRoutes.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.quarkus.component.mapstruct.it; + +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.quarkus.component.mapstruct.it.model.Bike; +import org.apache.camel.quarkus.component.mapstruct.it.model.Car; +import org.apache.camel.quarkus.component.mapstruct.it.model.CarDto; +import org.apache.camel.quarkus.component.mapstruct.it.model.Cat; +import org.apache.camel.quarkus.component.mapstruct.it.model.Dog; +import org.apache.camel.quarkus.component.mapstruct.it.model.Employee; +import org.apache.camel.quarkus.component.mapstruct.it.model.EmployeeDto; +import org.apache.camel.quarkus.component.mapstruct.it.model.Vehicle; + +public class MapStructRoutes extends RouteBuilder { + @Override + public void configure() throws Exception { + from("direct:component") + .toD("mapstruct:${header.toType}"); + + from("direct:converter") + .choice() + .when(simple("${header.toType} == 'bike'")) + .convertBodyTo(Bike.class) + .when(simple("${header.toType} == 'car'")) + .convertBodyTo(Car.class) + .when(simple("${header.toType} == 'cardto'")) + .convertBodyTo(CarDto.class) + .when(simple("${header.toType} == 'cat'")) + .convertBodyTo(Cat.class) + .when(simple("${header.toType} == 'dog'")) + .convertBodyTo(Dog.class) + .when(simple("${header.toType} == 'employee'")) + .convertBodyTo(Employee.class) + .when(simple("${header.toType} == 'employeedto'")) + .convertBodyTo(EmployeeDto.class) + .when(simple("${header.toType} == 'vehicle'")) + .convertBodyTo(Vehicle.class); + } +} diff --git a/integration-tests-jvm/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/mapper/CarMapper.java b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/mapper/car/CarMapper.java similarity index 79% rename from integration-tests-jvm/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/mapper/CarMapper.java rename to integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/mapper/car/CarMapper.java index 0a7f236df2f..d372532cf37 100644 --- a/integration-tests-jvm/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/mapper/CarMapper.java +++ b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/mapper/car/CarMapper.java @@ -14,8 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.camel.quarkus.component.mapstruct.it.mapper; +package org.apache.camel.quarkus.component.mapstruct.it.mapper.car; +import org.apache.camel.quarkus.component.mapstruct.it.model.Bike; import org.apache.camel.quarkus.component.mapstruct.it.model.Car; import org.apache.camel.quarkus.component.mapstruct.it.model.Vehicle; import org.mapstruct.Mapper; @@ -27,4 +28,9 @@ public interface CarMapper { @Mapping(source = "name", target = "model") @Mapping(source = "power", target = "electric") Car toCar(Vehicle vehicle); + + @Mapping(source = "make", target = "brand") + @Mapping(source = "modelNumber", target = "model") + @Mapping(source = "electric", target = "electric") + Car toCar(Bike bike); } diff --git a/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/mapper/cat/CatMapper.java b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/mapper/cat/CatMapper.java new file mode 100644 index 00000000000..d78cf081937 --- /dev/null +++ b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/mapper/cat/CatMapper.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.quarkus.component.mapstruct.it.mapper.cat; + +import org.apache.camel.quarkus.component.mapstruct.it.model.Cat; +import org.apache.camel.quarkus.component.mapstruct.it.model.Dog; +import org.mapstruct.Mapper; + +// Test with alternative class and package names (will be instantiated via ServiceLoader) +@Mapper(implementationName = "CamelQuarkusCatMapper", implementationPackage = "org.test.mapper") +public interface CatMapper { + // Test hand-written mapping logic + default Cat dogToCat(Dog dog) { + return new Cat(dog.getDogId(), dog.getName(), dog.getAge(), "meow"); + } +} diff --git a/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/mapper/dog/DogMapper.java b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/mapper/dog/DogMapper.java new file mode 100644 index 00000000000..a0595f47e94 --- /dev/null +++ b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/mapper/dog/DogMapper.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.quarkus.component.mapstruct.it.mapper.dog; + +import java.util.UUID; + +import org.apache.camel.quarkus.component.mapstruct.it.model.Cat; +import org.apache.camel.quarkus.component.mapstruct.it.model.Dog; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; + +// Test CDI support for ApplicationScope beans +@Mapper(imports = UUID.class, componentModel = MappingConstants.ComponentModel.JAKARTA_CDI) +public interface DogMapper { + // Test expressions + @Mapping(target = "dogId", source = "catId", defaultExpression = "java( UUID.randomUUID().toString() )") + @Mapping(source = "name", target = "name") + @Mapping(source = "age", target = "age") + @Mapping(target = "vocalization", constant = "bark") + Dog catToDog(Cat cat); +} diff --git a/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/mapper/employee/EmployeeMapper.java b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/mapper/employee/EmployeeMapper.java new file mode 100644 index 00000000000..01acfa42242 --- /dev/null +++ b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/mapper/employee/EmployeeMapper.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.quarkus.component.mapstruct.it.mapper.employee; + +import org.apache.camel.quarkus.component.mapstruct.it.model.Employee; +import org.apache.camel.quarkus.component.mapstruct.it.model.EmployeeDto; +import org.mapstruct.Mapper; + +@Mapper(implementationName = "CamelQuarkusEmployeeMapper") +public abstract class EmployeeMapper extends EmployeeMapperBase { + // Verify hand-written mapping logic + public EmployeeDto employeeToemployeeDto(Employee employee) { + EmployeeDto dto = new EmployeeDto(); + dto.setEmployeeId(employee.getId()); + dto.setEmployeeName(employee.getName()); + return dto; + } +} diff --git a/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/mapper/employee/EmployeeMapperBase.java b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/mapper/employee/EmployeeMapperBase.java new file mode 100644 index 00000000000..55f15d522c9 --- /dev/null +++ b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/mapper/employee/EmployeeMapperBase.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.quarkus.component.mapstruct.it.mapper.employee; + +import org.apache.camel.quarkus.component.mapstruct.it.model.Employee; +import org.apache.camel.quarkus.component.mapstruct.it.model.EmployeeDto; + +public abstract class EmployeeMapperBase { + // Verify mapper method inheritance + public Employee employeeDtoToEmployee(EmployeeDto employeeDto) { + Employee employee = new Employee(); + employee.setId(employeeDto.getEmployeeId()); + employee.setName(employeeDto.getEmployeeName()); + return employee; + } +} diff --git a/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/mapper/vehicle/VehicleMapper.java b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/mapper/vehicle/VehicleMapper.java new file mode 100644 index 00000000000..7097189cda4 --- /dev/null +++ b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/mapper/vehicle/VehicleMapper.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.quarkus.component.mapstruct.it.mapper.vehicle; + +import org.apache.camel.quarkus.component.mapstruct.it.model.Car; +import org.apache.camel.quarkus.component.mapstruct.it.model.Vehicle; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper +public abstract class VehicleMapper { + // Test static Mapper field. It will be used by the generated TypeConverter + public static final VehicleMapper MAPPER = Mappers.getMapper(VehicleMapper.class); + + @Mapping(source = "brand", target = "company") + @Mapping(source = "model", target = "name") + @Mapping(source = "electric", target = "power") + public abstract Vehicle carToVehicle(Car car); +} diff --git a/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/Bike.java b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/Bike.java new file mode 100644 index 00000000000..760c8b892b8 --- /dev/null +++ b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/Bike.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.quarkus.component.mapstruct.it.model; + +public class Bike { + private String make; + private String modelNumber; + private int year; + private boolean electric; + + public Bike(String make, String modelNumber, int year, boolean electric) { + this.make = make; + this.modelNumber = modelNumber; + this.year = year; + this.electric = electric; + } + + public String getMake() { + return make; + } + + public void setMake(String make) { + this.make = make; + } + + public String getModelNumber() { + return modelNumber; + } + + public void setModelNumber(String modelNumber) { + this.modelNumber = modelNumber; + } + + public int getYear() { + return year; + } + + public void setYear(int year) { + this.year = year; + } + + public boolean isElectric() { + return electric; + } + + public void setElectric(boolean electric) { + this.electric = electric; + } + + @Override + public String toString() { + return String.join(",", make, modelNumber, String.valueOf(year), String.valueOf(electric)); + } + + public static Bike fromString(String bikeString) { + final String[] split = bikeString.split(","); + return new Bike(split[0], split[1], Integer.parseInt(split[2]), Boolean.parseBoolean(split[3])); + } +} diff --git a/integration-tests-jvm/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/Car.java b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/Car.java similarity index 95% rename from integration-tests-jvm/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/Car.java rename to integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/Car.java index 09eda2f054f..8dbb4ed3409 100644 --- a/integration-tests-jvm/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/Car.java +++ b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/Car.java @@ -63,7 +63,7 @@ public void setElectric(boolean electric) { @Override public String toString() { - return String.join(",", brand, model, year + "", electric + ""); + return String.join(",", brand, model, String.valueOf(year), String.valueOf(electric)); } public static Car fromString(String carString) { diff --git a/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/CarDto.java b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/CarDto.java new file mode 100644 index 00000000000..73a61f4c5e7 --- /dev/null +++ b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/CarDto.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.quarkus.component.mapstruct.it.model; + +public class CarDto { + private String brandName; + private String modelName; + private int year; + private boolean electric; + + public CarDto(String brandName, String modelName, int year, boolean electric) { + this.brandName = brandName; + this.modelName = modelName; + this.year = year; + this.electric = electric; + } + + public String getBrandName() { + return brandName; + } + + public void setBrandName(String brandName) { + this.brandName = brandName; + } + + public String getModelName() { + return modelName; + } + + public void setModelName(String modelName) { + this.modelName = modelName; + } + + public int getYear() { + return year; + } + + public void setYear(int year) { + this.year = year; + } + + public boolean isElectric() { + return electric; + } + + public void setElectric(boolean electric) { + this.electric = electric; + } + + @Override + public String toString() { + return String.join(",", brandName, modelName, String.valueOf(year), String.valueOf(electric)); + } + + public static CarDto fromString(String carDtoString) { + final String[] split = carDtoString.split(","); + return new CarDto(split[0], split[1], Integer.parseInt(split[2]), Boolean.parseBoolean(split[3])); + } +} diff --git a/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/Cat.java b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/Cat.java new file mode 100644 index 00000000000..6222aafed63 --- /dev/null +++ b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/Cat.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.quarkus.component.mapstruct.it.model; + +public class Cat { + private String catId; + private String name; + private int age; + private String sound; + + public Cat(String catId, String name, int age, String sound) { + this.catId = catId; + this.name = name; + this.age = age; + this.sound = sound; + } + + public String getCatId() { + return catId; + } + + public void setCatId(String catId) { + this.catId = catId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getSound() { + return sound; + } + + public void setSound(String sound) { + this.sound = sound; + } + + @Override + public String toString() { + return String.join(",", catId, name, String.valueOf(age), sound); + } + + public static Cat fromString(String catString) { + final String[] split = catString.split(","); + return new Cat(split[0], split[1], Integer.parseInt(split[2]), split[3]); + } +} diff --git a/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/Dog.java b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/Dog.java new file mode 100644 index 00000000000..1ee8496b6a8 --- /dev/null +++ b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/Dog.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.quarkus.component.mapstruct.it.model; + +public class Dog { + private String dogId; + private String name; + private int age; + private String vocalization; + + public Dog(String dogId, String name, int age, String vocalization) { + this.dogId = dogId; + this.name = name; + this.age = age; + this.vocalization = vocalization; + } + + public String getDogId() { + return dogId; + } + + public void setDogId(String dogId) { + this.dogId = dogId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getVocalization() { + return vocalization; + } + + public void setVocalization(String vocalization) { + this.vocalization = vocalization; + } + + @Override + public String toString() { + return String.join(",", dogId, name, String.valueOf(age), vocalization); + } + + public static Dog fromString(String dogString) { + final String[] split = dogString.split(","); + return new Dog(split[0], split[1], Integer.parseInt(split[2]), split[3]); + } +} diff --git a/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/Employee.java b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/Employee.java new file mode 100644 index 00000000000..e9a3ed57ef9 --- /dev/null +++ b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/Employee.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.quarkus.component.mapstruct.it.model; + +public class Employee { + private int id; + private String name; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return String.join(",", String.valueOf(id), name); + } + + public static Employee fromString(String employeeString) { + final String[] split = employeeString.split(","); + Employee employee = new Employee(); + employee.setId(Integer.parseInt(split[0])); + employee.setName(split[1]); + return employee; + } +} diff --git a/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/EmployeeDto.java b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/EmployeeDto.java new file mode 100644 index 00000000000..cb93a8f1d17 --- /dev/null +++ b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/EmployeeDto.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.quarkus.component.mapstruct.it.model; + +public class EmployeeDto { + private int employeeId; + private String employeeName; + + public int getEmployeeId() { + return employeeId; + } + + public void setEmployeeId(int employeeId) { + this.employeeId = employeeId; + } + + public String getEmployeeName() { + return employeeName; + } + + public void setEmployeeName(String employeeName) { + this.employeeName = employeeName; + } + + @Override + public String toString() { + return String.join(",", String.valueOf(employeeId), employeeName); + } + + public static EmployeeDto fromString(String employeeDtoString) { + final String[] split = employeeDtoString.split(","); + EmployeeDto employee = new EmployeeDto(); + employee.setEmployeeId(Integer.parseInt(split[0])); + employee.setEmployeeName(split[1]); + return employee; + } +} diff --git a/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/ModelFactory.java b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/ModelFactory.java new file mode 100644 index 00000000000..f1f0803f1e7 --- /dev/null +++ b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/ModelFactory.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.quarkus.component.mapstruct.it.model; + +public class ModelFactory { + + public static Object getModel(String modelString, String modelClassName) { + if (Bike.class.getName().equals(modelClassName)) { + return Bike.fromString(modelString); + } else if (Car.class.getName().equals(modelClassName)) { + return Car.fromString(modelString); + } else if (CarDto.class.getName().equals(modelClassName)) { + return CarDto.fromString(modelString); + } else if (Cat.class.getName().equals(modelClassName)) { + return Cat.fromString(modelString); + } else if (Dog.class.getName().equals(modelClassName)) { + return Dog.fromString(modelString); + } else if (Employee.class.getName().equals(modelClassName)) { + return Employee.fromString(modelString); + } else if (EmployeeDto.class.getName().equals(modelClassName)) { + return EmployeeDto.fromString(modelString); + } else if (Vehicle.class.getName().equals(modelClassName)) { + return Vehicle.fromString(modelString); + } + throw new IllegalArgumentException("Unknown model class: " + modelClassName); + } +} diff --git a/integration-tests-jvm/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/Vehicle.java b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/Vehicle.java similarity index 96% rename from integration-tests-jvm/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/Vehicle.java rename to integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/Vehicle.java index 22566c9c347..27e27449f08 100644 --- a/integration-tests-jvm/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/Vehicle.java +++ b/integration-tests/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/model/Vehicle.java @@ -63,7 +63,7 @@ public void setYear(int year) { @Override public String toString() { - return String.join(",", company, name, power, year + ""); + return String.join(",", company, name, power, String.valueOf(year)); } public static Vehicle fromString(String vehicleString) { diff --git a/integration-tests/mapstruct/src/test/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructExplicitPackagesTest.java b/integration-tests/mapstruct/src/test/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructExplicitPackagesTest.java new file mode 100644 index 00000000000..1586a542615 --- /dev/null +++ b/integration-tests/mapstruct/src/test/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructExplicitPackagesTest.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.quarkus.component.mapstruct.it; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.restassured.RestAssured; +import org.apache.camel.quarkus.component.mapstruct.it.mapper.car.CarMapper; +import org.apache.camel.quarkus.component.mapstruct.it.mapper.vehicle.VehicleMapper; +import org.apache.camel.quarkus.component.mapstruct.it.model.Car; +import org.apache.camel.quarkus.component.mapstruct.it.model.Employee; +import org.apache.camel.quarkus.component.mapstruct.it.model.EmployeeDto; +import org.apache.camel.quarkus.component.mapstruct.it.model.Vehicle; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@QuarkusTest +@TestProfile(MapStructExplicitPackagesTestProfile.class) +public class MapStructExplicitPackagesTest { + @ParameterizedTest + @ValueSource(strings = { "component", "converter" }) + void mapVehicleToCarSuccess(String value) { + Vehicle vehicle = new Vehicle("Volvo", "XC60", "true", 2021); + + String response = RestAssured.given() + .queryParam("fromTypeName", Vehicle.class.getName()) + .queryParam("toTypeName", Car.class.getName()) + .body(vehicle.toString()) + .post("/mapstruct/" + value) + .then() + .statusCode(200) + .extract() + .body() + .asString(); + + Car car = Car.fromString(response); + + assertEquals(vehicle.getCompany(), car.getBrand()); + assertEquals(vehicle.getName(), car.getModel()); + assertEquals(vehicle.getYear(), car.getYear()); + assertEquals(Boolean.parseBoolean(vehicle.getPower()), car.isElectric()); + } + + @ParameterizedTest + @ValueSource(strings = { "component", "converter" }) + void mapEmployeeToEmployeeDtoFail(String value) { + Employee employee = new Employee(); + employee.setId(1); + employee.setName("Mr Camel Quarkus"); + + // Mapping should fail because the configured mapper packages do not handle employee types + RestAssured.given() + .queryParam("fromTypeName", Employee.class.getName()) + .queryParam("toTypeName", EmployeeDto.class.getName()) + .body(employee.toString()) + .post("/mapstruct/" + value) + .then() + .statusCode(500) + .body(containsString("NoTypeConversionAvailableException")); + } + + @Test + void mapStructComponentPackages() { + RestAssured.get("/mapstruct/component/packages") + .then() + .statusCode(200) + .body(containsString(CarMapper.class.getPackageName()), containsString(VehicleMapper.class.getPackageName())); + } +} diff --git a/integration-tests-jvm/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructRoutes.java b/integration-tests/mapstruct/src/test/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructExplicitPackagesTestProfile.java similarity index 56% rename from integration-tests-jvm/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructRoutes.java rename to integration-tests/mapstruct/src/test/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructExplicitPackagesTestProfile.java index 3671184da9a..02c708ecb1c 100644 --- a/integration-tests-jvm/mapstruct/src/main/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructRoutes.java +++ b/integration-tests/mapstruct/src/test/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructExplicitPackagesTestProfile.java @@ -16,22 +16,17 @@ */ package org.apache.camel.quarkus.component.mapstruct.it; -import jakarta.inject.Named; -import org.apache.camel.builder.RouteBuilder; -import org.apache.camel.component.mapstruct.MapstructComponent; -import org.apache.camel.quarkus.component.mapstruct.it.model.Car; +import java.util.Map; -public class MapStructRoutes extends RouteBuilder { - @Named("mapstruct") - MapstructComponent mapstruct() { - MapstructComponent mapstruct = new MapstructComponent(); - mapstruct.setMapperPackageName("org.apache.camel.quarkus.component.mapstruct.it.mapper"); - return mapstruct; - } +import io.quarkus.test.junit.QuarkusTestProfile; +import org.apache.camel.quarkus.component.mapstruct.it.mapper.car.CarMapper; +import org.apache.camel.quarkus.component.mapstruct.it.mapper.vehicle.VehicleMapper; +public class MapStructExplicitPackagesTestProfile implements QuarkusTestProfile { @Override - public void configure() throws Exception { - from("direct:component").to("mapstruct:" + Car.class.getName()); - from("direct:converter").convertBodyTo(Car.class); + public Map getConfigOverrides() { + // Join package names with extra whitespace to verify it is trimmed at build time + return Map.of("camel.component.mapstruct.mapper-package-name", + String.join(" , ", CarMapper.class.getPackageName(), VehicleMapper.class.getPackageName())); } } diff --git a/integration-tests-jvm/mapstruct/src/test/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructIT.java b/integration-tests/mapstruct/src/test/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructIT.java similarity index 100% rename from integration-tests-jvm/mapstruct/src/test/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructIT.java rename to integration-tests/mapstruct/src/test/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructIT.java diff --git a/integration-tests/mapstruct/src/test/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructTest.java b/integration-tests/mapstruct/src/test/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructTest.java new file mode 100644 index 00000000000..d8c534bbd2b --- /dev/null +++ b/integration-tests/mapstruct/src/test/java/org/apache/camel/quarkus/component/mapstruct/it/MapStructTest.java @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.quarkus.component.mapstruct.it; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; +import org.apache.camel.quarkus.component.mapstruct.CamelQuarkusMapStructMapperFinder; +import org.apache.camel.quarkus.component.mapstruct.it.mapper.car.CarMapper; +import org.apache.camel.quarkus.component.mapstruct.it.mapper.cat.CatMapper; +import org.apache.camel.quarkus.component.mapstruct.it.mapper.dog.DogMapper; +import org.apache.camel.quarkus.component.mapstruct.it.mapper.employee.EmployeeMapper; +import org.apache.camel.quarkus.component.mapstruct.it.mapper.vehicle.VehicleMapper; +import org.apache.camel.quarkus.component.mapstruct.it.model.Bike; +import org.apache.camel.quarkus.component.mapstruct.it.model.Car; +import org.apache.camel.quarkus.component.mapstruct.it.model.Cat; +import org.apache.camel.quarkus.component.mapstruct.it.model.Dog; +import org.apache.camel.quarkus.component.mapstruct.it.model.Employee; +import org.apache.camel.quarkus.component.mapstruct.it.model.EmployeeDto; +import org.apache.camel.quarkus.component.mapstruct.it.model.Vehicle; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@QuarkusTest +public class MapStructTest { + @ParameterizedTest + @ValueSource(strings = { "component", "converter" }) + void mapVehicleToCar(String value) { + Vehicle vehicle = new Vehicle("Volvo", "XC60", "true", 2021); + + String response = RestAssured.given() + .queryParam("fromTypeName", Vehicle.class.getName()) + .queryParam("toTypeName", Car.class.getName()) + .body(vehicle.toString()) + .post("/mapstruct/" + value) + .then() + .statusCode(200) + .extract() + .body() + .asString(); + + Car car = Car.fromString(response); + + assertEquals(vehicle.getCompany(), car.getBrand()); + assertEquals(vehicle.getName(), car.getModel()); + assertEquals(vehicle.getYear(), car.getYear()); + assertEquals(Boolean.parseBoolean(vehicle.getPower()), car.isElectric()); + } + + @ParameterizedTest + @ValueSource(strings = { "component", "converter" }) + void mapBikeToCar(String value) { + Bike bike = new Bike("Honda", "CBR65R", 2023, false); + + String response = RestAssured.given() + .queryParam("fromTypeName", Bike.class.getName()) + .queryParam("toTypeName", Car.class.getName()) + .body(bike.toString()) + .post("/mapstruct/" + value) + .then() + .statusCode(200) + .extract() + .body() + .asString(); + + Car car = Car.fromString(response); + + assertEquals(bike.getMake(), car.getBrand()); + assertEquals(bike.getModelNumber(), car.getModel()); + assertEquals(bike.getYear(), car.getYear()); + assertEquals(bike.isElectric(), car.isElectric()); + } + + @ParameterizedTest + @ValueSource(strings = { "component", "converter" }) + void mapDogToCatWithAlternatePackageAndClassName(String value) { + Dog dog = new Dog("1", "Snoopy", 8, "bark"); + + String response = RestAssured.given() + .queryParam("fromTypeName", Dog.class.getName()) + .queryParam("toTypeName", Cat.class.getName()) + .body(dog.toString()) + .post("/mapstruct/" + value) + .then() + .statusCode(200) + .extract() + .body() + .asString(); + + Cat cat = Cat.fromString(response); + + assertEquals(dog.getDogId(), cat.getCatId()); + assertEquals(dog.getName(), cat.getName()); + assertEquals(dog.getAge(), cat.getAge()); + assertEquals("meow", cat.getSound()); + } + + @ParameterizedTest + @ValueSource(strings = { "component", "converter" }) + void mapCatToDogWithApplicationScopedMapperBean(String value) { + Cat cat = new Cat("1", "Garfield", 12, "meow"); + + String response = RestAssured.given() + .queryParam("fromTypeName", Cat.class.getName()) + .queryParam("toTypeName", Dog.class.getName()) + .body(cat.toString()) + .post("/mapstruct/" + value) + .then() + .statusCode(200) + .extract() + .body() + .asString(); + + Dog dog = Dog.fromString(response); + + assertEquals(cat.getCatId(), dog.getDogId()); + assertEquals(cat.getName(), dog.getName()); + assertEquals(cat.getAge(), dog.getAge()); + assertEquals("bark", dog.getVocalization()); + } + + @ParameterizedTest + @ValueSource(strings = { "component", "converter" }) + void mapEmployeeToEmployeeDtoWithAlternateClassName(String value) { + Employee employee = new Employee(); + employee.setId(1); + employee.setName("Mr Camel Quarkus"); + + String response = RestAssured.given() + .queryParam("fromTypeName", Employee.class.getName()) + .queryParam("toTypeName", EmployeeDto.class.getName()) + .body(employee.toString()) + .post("/mapstruct/" + value) + .then() + .statusCode(200) + .extract() + .body() + .asString(); + + EmployeeDto dto = EmployeeDto.fromString(response); + assertEquals(employee.getId(), dto.getEmployeeId()); + assertEquals(employee.getName(), dto.getEmployeeName()); + } + + @ParameterizedTest + @ValueSource(strings = { "component", "converter" }) + void mapEmployeeDtoToEmployeeWithInheritedMapperMethod(String value) { + EmployeeDto dto = new EmployeeDto(); + dto.setEmployeeId(1); + dto.setEmployeeName("Mr Camel Quarkus"); + + String response = RestAssured.given() + .queryParam("fromTypeName", EmployeeDto.class.getName()) + .queryParam("toTypeName", Employee.class.getName()) + .body(dto.toString()) + .post("/mapstruct/" + value) + .then() + .statusCode(200) + .extract() + .body() + .asString(); + + Employee employee = Employee.fromString(response); + assertEquals(dto.getEmployeeId(), employee.getId()); + assertEquals(dto.getEmployeeName(), employee.getName()); + } + + @ParameterizedTest + @ValueSource(strings = { "component", "converter" }) + void mapCarToVehicleUsingStaticFieldMapper(String value) { + Car car = new Car("Volvo", "XC60", 2021, true); + + String response = RestAssured.given() + .queryParam("fromTypeName", Car.class.getName()) + .queryParam("toTypeName", Vehicle.class.getName()) + .body(car.toString()) + .post("/mapstruct/" + value) + .then() + .statusCode(200) + .extract() + .body() + .asString(); + + Vehicle vehicle = Vehicle.fromString(response); + + assertEquals(car.getBrand(), vehicle.getCompany()); + assertEquals(car.getModel(), vehicle.getName()); + assertEquals(car.getYear(), vehicle.getYear()); + assertEquals(car.isElectric(), Boolean.parseBoolean(vehicle.getPower())); + } + + @Test + void mapStructMapperFinderImpl() { + RestAssured.get("/mapstruct/finder/mapper") + .then() + .statusCode(200) + .body(is(CamelQuarkusMapStructMapperFinder.class.getName())); + } + + @Test + void mapStructComponentPackages() { + RestAssured.get("/mapstruct/component/packages") + .then() + .statusCode(200) + .body( + containsString(CarMapper.class.getPackageName()), + containsString(CatMapper.class.getPackageName()), + containsString(DogMapper.class.getPackageName()), + containsString(EmployeeMapper.class.getPackageName()), + containsString(VehicleMapper.class.getPackageName())); + } +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 54339ea2ddd..1a3fdd4d03a 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -159,6 +159,7 @@ lumberjack mail management + mapstruct master master-file master-openshift diff --git a/tooling/scripts/test-categories.yaml b/tooling/scripts/test-categories.yaml index 426c338a2cf..6dc3d2ed948 100644 --- a/tooling/scripts/test-categories.yaml +++ b/tooling/scripts/test-categories.yaml @@ -88,6 +88,7 @@ group-05: - datasonnet - hl7 - jaxb + - mapstruct - ssh - soap - xmlsecurity