diff --git a/applications/spring-shell/src/main/resources/application.properties b/applications/spring-shell/src/main/resources/application.properties index 5dfe894e2..f1c94f72e 100644 --- a/applications/spring-shell/src/main/resources/application.properties +++ b/applications/spring-shell/src/main/resources/application.properties @@ -18,6 +18,7 @@ spring.profiles.active=default, core spring.application.name=spring-boot-migrator # toggle support for git to sync and auto-commit sbm.gitSupportEnabled=true +sbm.muleTriggerMeshTransformEnabled=true logging.level.org=ERROR logging.level.org.springframework.sbm.logging.MethodCallTraceInterceptor=DEBUG logging.level.org.springframework.sbm.logging.StopWatchTraceInterceptor=DEBUG diff --git a/applications/spring-shell/src/main/resources/banner.txt b/applications/spring-shell/src/main/resources/banner.txt index 763e673d4..5b724d10c 100644 --- a/applications/spring-shell/src/main/resources/banner.txt +++ b/applications/spring-shell/src/main/resources/banner.txt @@ -38,6 +38,15 @@ Base Package: ${sbm.defaultBasePackage} Use -Dsbm.defaultBasePackage=com.acme.packagename as VM parameter on startup to set the property. +TriggerMesh transformation support of Dataweave: ${sbm.muleTriggerMeshTransformEnabled} + + - When applying the mule-to-boot recipe, use `sbm.muleTriggerMeshTransformEnabled` to + generate the code required to send the Dataweave transformation to TriggerMesh using + the TriggerMesh Dataweave transformation service (https://docs.triggermesh.io/guides/dataweavetransformation/). + - This will require a TriggerMesh transformation exist on your Kubernetes cluster to function. When running the + service, be sure to set the `K_SINK` environment variable to your exposed service URL. + + Use -Dsbm.muleTriggerMeshTransformEnabled=true|false as VM parameter on startup to set property. Get Started: ------------ diff --git a/applications/spring-shell/src/test/java/org/springframework/sbm/MigrateSimpleMuleAppDataweaveIntegrationTest.java b/applications/spring-shell/src/test/java/org/springframework/sbm/MigrateSimpleMuleAppDataweaveIntegrationTest.java new file mode 100644 index 000000000..f6106b988 --- /dev/null +++ b/applications/spring-shell/src/test/java/org/springframework/sbm/MigrateSimpleMuleAppDataweaveIntegrationTest.java @@ -0,0 +1,103 @@ +/* + * Copyright 2021 - 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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.springframework.sbm; + +import org.junit.jupiter.api.*; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import java.util.*; + +import static org.assertj.core.api.Assertions.assertThat; + +class MigrateSimpleMuleAppDataweaveIntegrationTest extends IntegrationTestBaseClass { + private final RestTemplate restTemplate = new RestTemplate(); + private static RunningNetworkedContainer tmDataweaveContainer; + + @Override + protected String getTestSubDir() { + + return "mule-app/spring-dw-mule"; + + } + + @BeforeAll + public static void beforeAll() { + IntegrationTestBaseClass.beforeAll(); + + // Will need to ensure this is set globally for the test + System.setProperty("sbm.muleTriggerMeshTransformEnabled", "true"); + + // start TriggerMesh Dataweave Translator + HashMap envMap = new HashMap<>(); + envMap.put("NAMESPACE", "default"); + envMap.put("DATAWEAVETRANSFORMATION_ALLOW_SPELL_OVERRIDE", "true"); + + tmDataweaveContainer = startDockerContainer( + new NetworkedContainer( + "gcr.io/triggermesh/dataweavetransformation-adapter:v1.21.0", + List.of(8080), + "dwhost"), + null, + envMap); + if (!tmDataweaveContainer.getContainer().isRunning()) { + throw new RuntimeException("TriggerMesh Dataweave Transformer container could not be started"); + } + } + + @AfterAll + public static void afterAll() { + if (tmDataweaveContainer != null && tmDataweaveContainer.getContainer() != null) { + tmDataweaveContainer.getContainer().stop(); + } + } + + @Test + @Tag("integration") + public void t2_dataweaveIntegrationWorks() { + intializeTestProject(); + scanProject(); + applyRecipe("initialize-spring-boot-migration"); + applyRecipe("migrate-mule-to-triggermesh-boot"); + + executeMavenGoals(getTestDir(), "clean", "package", "spring-boot:build-image"); + + int dwPort = tmDataweaveContainer.getContainer().getMappedPort(8080); + HashMap runtimeEnv = new HashMap<>(); + runtimeEnv.put("K_SINK", "http://dwhost:8080"); + + RunningNetworkedContainer container = startDockerContainer( + new NetworkedContainer("hellomuledw-migrated:1.0-SNAPSHOT", List.of(9081), "spring"), + tmDataweaveContainer.getNetwork(), + runtimeEnv); + + checkSendHttpMessage(container.getContainer().getMappedPort(9081)); + checkTranslatedInboundGatewayHttpMessage(container.getContainer().getMappedPort(9081)); + } + + private void checkTranslatedInboundGatewayHttpMessage(int port) { + ResponseEntity responseEntity = restTemplate.getForEntity("http://localhost:" + port + "/dwtest", String.class); + + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(responseEntity.getBody()).isEqualTo("\n{\n \"greeting\": \"hello from SBM\"\n}"); + } + + private void checkSendHttpMessage(int port) { + ResponseEntity responseEntity = restTemplate.getForEntity("http://localhost:" + port + "/test", String.class); + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + } +} diff --git a/applications/spring-shell/src/test/resources/testcode/mule-app/spring-dw-mule/.gitignore b/applications/spring-shell/src/test/resources/testcode/mule-app/spring-dw-mule/.gitignore new file mode 100644 index 000000000..549e00a2a --- /dev/null +++ b/applications/spring-shell/src/test/resources/testcode/mule-app/spring-dw-mule/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/applications/spring-shell/src/test/resources/testcode/mule-app/spring-dw-mule/pom.xml b/applications/spring-shell/src/test/resources/testcode/mule-app/spring-dw-mule/pom.xml new file mode 100644 index 000000000..dc265af89 --- /dev/null +++ b/applications/spring-shell/src/test/resources/testcode/mule-app/spring-dw-mule/pom.xml @@ -0,0 +1,39 @@ + + + + + 4.0.0 + + org.example + hellomuledw-migrated + 1.0-SNAPSHOT + + + 11 + 11 + + + + + org.junit.jupiter + junit-jupiter-api + 5.8.2 + + + \ No newline at end of file diff --git a/applications/spring-shell/src/test/resources/testcode/mule-app/spring-dw-mule/src/main/java/com/example/javadsl/Foo.java b/applications/spring-shell/src/test/resources/testcode/mule-app/spring-dw-mule/src/main/java/com/example/javadsl/Foo.java new file mode 100644 index 000000000..4d3bdaafd --- /dev/null +++ b/applications/spring-shell/src/test/resources/testcode/mule-app/spring-dw-mule/src/main/java/com/example/javadsl/Foo.java @@ -0,0 +1,7 @@ +package com.example.javadsl; + +import org.springframework.context.annotation.Configuration; + +@Configuration +public class Foo { +} diff --git a/applications/spring-shell/src/test/resources/testcode/mule-app/spring-dw-mule/src/main/resources/apikit-dw-mule.xml b/applications/spring-shell/src/test/resources/testcode/mule-app/spring-dw-mule/src/main/resources/apikit-dw-mule.xml new file mode 100644 index 000000000..c1d977bd7 --- /dev/null +++ b/applications/spring-shell/src/test/resources/testcode/mule-app/spring-dw-mule/src/main/resources/apikit-dw-mule.xml @@ -0,0 +1,32 @@ + + + + + + + + + + \ No newline at end of file diff --git a/applications/spring-shell/src/test/resources/testcode/mule-app/spring-dw-mule/src/main/resources/http-mule.xml b/applications/spring-shell/src/test/resources/testcode/mule-app/spring-dw-mule/src/main/resources/http-mule.xml new file mode 100644 index 000000000..83ade0b3f --- /dev/null +++ b/applications/spring-shell/src/test/resources/testcode/mule-app/spring-dw-mule/src/main/resources/http-mule.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + diff --git a/components/sbm-core/src/main/java/org/springframework/sbm/project/resource/SbmApplicationProperties.java b/components/sbm-core/src/main/java/org/springframework/sbm/project/resource/SbmApplicationProperties.java index e2bb4db66..8c83e632a 100644 --- a/components/sbm-core/src/main/java/org/springframework/sbm/project/resource/SbmApplicationProperties.java +++ b/components/sbm-core/src/main/java/org/springframework/sbm/project/resource/SbmApplicationProperties.java @@ -30,6 +30,7 @@ @ConfigurationProperties(prefix = "sbm") public class SbmApplicationProperties { private boolean gitSupportEnabled; + private boolean muleTriggerMeshTransformEnabled; private String defaultBasePackage; private boolean writeInMavenLocal; private boolean javaParserLoggingCompilationWarningsAndErrors; diff --git a/components/sbm-core/src/main/resources/application-core.properties b/components/sbm-core/src/main/resources/application-core.properties index b4f42efc8..f9e8f3af1 100644 --- a/components/sbm-core/src/main/resources/application-core.properties +++ b/components/sbm-core/src/main/resources/application-core.properties @@ -18,6 +18,8 @@ # toggle support for git to sync and auto-commit sbm.gitSupportEnabled=true +# toggle support to use TriggerMesh for dataweave transformations +sbm.muleTriggerMeshTransformEnabled=false # default base package when adding classes and no base package can be calculated sbm.defaultBasePackage=org.springframework.sbm # default groupId when creating a new build file diff --git a/components/sbm-recipes-mule-to-boot/src/main/java/org/springframework/sbm/mule/MigrateMuleToBoot.java b/components/sbm-recipes-mule-to-boot/src/main/java/org/springframework/sbm/mule/MigrateMuleToBoot.java index e66224a56..e0b8991b6 100644 --- a/components/sbm-recipes-mule-to-boot/src/main/java/org/springframework/sbm/mule/MigrateMuleToBoot.java +++ b/components/sbm-recipes-mule-to-boot/src/main/java/org/springframework/sbm/mule/MigrateMuleToBoot.java @@ -15,6 +15,7 @@ */ package org.springframework.sbm.mule; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -30,23 +31,34 @@ import org.springframework.sbm.java.migration.conditions.HasTypeAnnotation; import org.springframework.sbm.mule.actions.JavaDSLAction2; import org.springframework.sbm.mule.conditions.MuleConfigFileExist; +import org.springframework.sbm.project.resource.SbmApplicationProperties; import java.util.List; +@RequiredArgsConstructor @Configuration public class MigrateMuleToBoot { + private final SbmApplicationProperties sbmProperties; @Autowired private JavaDSLAction2 javaDSLAction2; - @Bean public Recipe muleRecipe() { + String name = "migrate-mule-to-boot"; + String description = "Migrate Mulesoft 3.9 to Spring Boot."; + + // Flag to enable TriggerMesh ransformation mode + if (sbmProperties.isMuleTriggerMeshTransformEnabled()) { + name = "migrate-mule-to-triggermesh-boot"; + description = "Migrate Mulesoft 3.9 to Spring Boot using TriggerMesh."; + javaDSLAction2.setMuleTriggerMeshTransformEnabled(true); + } + return Recipe.builder() - .name("migrate-mule-to-boot") - .description("Migrate Mulesoft 3.9 to Spring Boot") + .name(name) + .description(description) .order(60) - .description("Migrate Mulesoft to Spring Boot") .condition(new MuleConfigFileExist()) .actions(List.of( /* diff --git a/components/sbm-recipes-mule-to-boot/src/main/java/org/springframework/sbm/mule/actions/JavaDSLAction2.java b/components/sbm-recipes-mule-to-boot/src/main/java/org/springframework/sbm/mule/actions/JavaDSLAction2.java index 0e7000242..0454fccc3 100644 --- a/components/sbm-recipes-mule-to-boot/src/main/java/org/springframework/sbm/mule/actions/JavaDSLAction2.java +++ b/components/sbm-recipes-mule-to-boot/src/main/java/org/springframework/sbm/mule/actions/JavaDSLAction2.java @@ -15,6 +15,7 @@ */ package org.springframework.sbm.mule.actions; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; @@ -30,8 +31,10 @@ import org.springframework.sbm.java.api.JavaSource; import org.springframework.sbm.java.api.JavaSourceAndType; import org.springframework.sbm.java.api.Type; +import org.springframework.sbm.mule.actions.javadsl.translators.DslSnippet; import org.springframework.sbm.mule.api.MuleMigrationContext; import org.springframework.sbm.mule.api.MuleMigrationContextFactory; +import org.springframework.sbm.mule.api.toplevel.AbstractTopLevelElement; import org.springframework.sbm.mule.api.toplevel.TopLevelElement; import org.springframework.sbm.mule.api.toplevel.TopLevelElementFactory; import org.springframework.sbm.mule.api.toplevel.UnknownTopLevelElement; @@ -54,6 +57,9 @@ public class JavaDSLAction2 extends AbstractAction { private final MuleMigrationContextFactory muleMigrationContextFactory; private final Map, TopLevelElementFactory> topLevelTypeMap; + @Setter + private boolean muleTriggerMeshTransformEnabled; + @Autowired public JavaDSLAction2(MuleMigrationContextFactory muleMigrationContextFactory, List topLevelTypeFactories) { topLevelTypeMap = topLevelTypeFactories.stream() @@ -110,6 +116,11 @@ private void handleTopLevelElements(BuildFile buildFile, MuleMigrationContext mu buildFile.addDependencies(new ArrayList<>(dependencies)); endProcess(); + if (muleTriggerMeshTransformEnabled) { + logEvent("Adding TriggerMesh Dataweave payload class"); + createClass(context, createTmDwPayloadClass(context)); + } + logEvent("Adding " + topLevelElements.size() + " methods"); topLevelElements.forEach(topLevelElement -> { flowConfigurationSource.getType().addMethod( @@ -228,4 +239,21 @@ private SpringBootApplicationProperties findOrCreateDefaultApplicationProperties .findFirst() .get(); } + + private String createTmDwPayloadClass(ProjectContext projectContext) { + JavaSourceSet mainJavaSourceSet = projectContext.getApplicationModules().getTopmostApplicationModules().get(0).getMainJavaSourceSet(); + String packageName = mainJavaSourceSet.getJavaSourceLocation().getPackageName(); + return "package " + packageName + ";\n" + + "import " + SPRING_CONFIGURATION_ANNOTATION + ";\n\n" + + "import lombok.Data;\n\n" + + "/* Included with the baseline to support bridging between the Flow configuration and the translation implementation. */\n\n" + + "@Data\n" + + "public class TmDwPayload {\n" + + " private String id;\n" + + " private String source;\n" + + " private String sourceType;\n" + + " private String payload;\n" + + "}\n"; + } + } diff --git a/components/sbm-recipes-mule-to-boot/src/main/java/org/springframework/sbm/mule/actions/javadsl/translators/dwl/DwlTransformTranslator.java b/components/sbm-recipes-mule-to-boot/src/main/java/org/springframework/sbm/mule/actions/javadsl/translators/dwl/DwlTransformTranslator.java index 074fa83c7..13d6eb234 100644 --- a/components/sbm-recipes-mule-to-boot/src/main/java/org/springframework/sbm/mule/actions/javadsl/translators/dwl/DwlTransformTranslator.java +++ b/components/sbm-recipes-mule-to-boot/src/main/java/org/springframework/sbm/mule/actions/javadsl/translators/dwl/DwlTransformTranslator.java @@ -16,7 +16,14 @@ package org.springframework.sbm.mule.actions.javadsl.translators.dwl; +import com.fasterxml.jackson.annotation.JsonIgnore; +import freemarker.cache.FileTemplateLoader; +import freemarker.template.Configuration; +import freemarker.template.Version; +import freemarker.template.Template; +import lombok.Setter; import org.mulesoft.schema.mule.ee.dw.TransformMessageType; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.sbm.java.util.Helper; import org.springframework.sbm.mule.actions.javadsl.translators.DslSnippet; import org.springframework.sbm.mule.actions.javadsl.translators.MuleComponentToSpringIntegrationDslTranslator; @@ -24,15 +31,23 @@ import org.springframework.stereotype.Component; import javax.xml.namespace.QName; -import java.util.Collections; +import java.io.File; +import java.io.StringWriter; +import java.util.HashMap; import java.util.Map; -import java.util.Set; @Component public class DwlTransformTranslator implements MuleComponentToSpringIntegrationDslTranslator { - public static final String STATEMENT_CONTENT = ".transform($CLASSNAME::transform)"; - private static final String externalClassContentPrefixTemplate = "package com.example.javadsl;\n" + - "\n" + + public static final String TRANSFORM_STATEMENT_CONTENT = ".transform($CLASSNAME::transform)"; + public static final String externalPackageName = "com.example.javadsl"; + + @Autowired + @Setter + @JsonIgnore + private Configuration templateConfiguration; + + /* Define the stubs for adding the transformation as a comment to be addressed */ + private static final String externalClassContentPrefixTemplate = "package " + externalPackageName + ";\n\n" + "public class $CLASSNAME {\n" + " /*\n" + " * TODO:\n" + @@ -46,6 +61,22 @@ public class DwlTransformTranslator implements MuleComponentToSpringIntegrationD " }\n" + "}"; + /* + * Define the TriggerMesh specific stubs when enabled. This will capture the transformation, and send it along + * with the payload to the TriggerMesh Dataweave Transformation Service. + */ + private static final String triggermeshPayloadHandlerContent = "" + + ".handle((p, h) -> {\n" + + " TmDwPayload dwPayload = new TmDwPayload();\n" + + " String contentType = \"application/json\";\n" + + " if (h.get(\"contentType\") != null) { contentType = h.get(\"contentType\").toString(); }\n" + + " dwPayload.setId(h.getId().toString());\n" + + " dwPayload.setSourceType(contentType);\n" + + " dwPayload.setSource(h.get(\"http_requestUrl\").toString());\n" + + " dwPayload.setPayload(p.toString());\n" + + " return dwPayload;\n" + + " })"; + @Override public Class getSupportedMuleType() { return TransformMessageType.class; @@ -60,13 +91,19 @@ public DslSnippet translate( String flowName, Map translatorsMap ) { + // Ugly hack to work around an inability to inject a sbm property into the mulesoft parser. + String isTmTransformationEnabled = System.getProperty("sbm.muleTriggerMeshTransformEnabled"); if (component.getSetPayload() != null) { if (isComponentReferencingAnExternalFile(component)) { return formExternalFileBasedDSLSnippet(component); } - return formEmbeddedDWLBasedDSLSnippet(component, Helper.sanitizeForBeanMethodName(flowName), id); + if (isTmTransformationEnabled != null && isTmTransformationEnabled.equals("true")) { + return formTriggerMeshDWLBasedDSLSnippet(component, Helper.sanitizeForBeanMethodName(flowName), id); + } else { + return formEmbeddedDWLBasedDSLSnippet(component, Helper.sanitizeForBeanMethodName(flowName), id); + } } return noSupportDslSnippet(); @@ -90,11 +127,67 @@ private DslSnippet formEmbeddedDWLBasedDSLSnippet(TransformMessageType component replaceClassName(externalClassContentSuffixTemplate, className); return DslSnippet.builder() - .renderedSnippet(replaceClassName(STATEMENT_CONTENT, className)) + .renderedSnippet(replaceClassName(TRANSFORM_STATEMENT_CONTENT, className)) .externalClassContent(externalClassContent) .build(); } + private DslSnippet formTriggerMeshDWLBasedDSLSnippet(TransformMessageType component, String flowName, int id) { + String className = capitalizeFirstLetter(flowName) + "TransformTM_" + id; + String dwlSpell = component.getSetPayload().getContent().toString(); + + // Locate the output content type based on the spell. If it isn't present, default to + // application/json + String outputContentType = getSpellOutputType(dwlSpell); + Map templateParams = new HashMap<>(); + templateParams.put("className", className); + templateParams.put("outputContentType", outputContentType); + templateParams.put("dwSpell", sanitizeSpell(dwlSpell)); + templateParams.put("packageName", externalPackageName); + + StringWriter sw = new StringWriter(); + try { + // In cases where the template library is not initialized (unit testing) + if (templateConfiguration == null) { + templateConfiguration = new Configuration(new Version("2.3.0")); + templateConfiguration.setTemplateLoader(new FileTemplateLoader(new File("./src/main/resources/templates"))); + } + + Template template = templateConfiguration.getTemplate("triggermesh-dw-transformation-template.ftl"); + template.process(templateParams, sw); + } catch (Exception e) { + throw new RuntimeException(e); + } + + String tmTransformationContent = sw.toString(); + + // Build the dw payload + return DslSnippet.builder() + .renderedSnippet(triggermeshPayloadHandlerContent + "\n" + replaceClassName(TRANSFORM_STATEMENT_CONTENT, className)) + .externalClassContent(tmTransformationContent) + .build(); + } + + private String getSpellOutputType(String spell) { + String spellOutputType = "application/json"; + + String []spellElements = spell.split(" "); + for (int i = 0; i < spellElements.length; i++) { + if (spellElements[i].equals("%output")) { + spellOutputType = spellElements[i+1].trim(); + break; + } else if (spellElements[i].equals("---")) { + break; + } + } + + if (spellOutputType.contains(";")) { + spellOutputType = spellOutputType.split(";")[0]; + } + + return spellOutputType; + } + private DslSnippet formExternalFileBasedDSLSnippet(TransformMessageType component) { String resource = component.getSetPayload().getResource(); String className = sanitizeForClassName(resource); @@ -104,7 +197,7 @@ private DslSnippet formExternalFileBasedDSLSnippet(TransformMessageType componen + resource.replace("classpath:", "") + replaceClassName(externalClassContentSuffixTemplate, className); return DslSnippet.builder() - .renderedSnippet(replaceClassName(STATEMENT_CONTENT, className)) + .renderedSnippet(replaceClassName(TRANSFORM_STATEMENT_CONTENT, className)) .externalClassContent(content) .build(); } @@ -115,6 +208,18 @@ public static String sanitizeForClassName(String classNameCandidate) { return (capitalizeFirstLetter(sanitizedClassName) + "Transform"); } + // Remove the leading/trailing spaces, [], ensure the double quote marks are escaped, and swap out the newlines + private static String sanitizeSpell(String spell) { + String s = spell.trim(); + if (s.charAt(0) == '[' && s.charAt(s.length() -1) == ']') { + s = s.substring(1); + s = s.substring(0, s.length() - 1); + } + s = s.replace("\"", "\\\""); + s = s.replace("\n", "\\n"); + return s; + } + private boolean isComponentReferencingAnExternalFile(TransformMessageType component) { return component.getSetPayload().getContent().isEmpty(); } diff --git a/components/sbm-recipes-mule-to-boot/src/main/resources/templates/triggermesh-dw-transformation-template.ftl b/components/sbm-recipes-mule-to-boot/src/main/resources/templates/triggermesh-dw-transformation-template.ftl new file mode 100644 index 000000000..0e82a634e --- /dev/null +++ b/components/sbm-recipes-mule-to-boot/src/main/resources/templates/triggermesh-dw-transformation-template.ftl @@ -0,0 +1,68 @@ +<#if packageName?has_content> +package ${packageName}; + +<#else> +package com.example.javadsl; + + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +public class ${className} { + public static class DataWeavePayload { + public String input_data; + public String spell; + public String input_content_type; + public String output_content_type; + }; + + public static String transform(TmDwPayload payload) { + String uuid = payload.getId(); + String url = System.getenv("K_SINK"); + HttpClient client = HttpClient.newHttpClient(); + HttpRequest.Builder requestBuilder; + DataWeavePayload dwPayload = new DataWeavePayload(); + + if (payload.getSourceType().contains(";")) { + dwPayload.input_content_type = payload.getSourceType().split(";")[0]; + } else { + dwPayload.input_content_type = payload.getSourceType(); + } + dwPayload.output_content_type = "${outputContentType}"; + + //TODO: Verify the spell conforms to Dataweave 2.x: https://docs.mulesoft.com/mule-runtime/4.4/migration-dataweave + dwPayload.spell = "${dwSpell}"; + dwPayload.input_data = payload.getPayload(); + String body; + + try { + requestBuilder = HttpRequest.newBuilder(new URI(url)); + ObjectMapper om = new ObjectMapper(); + body = om.writeValueAsString(dwPayload); + } catch (Exception e) { + System.out.println("Error sending request: " + e.toString()); + return null; + } + + requestBuilder.setHeader("content-type", "application/json"); + requestBuilder.setHeader("ce-specversion", "1.0"); + requestBuilder.setHeader("ce-source", payload.getSource()); + requestBuilder.setHeader("ce-type", "io.triggermesh.dataweave.transform"); + requestBuilder.setHeader("ce-id", payload.getId()); + + HttpRequest request = requestBuilder.POST(HttpRequest.BodyPublishers.ofString(body)).build(); + + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + // TODO: verify the response status and body + return response.body(); + } catch (Exception e) { + System.out.println("Error sending event: " + e.toString()); + return null; + } + } +} diff --git a/components/sbm-recipes-mule-to-boot/src/test/java/org/springframework/sbm/mule/actions/MuleToJavaDSLDwlTransformTest.java b/components/sbm-recipes-mule-to-boot/src/test/java/org/springframework/sbm/mule/actions/MuleToJavaDSLDwlTransformTest.java index 6db672baa..23d80615d 100644 --- a/components/sbm-recipes-mule-to-boot/src/test/java/org/springframework/sbm/mule/actions/MuleToJavaDSLDwlTransformTest.java +++ b/components/sbm-recipes-mule-to-boot/src/test/java/org/springframework/sbm/mule/actions/MuleToJavaDSLDwlTransformTest.java @@ -21,6 +21,12 @@ public class MuleToJavaDSLDwlTransformTest extends JavaDSLActionBaseTest { + // workaround to force-enable the TriggerMesh transform mode + private void enableTriggerMeshTransform() { + myAction.setMuleTriggerMeshTransformEnabled(true); + System.setProperty("sbm.muleTriggerMeshTransformEnabled", "true"); + } + private static final String muleXmlSetPayload = "\n" + "\n" + " p)\n" + - " .log(LoggingHandler.Level.INFO, \"payload to be sent: #[new String(payload)]\")\n" + - " .transform(DwlFlowTransform_2::transform)\n" + - " .log(LoggingHandler.Level.INFO, \"payload to be sent: #[new String(payload)]\")\n" + - " .get();\n" + - " }\n" + - "}"); + """ + package com.example.javadsl; + import org.springframework.context.annotation.Bean; + import org.springframework.context.annotation.Configuration; + import org.springframework.integration.dsl.IntegrationFlow; + import org.springframework.integration.dsl.IntegrationFlows; + import org.springframework.integration.handler.LoggingHandler; + import org.springframework.integration.http.dsl.Http; + + @Configuration + public class FlowConfigurations { + @Bean + IntegrationFlow dwlFlow() { + return IntegrationFlows.from(Http.inboundChannelAdapter("/dwl")).handle((p, h) -> p) + .log(LoggingHandler.Level.INFO, "payload to be sent: #[new String(payload)]") + .transform(DwlFlowTransform_2::transform) + .log(LoggingHandler.Level.INFO, "payload to be sent: #[new String(payload)]") + .get(); + } + }"""); assertThat(projectContext.getProjectJavaSources().list().get(1).print()) - .isEqualTo( - "package com.example.javadsl;\n" + - "\n" + - "public class DwlFlowTransform_2 {\n" + - " /*\n" + - " * TODO:\n" + - " *\n" + - " * Please add necessary transformation for below snippet\n" + - " * [%dw 1.0\n" + - " * %output application/json\n" + - " * ---\n" + - " * {\n" + - " * action_Code: 10,\n" + - " * returnCode: 20\n" + - " * }]\n" + - " * */\n" + - " public static DwlFlowTransform_2 transform(Object payload) {\n" + - "\n" + - " return new DwlFlowTransform_2();\n" + - " }\n" + - "}"); + .isEqualTo(""" + package com.example.javadsl; + + public class DwlFlowTransform_2 { + /* + * TODO: + * + * Please add necessary transformation for below snippet + * [%dw 1.0 + * %output application/json + * --- + * { + * action_Code: 10, + * returnCode: 20 + * }] + * */ + public static DwlFlowTransform_2 transform(Object payload) { + + return new DwlFlowTransform_2(); + } + }"""); + } + + @Test + public void shouldTranslateDwlTransformationWithMuleTriggerMeshTransformAndSetPayloadEnabled() { + enableTriggerMeshTransform(); + addXMLFileToResource(muleXmlSetPayload); + runAction(); + + assertThat(projectContext.getProjectJavaSources().list()).hasSize(3); + assertThat(getGeneratedJavaFile()) + .isEqualTo(""" + package com.example.javadsl; + import org.springframework.context.annotation.Bean; + import org.springframework.context.annotation.Configuration; + import org.springframework.integration.dsl.IntegrationFlow; + import org.springframework.integration.dsl.IntegrationFlows; + import org.springframework.integration.handler.LoggingHandler; + import org.springframework.integration.http.dsl.Http; + + @Configuration + public class FlowConfigurations { + @Bean + IntegrationFlow dwlFlow() { + return IntegrationFlows.from(Http.inboundChannelAdapter("/dwl")).handle((p, h) -> p) + .log(LoggingHandler.Level.INFO, "payload to be sent: #[new String(payload)]") + .handle((p, h) -> { + TmDwPayload dwPayload = new TmDwPayload(); + String contentType = "application/json"; + if (h.get("contentType") != null) { + contentType = h.get("contentType").toString(); + } + dwPayload.setId(h.getId().toString()); + dwPayload.setSourceType(contentType); + dwPayload.setSource(h.get("http_requestUrl").toString()); + dwPayload.setPayload(p.toString()); + return dwPayload; + }) + .transform(DwlFlowTransformTM_2::transform) + .log(LoggingHandler.Level.INFO, "payload to be sent: #[new String(payload)]") + .get(); + } + }"""); + assertThat(projectContext.getProjectJavaSources().list().get(1).print()) + .isEqualTo(""" + package com.example.javadsl; + import org.springframework.context.annotation.Configuration; + + import lombok.Data; + + /* Included with the baseline to support bridging between the Flow configuration and the translation implementation. */ + + @Data + public class TmDwPayload { + private String id; + private String source; + private String sourceType; + private String payload; + } + """ + ); + assertThat(projectContext.getProjectJavaSources().list().get(2).print()) + .isEqualTo(""" + package com.example.javadsl; + + import com.fasterxml.jackson.databind.ObjectMapper; + + import java.net.URI; + import java.net.http.HttpClient; + import java.net.http.HttpRequest; + import java.net.http.HttpResponse; + + public class DwlFlowTransformTM_2 { + public static class DataWeavePayload { + public String input_data; + public String spell; + public String input_content_type; + public String output_content_type; + }; + + public static String transform(TmDwPayload payload) { + String uuid = payload.getId(); + String url = System.getenv("K_SINK"); + HttpClient client = HttpClient.newHttpClient(); + HttpRequest.Builder requestBuilder; + DataWeavePayload dwPayload = new DataWeavePayload(); + + if (payload.getSourceType().contains(";")) { + dwPayload.input_content_type = payload.getSourceType().split(";")[0]; + } else { + dwPayload.input_content_type = payload.getSourceType(); + } + dwPayload.output_content_type = "application/json"; + + //TODO: Verify the spell conforms to Dataweave 2.x: https://docs.mulesoft.com/mule-runtime/4.4/migration-dataweave + dwPayload.spell = "%dw 1.0\\n%output application/json\\n---\\n{\\n action_Code: 10,\\n returnCode: 20\\n}"; + dwPayload.input_data = payload.getPayload(); + String body; + + try { + requestBuilder = HttpRequest.newBuilder(new URI(url)); + ObjectMapper om = new ObjectMapper(); + body = om.writeValueAsString(dwPayload); + } catch (Exception e) { + System.out.println("Error sending request: " + e.toString()); + return null; + } + + requestBuilder.setHeader("content-type", "application/json"); + requestBuilder.setHeader("ce-specversion", "1.0"); + requestBuilder.setHeader("ce-source", payload.getSource()); + requestBuilder.setHeader("ce-type", "io.triggermesh.dataweave.transform"); + requestBuilder.setHeader("ce-id", payload.getId()); + + HttpRequest request = requestBuilder.POST(HttpRequest.BodyPublishers.ofString(body)).build(); + + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + // TODO: verify the response status and body + return response.body(); + } catch (Exception e) { + System.out.println("Error sending event: " + e.toString()); + return null; + } + } + } + """); } @Test public void shouldTransformDWLWithFileWithSetPayload() { - final String dwlXMLWithExternalFile = "\n" + - "\n" + - "\n" + - " \n" + - " \n" + - "\n" + - " \n" + - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "\n" + - " \n" + - " \n" + - ""; + final String dwlXMLWithExternalFile = """ + + + + + + + + + + + + + + + + + + + """; addXMLFileToResource(dwlXMLWithExternalFile); runAction(); assertThat(projectContext.getProjectJavaSources().list()).hasSize(2); assertThat(getGeneratedJavaFile()) - .isEqualTo( - "package com.example.javadsl;\n" + - "import org.springframework.context.annotation.Bean;\n" + - "import org.springframework.context.annotation.Configuration;\n" + - "import org.springframework.integration.dsl.IntegrationFlow;\n" + - "import org.springframework.integration.dsl.IntegrationFlows;\n" + - "import org.springframework.integration.handler.LoggingHandler;\n" + - "import org.springframework.integration.http.dsl.Http;\n" + - "\n" + - "@Configuration\n" + - "public class FlowConfigurations {\n" + - " @Bean\n" + - " IntegrationFlow dwlFlow() {\n" + - " return IntegrationFlows.from(Http.inboundChannelAdapter(\"/dwl\")).handle((p, h) -> p)\n" + - " .log(LoggingHandler.Level.INFO, \"payload to be sent: #[new String(payload)]\")\n" + - " .transform(MapClientRiskRatingResponseTransform::transform)\n" + - " .log(LoggingHandler.Level.INFO, \"payload to be sent: #[new String(payload)]\")\n" + - " .get();\n" + - " }\n" + - "}"); + .isEqualTo(""" + package com.example.javadsl; + import org.springframework.context.annotation.Bean; + import org.springframework.context.annotation.Configuration; + import org.springframework.integration.dsl.IntegrationFlow; + import org.springframework.integration.dsl.IntegrationFlows; + import org.springframework.integration.handler.LoggingHandler; + import org.springframework.integration.http.dsl.Http; + + @Configuration + public class FlowConfigurations { + @Bean + IntegrationFlow dwlFlow() { + return IntegrationFlows.from(Http.inboundChannelAdapter("/dwl")).handle((p, h) -> p) + .log(LoggingHandler.Level.INFO, "payload to be sent: #[new String(payload)]") + .transform(MapClientRiskRatingResponseTransform::transform) + .log(LoggingHandler.Level.INFO, "payload to be sent: #[new String(payload)]") + .get(); + } + }"""); assertThat(projectContext.getProjectJavaSources().list().get(1).print()) - .isEqualTo( - "package com.example.javadsl;\n" + - "\n" + - "public class MapClientRiskRatingResponseTransform {\n" + - " /*\n" + - " * TODO:\n" + - " *\n" + - " * Please add necessary transformation for below snippet\n" + - " * from file dwl/mapClientRiskRatingResponse.dwl" + - " * */\n" + - " public static MapClientRiskRatingResponseTransform transform(Object payload) {\n" + - "\n" + - " return new MapClientRiskRatingResponseTransform();\n" + - " }\n" + - "}"); + .isEqualTo(""" + package com.example.javadsl; + + public class MapClientRiskRatingResponseTransform { + /* + * TODO: + * + * Please add necessary transformation for below snippet + * from file dwl/mapClientRiskRatingResponse.dwl * */ + public static MapClientRiskRatingResponseTransform transform(Object payload) { + + return new MapClientRiskRatingResponseTransform(); + } + }"""); } @Test public void shouldTranslateDWLTransformationWithOnlyOneSetVariable() { - String muleXMLSetVariable = "\n" + - "\n" + - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - ""; + String muleXMLSetVariable = """ + + + + + + + + + + + + + + + + """; addXMLFileToResource(muleXMLSetVariable); runAction(); assertThat(projectContext.getProjectJavaSources().list()).hasSize(1); assertThat(getGeneratedJavaFile()) - .isEqualTo( - "package com.example.javadsl;\n" + - "import org.springframework.context.annotation.Bean;\n" + - "import org.springframework.context.annotation.Configuration;\n" + - "import org.springframework.integration.dsl.IntegrationFlow;\n" + - "import org.springframework.integration.dsl.IntegrationFlows;\n" + - "import org.springframework.integration.handler.LoggingHandler;\n" + - "import org.springframework.integration.http.dsl.Http;\n" + - "\n" + - "@Configuration\n" + - "public class FlowConfigurations {\n" + - " @Bean\n" + - " IntegrationFlow dwlFlow() {\n" + - " return IntegrationFlows.from(Http.inboundChannelAdapter(\"/dwl\")).handle((p, h) -> p)\n" + - " // FIXME: No support for following DW transformation: \n" + - " .log(LoggingHandler.Level.INFO, \"Hello World: ${flowVars.temp}\")\n" + - " .get();\n" + - " }\n" + - "}"); + .isEqualTo(""" + package com.example.javadsl; + import org.springframework.context.annotation.Bean; + import org.springframework.context.annotation.Configuration; + import org.springframework.integration.dsl.IntegrationFlow; + import org.springframework.integration.dsl.IntegrationFlows; + import org.springframework.integration.handler.LoggingHandler; + import org.springframework.integration.http.dsl.Http; + + @Configuration + public class FlowConfigurations { + @Bean + IntegrationFlow dwlFlow() { + return IntegrationFlows.from(Http.inboundChannelAdapter("/dwl")).handle((p, h) -> p) + // FIXME: No support for following DW transformation: + .log(LoggingHandler.Level.INFO, "Hello World: ${flowVars.temp}") + .get(); + } + }"""); } @Test public void shouldNotErrorWhenDWLFileHasDash() { - final String dwlExternalFileSpecialChars = "\n" + - "\n" + - "\n" + - " \n" + - " \n" + - "\n" + - " \n" + - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "\n" + - " \n" + - " \n" + - ""; + final String dwlExternalFileSpecialChars = """ + + + + + + + + + + + + + + + + + + + """; addXMLFileToResource(dwlExternalFileSpecialChars); runAction(); assertThat(projectContext.getProjectJavaSources().list()).hasSize(2); assertThat(getGeneratedJavaFile()) - .isEqualTo( - "package com.example.javadsl;\n" + - "import org.springframework.context.annotation.Bean;\n" + - "import org.springframework.context.annotation.Configuration;\n" + - "import org.springframework.integration.dsl.IntegrationFlow;\n" + - "import org.springframework.integration.dsl.IntegrationFlows;\n" + - "import org.springframework.integration.handler.LoggingHandler;\n" + - "import org.springframework.integration.http.dsl.Http;\n" + - "\n" + - "@Configuration\n" + - "public class FlowConfigurations {\n" + - " @Bean\n" + - " IntegrationFlow dwlFlow() {\n" + - " return IntegrationFlows.from(Http.inboundChannelAdapter(\"/dwl\")).handle((p, h) -> p)\n" + - " .log(LoggingHandler.Level.INFO, \"payload to be sent: #[new String(payload)]\")\n" + - " .transform(MapclientriskratingresponseTransform::transform)\n" + - " .log(LoggingHandler.Level.INFO, \"payload to be sent: #[new String(payload)]\")\n" + - " .get();\n" + - " }\n" + - "}"); + .isEqualTo(""" + package com.example.javadsl; + import org.springframework.context.annotation.Bean; + import org.springframework.context.annotation.Configuration; + import org.springframework.integration.dsl.IntegrationFlow; + import org.springframework.integration.dsl.IntegrationFlows; + import org.springframework.integration.handler.LoggingHandler; + import org.springframework.integration.http.dsl.Http; + + @Configuration + public class FlowConfigurations { + @Bean + IntegrationFlow dwlFlow() { + return IntegrationFlows.from(Http.inboundChannelAdapter("/dwl")).handle((p, h) -> p) + .log(LoggingHandler.Level.INFO, "payload to be sent: #[new String(payload)]") + .transform(MapclientriskratingresponseTransform::transform) + .log(LoggingHandler.Level.INFO, "payload to be sent: #[new String(payload)]") + .get(); + } + }"""); assertThat(projectContext.getProjectJavaSources().list().get(1).print()) - .isEqualTo( - "package com.example.javadsl;\n" + - "\n" + - "public class MapclientriskratingresponseTransform {\n" + - " /*\n" + - " * TODO:\n" + - " *\n" + - " * Please add necessary transformation for below snippet\n" + - " * from file dwl/map-client-risk-rating-response.dwl * */\n" + - " public static MapclientriskratingresponseTransform transform(Object payload) {\n" + - "\n" + - " return new MapclientriskratingresponseTransform();\n" + - " }\n" + - "}"); + .isEqualTo(""" + package com.example.javadsl; + + public class MapclientriskratingresponseTransform { + /* + * TODO: + * + * Please add necessary transformation for below snippet + * from file dwl/map-client-risk-rating-response.dwl * */ + public static MapclientriskratingresponseTransform transform(Object payload) { + + return new MapclientriskratingresponseTransform(); + } + }"""); } @Test public void multipleDWLTransformInSameFlowShouldProduceMultipleClasses() { - final String xml = " " + - " " + - " " + - " " + - " " + - " " + - " " + - " " + - " " + - " " + - " " + - ""; + final String xml = """ + + + + + + + + + + + + + """; addXMLFileToResource(xml); runAction(); assertThat(projectContext.getProjectJavaSources().list()).hasSize(3); assertThat(projectContext.getProjectJavaSources().list().get(0).getTypes().get(0).toString()).isEqualTo("com.example.javadsl.FlowConfigurations"); - assertThat(projectContext.getProjectJavaSources().list().get(1).getTypes().get(0).toString()).isEqualTo("com.example.javadsl.MultipleTransformsTransform_2"); - assertThat(projectContext.getProjectJavaSources().list().get(2).getTypes().get(0).toString()).isEqualTo("com.example.javadsl.MultipleTransformsTransform_0"); + assertThat(projectContext.getProjectJavaSources().list().get(2).getTypes().get(0).toString()).isEqualTo("com.example.javadsl.MultipleTransformsTransform_2"); + assertThat(projectContext.getProjectJavaSources().list().get(1).getTypes().get(0).toString()).isEqualTo("com.example.javadsl.MultipleTransformsTransform_0"); + } + + @Test + public void multipleDWLTransformInSameFlowShouldProduceMultipleClassesWithTriggerMeshEnabled() { + enableTriggerMeshTransform(); + + final String xml = """ + + + + + + + + + + + + + """; + + addXMLFileToResource(xml); + runAction(); + + assertThat(projectContext.getProjectJavaSources().list()).hasSize(4); + assertThat(projectContext.getProjectJavaSources().list().get(0).getTypes().get(0).toString()).isEqualTo("com.example.javadsl.FlowConfigurations"); + assertThat(projectContext.getProjectJavaSources().list().get(1).getTypes().get(0).toString()).isEqualTo("com.example.javadsl.TmDwPayload"); + assertThat(projectContext.getProjectJavaSources().list().get(2).getTypes().get(0).toString()).isEqualTo("com.example.javadsl.MultipleTransformsTransformTM_2"); + assertThat(projectContext.getProjectJavaSources().list().get(3).getTypes().get(0).toString()).isEqualTo("com.example.javadsl.MultipleTransformsTransformTM_0"); } }