From 3e8a0cc2e98b2f013f95fbd11569b0ef72975eb2 Mon Sep 17 00:00:00 2001 From: Gabriele Cardosi Date: Thu, 3 Oct 2024 12:04:33 +0200 Subject: [PATCH] [incubator-kie-issues#1473] Introduce Transactional annotation, conditionally deleted (#3671) * [incubator-kie-issues#1473] WIP - Implemented transactional imports and annotations with optional removal. Add unit tests. Small refactoring to allow unit testing/ TDD * [incubator-kie-issues#1473] Simplified following suggestions. Now the annotation is conditionally add during code generation. * [incubator-kie-issues#1473] Implemented KogitoContextTestUtils#restContextBuilders for rest-specific tests. Enforced exclusion of "Java" context from processes' rest generation. * [incubator-kie-issues#1473] Set log level to debug inside manageTransactional * [incubator-kie-issues#1473] Add spring-tx dependency to jbpm-spring-boot-starter to be always available for springboot projects * [incubator-kie-issues#1473] Add quarkus-narayana-jta dependency to kogito-quarkus-workflow-common to be always available for quarkus projects * [incubator-kie-issues#1473] Add quarkus-narayana-jta-deployment dependency * [incubator-kie-issues#1473] Introduce nasty workaround (delay) to avoid ARJUNA issues with nested threads * [incubator-kie-issues#1473] Disabling transaction for sonataflow-quarkus-integration-test * [incubator-kie-issues#1473] Conditionally add the thread sleep. Fix kogito.processes.transactionEnabled variable * [incubator-kie-issues#1473] Importing new wait strategy. * [incubator-kie-issues#1473] WIP - Experimenting Thread.join inside transaction (instead of Thread.sleep) * [incubator-kie-issues#1473] WIP - fix formatting * [incubator-kie-issues#1473] WIP - commented out conditionally excluded serverless transaction. Disable transaction workaround. * [incubator-kie-issues#1473] WIP - commented out conditionally excluded serverless transaction. Disable transaction workaround. * [incubator-kie-issues#1473] Disabling transaction for serverless * [incubator-kie-issues#1473] Fix formatting * [incubator-kie-issues#1473] Fix test * [incubator-kie-issues#1473] Fixed as per PR suggestion. Using context.hasRest() instead of name.equals("Java") to check for rest endpoint creation. Implemented MockQuarkusKogitoBuildContext and MockSpringBootKogitoBuildContext to override the hasRest method during tests. --------- Co-authored-by: Gabriele-Cardosi --- .../impl/MockQuarkusKogitoBuildContext.java | 51 +++++ .../MockSpringBootKogitoBuildContext.java | 50 +++++ .../api/utils/KogitoContextTestUtils.java | 12 ++ .../codegen/process/ProcessCodegen.java | 21 +- .../process/ProcessResourceGenerator.java | 199 +++++++++++++----- .../RestResourceSpringTemplate.java | 3 +- .../process/ProcessResourceGeneratorTest.java | 116 ++++++++-- .../events/CodegenMessageStartEventTest.java | 4 +- .../process/events/CodegenUserTaskTest.java | 2 +- .../pom.xml | 4 + .../pom.xml | 4 + .../kogito-quarkus-workflow-common/pom.xml | 4 + .../pom.xml | 4 + 13 files changed, 397 insertions(+), 77 deletions(-) create mode 100644 kogito-codegen-modules/kogito-codegen-api/src/test/java/org/kie/kogito/codegen/api/context/impl/MockQuarkusKogitoBuildContext.java create mode 100644 kogito-codegen-modules/kogito-codegen-api/src/test/java/org/kie/kogito/codegen/api/context/impl/MockSpringBootKogitoBuildContext.java diff --git a/kogito-codegen-modules/kogito-codegen-api/src/test/java/org/kie/kogito/codegen/api/context/impl/MockQuarkusKogitoBuildContext.java b/kogito-codegen-modules/kogito-codegen-api/src/test/java/org/kie/kogito/codegen/api/context/impl/MockQuarkusKogitoBuildContext.java new file mode 100644 index 00000000000..9972c2d7f0a --- /dev/null +++ b/kogito-codegen-modules/kogito-codegen-api/src/test/java/org/kie/kogito/codegen/api/context/impl/MockQuarkusKogitoBuildContext.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.kie.kogito.codegen.api.context.impl; + +/** + * Mocked QuarkusKogitoBuildContext to provide hasRest() = true + * during test + */ +public class MockQuarkusKogitoBuildContext extends QuarkusKogitoBuildContext { + + protected MockQuarkusKogitoBuildContext(MockQuarkusKogitoBuildContextBuilder builder) { + super(builder); + } + + public static Builder builder() { + return new MockQuarkusKogitoBuildContextBuilder(); + } + + @Override + public boolean hasRest() { + return true; + } + + protected static class MockQuarkusKogitoBuildContextBuilder extends QuarkusKogitoBuildContextBuilder { + + protected MockQuarkusKogitoBuildContextBuilder() { + } + + @Override + public MockQuarkusKogitoBuildContext build() { + return new MockQuarkusKogitoBuildContext(this); + } + + } +} \ No newline at end of file diff --git a/kogito-codegen-modules/kogito-codegen-api/src/test/java/org/kie/kogito/codegen/api/context/impl/MockSpringBootKogitoBuildContext.java b/kogito-codegen-modules/kogito-codegen-api/src/test/java/org/kie/kogito/codegen/api/context/impl/MockSpringBootKogitoBuildContext.java new file mode 100644 index 00000000000..32a865ebd1f --- /dev/null +++ b/kogito-codegen-modules/kogito-codegen-api/src/test/java/org/kie/kogito/codegen/api/context/impl/MockSpringBootKogitoBuildContext.java @@ -0,0 +1,50 @@ +/* + * 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.kie.kogito.codegen.api.context.impl; + +/** + * Mocked SpringBootKogitoBuildContext to provide hasRest() = true + * during test + */ +public class MockSpringBootKogitoBuildContext extends SpringBootKogitoBuildContext { + + protected MockSpringBootKogitoBuildContext(MockSpringBootKogitoBuildContextBuilder builder) { + super(builder); + } + + public static Builder builder() { + return new MockSpringBootKogitoBuildContextBuilder(); + } + + @Override + public boolean hasRest() { + return true; + } + + protected static class MockSpringBootKogitoBuildContextBuilder extends SpringBootKogitoBuildContextBuilder { + + protected MockSpringBootKogitoBuildContextBuilder() { + } + + @Override + public MockSpringBootKogitoBuildContext build() { + return new MockSpringBootKogitoBuildContext(this); + } + } +} \ No newline at end of file diff --git a/kogito-codegen-modules/kogito-codegen-api/src/test/java/org/kie/kogito/codegen/api/utils/KogitoContextTestUtils.java b/kogito-codegen-modules/kogito-codegen-api/src/test/java/org/kie/kogito/codegen/api/utils/KogitoContextTestUtils.java index e96c2c7a347..3c7a273ea18 100644 --- a/kogito-codegen-modules/kogito-codegen-api/src/test/java/org/kie/kogito/codegen/api/utils/KogitoContextTestUtils.java +++ b/kogito-codegen-modules/kogito-codegen-api/src/test/java/org/kie/kogito/codegen/api/utils/KogitoContextTestUtils.java @@ -25,6 +25,8 @@ import org.junit.jupiter.params.provider.Arguments; import org.kie.kogito.codegen.api.context.KogitoBuildContext; import org.kie.kogito.codegen.api.context.impl.JavaKogitoBuildContext; +import org.kie.kogito.codegen.api.context.impl.MockQuarkusKogitoBuildContext; +import org.kie.kogito.codegen.api.context.impl.MockSpringBootKogitoBuildContext; import org.kie.kogito.codegen.api.context.impl.QuarkusKogitoBuildContext; import org.kie.kogito.codegen.api.context.impl.SpringBootKogitoBuildContext; @@ -44,6 +46,16 @@ public static Stream contextBuilders() { Arguments.of(SpringBootKogitoBuildContext.builder())); } + /** + * + * @return Mocked QuarkusKogitoBuildContext and SpringBootKogitoBuildContext providing hasRest() = true + */ + public static Stream restContextBuilders() { + return Stream.of( + Arguments.of(MockQuarkusKogitoBuildContext.builder()), + Arguments.of(MockSpringBootKogitoBuildContext.builder())); + } + public static Predicate mockClassAvailabilityResolver(Collection includedClasses, Collection excludedClasses) { return mockClassAvailabilityResolver(includedClasses, excludedClasses, KogitoContextTestUtils.class.getClassLoader()); } diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/process/ProcessCodegen.java b/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/process/ProcessCodegen.java index 6f5ba05f7a5..c360ec5e8c2 100644 --- a/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/process/ProcessCodegen.java +++ b/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/process/ProcessCodegen.java @@ -76,6 +76,7 @@ import static java.lang.String.format; import static java.util.stream.Collectors.toList; +import static org.kie.kogito.codegen.process.ProcessResourceGenerator.TRANSACTION_ENABLED; import static org.kie.kogito.grafana.GrafanaConfigurationWriter.buildDashboardName; import static org.kie.kogito.grafana.GrafanaConfigurationWriter.generateOperationalDashboard; import static org.kie.kogito.internal.utils.ConversionUtils.sanitizeClassName; @@ -352,20 +353,23 @@ protected Collection internalGenerate() { ProcessMetaData metaData = processIdToMetadata.get(workFlowProcess.getId()); - //Creating and adding the ResourceGenerator - ProcessResourceGenerator processResourceGenerator = new ProcessResourceGenerator( + //Creating and adding the ResourceGenerator for REST generation + if (context().hasRest()) { + ProcessResourceGenerator processResourceGenerator = new ProcessResourceGenerator( context(), workFlowProcess, modelClassGenerator.className(), execModelGen.className(), applicationCanonicalName()); - processResourceGenerator + processResourceGenerator .withUserTasks(processIdToUserTaskModel.get(workFlowProcess.getId())) .withSignals(metaData.getSignals()) - .withTriggers(metaData.isStartable(), metaData.isDynamic(), metaData.getTriggers()); + .withTriggers(metaData.isStartable(), metaData.isDynamic(), metaData.getTriggers()) + .withTransaction(isTransactionEnabled()); - rgs.add(processResourceGenerator); + rgs.add(processResourceGenerator); + } if (metaData.getTriggers() != null) { @@ -468,7 +472,7 @@ protected Collection internalGenerate() { svgs.keySet().stream().forEach(key -> storeFile(GeneratedFileType.INTERNAL_RESOURCE, "META-INF/processSVG/" + key + ".svg", svgs.get(key))); } - if (context().hasRESTForGenerator(this)) { + if (context().hasRest() && context().hasRESTForGenerator(this)) { final ProcessCloudEventMetaFactoryGenerator topicsGenerator = new ProcessCloudEventMetaFactoryGenerator(context(), processExecutableModelGenerators); storeFile(REST_TYPE, topicsGenerator.generatedFilePath(), topicsGenerator.generate()); @@ -504,6 +508,11 @@ protected Collection internalGenerate() { return generatedFiles; } + protected boolean isTransactionEnabled() { + String processTransactionProperty = String.format("kogito.%s.%s", GENERATOR_NAME, TRANSACTION_ENABLED); + return "true".equalsIgnoreCase(context().getApplicationProperty(processTransactionProperty).orElse("true")); + } + private void storeFile(GeneratedFileType type, String path, String source) { if (generatedFiles.stream().anyMatch(f -> path.equals(f.relativePath()))) { LOGGER.warn("There's already a generated file named {} to be compiled. Ignoring.", path); diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/process/ProcessResourceGenerator.java b/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/process/ProcessResourceGenerator.java index 90ab0277b06..165477103f0 100644 --- a/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/process/ProcessResourceGenerator.java +++ b/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/process/ProcessResourceGenerator.java @@ -29,6 +29,8 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; +import org.drools.codegen.common.di.DependencyInjectionAnnotator; +import org.drools.codegen.common.rest.RestAnnotator; import org.drools.util.StringUtils; import org.jbpm.compiler.canonical.ProcessToExecModelGenerator; import org.jbpm.compiler.canonical.TriggerMetaData; @@ -44,6 +46,8 @@ import org.kie.kogito.codegen.core.GeneratorConfig; import org.kie.kogito.internal.process.runtime.KogitoWorkflowProcess; import org.kie.kogito.internal.utils.ConversionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.Modifier.Keyword; @@ -73,6 +77,15 @@ */ public class ProcessResourceGenerator { + /** + * Flag used to configure transaction enablement. Default to true + */ + public static final String TRANSACTION_ENABLED = "transactionEnabled"; + + static final String INVALID_CONTEXT_TEMPLATE = "ProcessResourceGenerator can't be used for context without Rest %s"; + + private static final Logger LOG = LoggerFactory.getLogger(ProcessResourceGenerator.class); + private static final String REST_TEMPLATE_NAME = "RestResource"; private static final String REACTIVE_REST_TEMPLATE_NAME = "ReactiveRestResource"; private static final String REST_USER_TASK_TEMPLATE_NAME = "RestResourceUserTask"; @@ -93,6 +106,7 @@ public class ProcessResourceGenerator { private boolean startable; private boolean dynamic; + private boolean transactionEnabled; private List triggers; private List userTasks; @@ -106,6 +120,9 @@ public ProcessResourceGenerator( String modelfqcn, String processfqcn, String appCanonicalName) { + if (!context.hasRest()) { + throw new IllegalArgumentException(String.format(INVALID_CONTEXT_TEMPLATE, context.name())); + } this.context = context; this.process = process; this.processId = process.getId(); @@ -134,6 +151,11 @@ public ProcessResourceGenerator withTriggers(boolean startable, boolean dynamic, return this; } + public ProcessResourceGenerator withTransaction(boolean transactionEnabled) { + this.transactionEnabled = transactionEnabled; + return this; + } + public String getTaskModelFactory() { return taskModelFactoryUnit.toString(); } @@ -149,27 +171,104 @@ public String className() { protected String getRestTemplateName() { boolean isReactiveGenerator = "reactive".equals(context.getApplicationProperty(GeneratorConfig.KOGITO_REST_RESOURCE_TYPE_PROP) .orElse("")); - boolean isQuarkus = context.name().equals(QuarkusKogitoBuildContext.CONTEXT_NAME); + return isQuarkus() && isReactiveGenerator ? REACTIVE_REST_TEMPLATE_NAME : REST_TEMPLATE_NAME; + } - return isQuarkus && isReactiveGenerator ? REACTIVE_REST_TEMPLATE_NAME : REST_TEMPLATE_NAME; + protected boolean isQuarkus() { + return context.name().equals(QuarkusKogitoBuildContext.CONTEXT_NAME); } public String generate() { - TemplatedGenerator.Builder templateBuilder = TemplatedGenerator.builder() - .withFallbackContext(QuarkusKogitoBuildContext.CONTEXT_NAME); - CompilationUnit clazz = templateBuilder.build(context, getRestTemplateName()) - .compilationUnitOrThrow(); - clazz.setPackageDeclaration(process.getPackageName()); - clazz.addImport(modelfqcn); - clazz.addImport(modelfqcn + "Output"); - clazz.addImport(modelfqcn + "Input"); - ClassOrInterfaceDeclaration template = clazz + return getCompilationUnit().toString(); + } + + protected CompilationUnit getCompilationUnit() { + TemplatedGenerator.Builder templateBuilder = createTemplatedGeneratorBuilder(); + CompilationUnit toReturn = createCompilationUnit(templateBuilder); + addPackageAndImports(toReturn); + ClassOrInterfaceDeclaration template = toReturn .findFirst(ClassOrInterfaceDeclaration.class) .orElseThrow(() -> new NoSuchElementException("Compilation unit doesn't contain a class or interface declaration!")); template.setName(resourceClazzName); AtomicInteger index = new AtomicInteger(0); //Generate signals endpoints + generateSignalsEndpoints(templateBuilder, template, index); + + // security must be applied before user tasks are added to make sure that user task + // endpoints are not security annotated as they should restrict access based on user assignments + securityAnnotated(template); + + Map typeInterpolations = new HashMap<>(); + taskModelFactoryUnit = parse(this.getClass().getResourceAsStream("/class-templates/TaskModelFactoryTemplate" + + ".java")); + String taskModelFactorySimpleClassName = + sanitizeClassName(ProcessToExecModelGenerator.extractProcessId(processId) + "_" + "TaskModelFactory"); + taskModelFactoryUnit.setPackageDeclaration(process.getPackageName()); + taskModelFactoryClassName = process.getPackageName() + "." + taskModelFactorySimpleClassName; + ClassOrInterfaceDeclaration taskModelFactoryClass = + taskModelFactoryUnit.findFirst(ClassOrInterfaceDeclaration.class).orElseThrow(IllegalStateException::new); + taskModelFactoryClass.setName(taskModelFactorySimpleClassName); + typeInterpolations.put("$TaskModelFactory$", taskModelFactoryClassName); + + manageUserTasks(templateBuilder, template, taskModelFactoryClass, index); + + typeInterpolations.put("$Clazz$", resourceClazzName); + typeInterpolations.put("$Type$", dataClazzName); + template.findAll(StringLiteralExpr.class).forEach(this::interpolateStrings); + template.findAll(ClassOrInterfaceType.class).forEach(cls -> interpolateTypes(cls, typeInterpolations)); + + TagResourceGenerator.addTags(toReturn, process, context); + + template.findAll(MethodDeclaration.class).forEach(this::interpolateMethods); + + if (context.hasDI()) { + template.findAll(FieldDeclaration.class, + CodegenUtils::isProcessField).forEach(fd -> context.getDependencyInjectionAnnotator().withNamedInjection(fd, processId)); + } else { + template.findAll(FieldDeclaration.class, + CodegenUtils::isProcessField).forEach(this::initializeProcessField); + } + + // if triggers are not empty remove createResource method as there is another trigger to start process instances + if ((!startable && !dynamic) || !isPublic()) { + Optional createResourceMethod = + template.findFirst(MethodDeclaration.class).filter(md -> md.getNameAsString().equals( + "createResource_" + processName)); + createResourceMethod.ifPresent(template::remove); + } + + if (context.hasDI()) { + context.getDependencyInjectionAnnotator().withApplicationComponent(template); + } + + enableValidation(template); + + manageTransactional(toReturn); + + template.getMembers().sort(new BodyDeclarationComparator()); + return toReturn; + } + + protected TemplatedGenerator.Builder createTemplatedGeneratorBuilder() { + return TemplatedGenerator.builder() + .withFallbackContext(QuarkusKogitoBuildContext.CONTEXT_NAME); + } + + protected CompilationUnit createCompilationUnit(TemplatedGenerator.Builder templateBuilder) { + return templateBuilder.build(context, getRestTemplateName()) + .compilationUnitOrThrow(); + } + + protected void addPackageAndImports(CompilationUnit compilationUnit) { + compilationUnit.setPackageDeclaration(process.getPackageName()); + compilationUnit.addImport(modelfqcn); + compilationUnit.addImport(modelfqcn + "Output"); + compilationUnit.addImport(modelfqcn + "Input"); + } + + protected void generateSignalsEndpoints(TemplatedGenerator.Builder templateBuilder, + ClassOrInterfaceDeclaration template, AtomicInteger index) { Optional.ofNullable(signals) .ifPresent(signalsMap -> { //using template class to the endpoints generation @@ -265,20 +364,10 @@ public String generate() { }); }); }); + } - // security must be applied before user tasks are added to make sure that user task - // endpoints are not security annotated as they should restrict access based on user assignments - securityAnnotated(template); - - Map typeInterpolations = new HashMap<>(); - taskModelFactoryUnit = parse(this.getClass().getResourceAsStream("/class-templates/TaskModelFactoryTemplate.java")); - String taskModelFactorySimpleClassName = sanitizeClassName(ProcessToExecModelGenerator.extractProcessId(processId) + "_" + "TaskModelFactory"); - taskModelFactoryUnit.setPackageDeclaration(process.getPackageName()); - taskModelFactoryClassName = process.getPackageName() + "." + taskModelFactorySimpleClassName; - ClassOrInterfaceDeclaration taskModelFactoryClass = taskModelFactoryUnit.findFirst(ClassOrInterfaceDeclaration.class).orElseThrow(IllegalStateException::new); - taskModelFactoryClass.setName(taskModelFactorySimpleClassName); - typeInterpolations.put("$TaskModelFactory$", taskModelFactoryClassName); - + protected void manageUserTasks(TemplatedGenerator.Builder templateBuilder, ClassOrInterfaceDeclaration template, + ClassOrInterfaceDeclaration taskModelFactoryClass, AtomicInteger index) { if (userTasks != null && !userTasks.isEmpty()) { CompilationUnit userTaskClazz = templateBuilder.build(context, REST_USER_TASK_TEMPLATE_NAME).compilationUnitOrThrow(); @@ -310,44 +399,42 @@ public String generate() { template.findAll(MethodDeclaration.class) .stream() .filter(md -> md.getNameAsString().equals(SIGNAL_METHOD_PREFFIX + methodSuffix)) - .collect(Collectors.toList()).forEach(template::remove); + .forEach(template::remove); } switchExpr.getEntries().add(0, userTask.getModelSwitchEntry()); } } + } - typeInterpolations.put("$Clazz$", resourceClazzName); - typeInterpolations.put("$Type$", dataClazzName); - template.findAll(StringLiteralExpr.class).forEach(this::interpolateStrings); - template.findAll(ClassOrInterfaceType.class).forEach(cls -> interpolateTypes(cls, typeInterpolations)); - - TagResourceGenerator.addTags(clazz, process, context); - - template.findAll(MethodDeclaration.class).forEach(this::interpolateMethods); - - if (context.hasDI()) { - template.findAll(FieldDeclaration.class, - CodegenUtils::isProcessField).forEach(fd -> context.getDependencyInjectionAnnotator().withNamedInjection(fd, processId)); - } else { - template.findAll(FieldDeclaration.class, - CodegenUtils::isProcessField).forEach(this::initializeProcessField); - } - - // if triggers are not empty remove createResource method as there is another trigger to start process instances - if ((!startable && !dynamic) || !isPublic()) { - Optional createResourceMethod = template.findFirst(MethodDeclaration.class).filter(md -> md.getNameAsString().equals("createResource_" + processName)); - createResourceMethod.ifPresent(template::remove); - } - - if (context.hasDI()) { - context.getDependencyInjectionAnnotator().withApplicationComponent(template); + /** + * Conditionally add the Transactional annotation + * + * @param compilationUnit + * + */ + protected void manageTransactional(CompilationUnit compilationUnit) { + if (transactionEnabled && context.hasDI() && !isServerless()) { // disabling transaction for serverless + LOG.debug("Transaction is enabled, adding annotations..."); + DependencyInjectionAnnotator dependencyInjectionAnnotator = context.getDependencyInjectionAnnotator(); + getRestMethods(compilationUnit) + .forEach(dependencyInjectionAnnotator::withTransactional); } + } - enableValidation(template); - - template.getMembers().sort(new BodyDeclarationComparator()); - return clazz.toString(); + /** + * Retrieves all the Rest endpoint MethodDeclarations from the given + * CompilationUnit + * + * @param compilationUnit + * @return + */ + protected Collection getRestMethods(CompilationUnit compilationUnit) { + RestAnnotator restAnnotator = context.getRestAnnotator(); + return compilationUnit.findAll(MethodDeclaration.class) + .stream() + .filter(restAnnotator::isRestAnnotated) + .toList(); } private void securityAnnotated(ClassOrInterfaceDeclaration template) { @@ -468,4 +555,8 @@ public String generatedFilePath() { protected boolean isPublic() { return KogitoWorkflowProcess.PUBLIC_VISIBILITY.equalsIgnoreCase(process.getVisibility()); } + + protected boolean isServerless() { + return KogitoWorkflowProcess.SW_TYPE.equalsIgnoreCase(process.getType()); + } } diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceSpringTemplate.java b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceSpringTemplate.java index 9c15e01b473..190a38af3b8 100644 --- a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceSpringTemplate.java +++ b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceSpringTemplate.java @@ -43,6 +43,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -55,8 +56,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ResponseStatusException; -import org.springframework.http.ResponseEntity; -import org.springframework.http.HttpStatus; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/test/java/org/kie/kogito/codegen/process/ProcessResourceGeneratorTest.java b/kogito-codegen-modules/kogito-codegen-processes/src/test/java/org/kie/kogito/codegen/process/ProcessResourceGeneratorTest.java index 57b4fa9a49a..52d1665166a 100644 --- a/kogito-codegen-modules/kogito-codegen-processes/src/test/java/org/kie/kogito/codegen/process/ProcessResourceGeneratorTest.java +++ b/kogito-codegen-modules/kogito-codegen-processes/src/test/java/org/kie/kogito/codegen/process/ProcessResourceGeneratorTest.java @@ -24,14 +24,17 @@ import java.util.Optional; import java.util.function.Predicate; +import org.assertj.core.api.ListAssert; import org.drools.io.FileSystemResource; import org.jbpm.compiler.canonical.ProcessMetaData; import org.jbpm.compiler.canonical.ProcessToExecModelGenerator; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.kie.api.definition.process.Process; import org.kie.kogito.codegen.api.AddonsConfig; import org.kie.kogito.codegen.api.context.KogitoBuildContext; +import org.kie.kogito.codegen.api.context.impl.JavaKogitoBuildContext; import org.kie.kogito.internal.process.runtime.KogitoWorkflowProcess; import com.github.javaparser.StaticJavaParser; @@ -45,14 +48,29 @@ import com.github.javaparser.ast.expr.StringLiteralExpr; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.kie.kogito.codegen.process.ProcessResourceGenerator.INVALID_CONTEXT_TEMPLATE; class ProcessResourceGeneratorTest { + private static final String SIGNAL_METHOD = "signal_"; private static final List JAVA_AND_QUARKUS_REST_ANNOTATIONS = List.of("DELETE", "GET", "POST"); private static final List SPRING_BOOT_REST_ANNOTATIONS = List.of("DeleteMapping", "GetMapping", "PostMapping"); + @Test + void testProcessResourceGeneratorForJava() { + KogitoBuildContext.Builder contextBuilder = JavaKogitoBuildContext.builder(); + String fileName = "src/test/resources/startsignal/StartSignalEventNoPayload.bpmn2"; // not relevant + boolean transactionEnabled = true; // not relevant + String expectedMessage = String.format(INVALID_CONTEXT_TEMPLATE, JavaKogitoBuildContext.CONTEXT_NAME); + assertThatThrownBy(() -> getProcessResourceGenerator(contextBuilder, fileName, + transactionEnabled)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(expectedMessage); + } + @ParameterizedTest - @MethodSource("org.kie.kogito.codegen.api.utils.KogitoContextTestUtils#contextBuilders") + @MethodSource("org.kie.kogito.codegen.api.utils.KogitoContextTestUtils#restContextBuilders") void testGenerateProcessWithDocumentation(KogitoBuildContext.Builder contextBuilder) { String fileName = "src/test/resources/ProcessWithDocumentation.bpmn"; String expectedSummary = "This is the documentation"; @@ -62,7 +80,7 @@ void testGenerateProcessWithDocumentation(KogitoBuildContext.Builder contextBuil } @ParameterizedTest - @MethodSource("org.kie.kogito.codegen.api.utils.KogitoContextTestUtils#contextBuilders") + @MethodSource("org.kie.kogito.codegen.api.utils.KogitoContextTestUtils#restContextBuilders") void testGenerateProcessWithoutDocumentation(KogitoBuildContext.Builder contextBuilder) { String fileName = "src/test/resources/ProcessWithoutDocumentation.bpmn"; String expectedSummary = "ProcessWithoutDocumentation"; @@ -72,7 +90,7 @@ void testGenerateProcessWithoutDocumentation(KogitoBuildContext.Builder contextB } @ParameterizedTest - @MethodSource("org.kie.kogito.codegen.api.utils.KogitoContextTestUtils#contextBuilders") + @MethodSource("org.kie.kogito.codegen.api.utils.KogitoContextTestUtils#restContextBuilders") void testGenerateBoundarySignalEventOnTask(KogitoBuildContext.Builder contextBuilder) { String fileName = "src/test/resources/signalevent/BoundarySignalEventOnTask.bpmn2"; @@ -88,7 +106,7 @@ void testGenerateBoundarySignalEventOnTask(KogitoBuildContext.Builder contextBui } @ParameterizedTest - @MethodSource("org.kie.kogito.codegen.api.utils.KogitoContextTestUtils#contextBuilders") + @MethodSource("org.kie.kogito.codegen.api.utils.KogitoContextTestUtils#restContextBuilders") void testGenerateStartSignalEventStringPayload(KogitoBuildContext.Builder contextBuilder) { String fileName = "src/test/resources/startsignal/StartSignalEventStringPayload.bpmn2"; String signalName = "start"; @@ -121,7 +139,7 @@ void testGenerateStartSignalEventStringPayload(KogitoBuildContext.Builder contex } @ParameterizedTest - @MethodSource("org.kie.kogito.codegen.api.utils.KogitoContextTestUtils#contextBuilders") + @MethodSource("org.kie.kogito.codegen.api.utils.KogitoContextTestUtils#restContextBuilders") void testGenerateStartSignalEventNoPayload(KogitoBuildContext.Builder contextBuilder) { String fileName = "src/test/resources/startsignal/StartSignalEventNoPayload.bpmn2"; String signalName = "start"; @@ -152,6 +170,68 @@ void testGenerateStartSignalEventNoPayload(KogitoBuildContext.Builder contextBui .forEach(method -> assertMethodOutputModelType(method, outputType)); } + @ParameterizedTest + @MethodSource("org.kie.kogito.codegen.api.utils.KogitoContextTestUtils#restContextBuilders") + void testManageTransactionalEnabled(KogitoBuildContext.Builder contextBuilder) { + String fileName = "src/test/resources/startsignal/StartSignalEventNoPayload.bpmn2"; + + boolean transactionEnabled = true; + ProcessResourceGenerator processResourceGenerator = getProcessResourceGenerator(contextBuilder, fileName, + transactionEnabled); + CompilationUnit compilationUnit = + processResourceGenerator.createCompilationUnit(processResourceGenerator.createTemplatedGeneratorBuilder()); + assertThat(compilationUnit).isNotNull(); + KogitoBuildContext kogitoBuildContext = contextBuilder.build(); + Collection restEndpoints = processResourceGenerator.getRestMethods(compilationUnit); + // before processResourceGenerator.manageTransactional, the annotation is not there + testTransaction(restEndpoints, kogitoBuildContext, false); + processResourceGenerator.manageTransactional(compilationUnit); + // the annotation is (conditionally) add after processResourceGenerator.manageTransactional + testTransaction(restEndpoints, kogitoBuildContext, transactionEnabled); + } + + @ParameterizedTest + @MethodSource("org.kie.kogito.codegen.api.utils.KogitoContextTestUtils#restContextBuilders") + void testManageTransactionalDisabled(KogitoBuildContext.Builder contextBuilder) { + String fileName = "src/test/resources/startsignal/StartSignalEventNoPayload.bpmn2"; + boolean transactionEnabled = false; + ProcessResourceGenerator processResourceGenerator = getProcessResourceGenerator(contextBuilder, fileName, + transactionEnabled); + CompilationUnit compilationUnit = + processResourceGenerator.createCompilationUnit(processResourceGenerator.createTemplatedGeneratorBuilder()); + assertThat(compilationUnit).isNotNull(); + KogitoBuildContext kogitoBuildContext = contextBuilder.build(); + Collection restEndpoints = processResourceGenerator.getRestMethods(compilationUnit); + // before processResourceGenerator.manageTransactional, the annotation is not there + testTransaction(restEndpoints, kogitoBuildContext, false); + processResourceGenerator.manageTransactional(compilationUnit); + // the annotation is (conditionally) add after processResourceGenerator.manageTransactional + testTransaction(restEndpoints, kogitoBuildContext, transactionEnabled); + } + + void testTransaction(Collection restEndpoints, + KogitoBuildContext kogitoBuildContext, + boolean enabled) { + String transactionalAnnotation = + kogitoBuildContext.getDependencyInjectionAnnotator().getTransactionalAnnotation(); + restEndpoints.forEach(methodDeclaration -> { + ListAssert transactionAnnotationAssert = assertThat( + methodDeclaration.getAnnotations().stream().filter(annotationExpr -> annotationExpr.getNameAsString().equals(transactionalAnnotation))); + if (enabled) { + transactionAnnotationAssert.hasSize(1); + } else { + transactionAnnotationAssert.isEmpty(); + } + if (methodDeclaration.getName().toString().startsWith("createResource_")) { + ListAssert stmtsAsserts = assertThat( + methodDeclaration.getBody() + .get() + .getStatements()); + stmtsAsserts.hasSize(2); + } + }); + } + void testOpenApiDocumentation(KogitoBuildContext.Builder contextBuilder, String fileName, String expectedSummary, String expectedDescription) { ClassOrInterfaceDeclaration classDeclaration = getResourceClassDeclaration(contextBuilder, fileName); @@ -168,15 +248,27 @@ private ClassOrInterfaceDeclaration getResourceClassDeclaration(KogitoBuildConte return classDeclaration.orElseThrow(); } - private CompilationUnit getCompilationUnit(KogitoBuildContext.Builder contextBuilder, KogitoWorkflowProcess process) { + private CompilationUnit getCompilationUnit(KogitoBuildContext.Builder contextBuilder, + KogitoWorkflowProcess process) { + ProcessResourceGenerator processResourceGenerator = getProcessResourceGenerator(contextBuilder, process, true); + return StaticJavaParser.parse(processResourceGenerator.generate()); + } + + private ProcessResourceGenerator getProcessResourceGenerator(KogitoBuildContext.Builder contextBuilder, + String fileName, boolean withTransaction) { + return getProcessResourceGenerator(contextBuilder, parseProcess(fileName), withTransaction); + } + + private ProcessResourceGenerator getProcessResourceGenerator(KogitoBuildContext.Builder contextBuilder, + KogitoWorkflowProcess process, + boolean withTransaction) { KogitoBuildContext context = createContext(contextBuilder); ProcessExecutableModelGenerator execModelGen = new ProcessExecutableModelGenerator(process, new ProcessToExecModelGenerator(context.getClassLoader())); KogitoWorkflowProcess workFlowProcess = execModelGen.process(); - - ProcessResourceGenerator processResourceGenerator = new ProcessResourceGenerator( + ProcessResourceGenerator toReturn = new ProcessResourceGenerator( context, workFlowProcess, new ModelClassGenerator(context, workFlowProcess).className(), @@ -185,11 +277,11 @@ private CompilationUnit getCompilationUnit(KogitoBuildContext.Builder contextBui ProcessMetaData metaData = execModelGen.generate(); - processResourceGenerator + toReturn .withSignals(metaData.getSignals()) - .withTriggers(metaData.isStartable(), metaData.isDynamic(), metaData.getTriggers()); - - return StaticJavaParser.parse(processResourceGenerator.generate()); + .withTriggers(metaData.isStartable(), metaData.isDynamic(), metaData.getTriggers()) + .withTransaction(withTransaction); + return toReturn; } private void assertThatMethodHasOpenApiDocumentation(MethodDeclaration method, String summary, String description) { diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/test/java/org/kie/kogito/codegen/process/events/CodegenMessageStartEventTest.java b/kogito-codegen-modules/kogito-codegen-processes/src/test/java/org/kie/kogito/codegen/process/events/CodegenMessageStartEventTest.java index 6badf99806b..f8c540a242a 100644 --- a/kogito-codegen-modules/kogito-codegen-processes/src/test/java/org/kie/kogito/codegen/process/events/CodegenMessageStartEventTest.java +++ b/kogito-codegen-modules/kogito-codegen-processes/src/test/java/org/kie/kogito/codegen/process/events/CodegenMessageStartEventTest.java @@ -49,7 +49,7 @@ public class CodegenMessageStartEventTest { private static final Path MESSAGE_START_END_EVENT_SOURCE_FULL_SOURCE = BASE_PATH.resolve(MESSAGE_START_END_EVENT_SOURCE); @ParameterizedTest - @MethodSource("org.kie.kogito.codegen.api.utils.KogitoContextTestUtils#contextBuilders") + @MethodSource("org.kie.kogito.codegen.api.utils.KogitoContextTestUtils#restContextBuilders") public void testRESTApiForMessageStartEvent(KogitoBuildContext.Builder contextBuilder) { KogitoBuildContext context = contextBuilder.build(); @@ -80,7 +80,7 @@ public void testRESTApiForMessageStartEvent(KogitoBuildContext.Builder contextBu } @ParameterizedTest - @MethodSource("org.kie.kogito.codegen.api.utils.KogitoContextTestUtils#contextBuilders") + @MethodSource("org.kie.kogito.codegen.api.utils.KogitoContextTestUtils#restContextBuilders") public void testRESTApiForMessageEndEvent(KogitoBuildContext.Builder contextBuilder) { KogitoBuildContext context = contextBuilder.build(); diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/test/java/org/kie/kogito/codegen/process/events/CodegenUserTaskTest.java b/kogito-codegen-modules/kogito-codegen-processes/src/test/java/org/kie/kogito/codegen/process/events/CodegenUserTaskTest.java index 271f9c897fb..24f875c2a0a 100644 --- a/kogito-codegen-modules/kogito-codegen-processes/src/test/java/org/kie/kogito/codegen/process/events/CodegenUserTaskTest.java +++ b/kogito-codegen-modules/kogito-codegen-processes/src/test/java/org/kie/kogito/codegen/process/events/CodegenUserTaskTest.java @@ -43,7 +43,7 @@ public class CodegenUserTaskTest { private static final Path MESSAGE_USERTASK_SOURCE_FULL_SOURCE = BASE_PATH.resolve(MESSAGE_USERTASK_SOURCE); @ParameterizedTest - @MethodSource("org.kie.kogito.codegen.api.utils.KogitoContextTestUtils#contextBuilders") + @MethodSource("org.kie.kogito.codegen.api.utils.KogitoContextTestUtils#restContextBuilders") public void testRESTApiForMessageStartEvent(KogitoBuildContext.Builder contextBuilder) { KogitoBuildContext context = contextBuilder.build(); diff --git a/quarkus/extensions/kogito-quarkus-processes-extension/kogito-quarkus-processes-deployment/pom.xml b/quarkus/extensions/kogito-quarkus-processes-extension/kogito-quarkus-processes-deployment/pom.xml index 9acc49370fb..60533728264 100644 --- a/quarkus/extensions/kogito-quarkus-processes-extension/kogito-quarkus-processes-deployment/pom.xml +++ b/quarkus/extensions/kogito-quarkus-processes-extension/kogito-quarkus-processes-deployment/pom.xml @@ -45,6 +45,10 @@ org.jbpm jbpm-quarkus + + io.quarkus + quarkus-narayana-jta-deployment + io.quarkus diff --git a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-deployment/pom.xml b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-deployment/pom.xml index e74b1c102b1..5caf3a85a6e 100644 --- a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-deployment/pom.xml +++ b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-deployment/pom.xml @@ -68,6 +68,10 @@ io.quarkiverse.jackson-jq quarkus-jackson-jq-deployment + + io.quarkus + quarkus-narayana-jta-deployment + diff --git a/quarkus/extensions/kogito-quarkus-workflow-extension-common/kogito-quarkus-workflow-common/pom.xml b/quarkus/extensions/kogito-quarkus-workflow-extension-common/kogito-quarkus-workflow-common/pom.xml index 69118742393..736a4158584 100644 --- a/quarkus/extensions/kogito-quarkus-workflow-extension-common/kogito-quarkus-workflow-common/pom.xml +++ b/quarkus/extensions/kogito-quarkus-workflow-extension-common/kogito-quarkus-workflow-common/pom.xml @@ -45,6 +45,10 @@ io.smallrye.reactive smallrye-mutiny-vertx-web-client + + io.quarkus + quarkus-narayana-jta + org.kie.kogito jbpm-deps-group-engine diff --git a/springboot/starters/kogito-processes-spring-boot-starter/pom.xml b/springboot/starters/kogito-processes-spring-boot-starter/pom.xml index c29c25ebe10..cd9817e03ee 100644 --- a/springboot/starters/kogito-processes-spring-boot-starter/pom.xml +++ b/springboot/starters/kogito-processes-spring-boot-starter/pom.xml @@ -56,6 +56,10 @@ org.springframework.security spring-security-core + + org.springframework + spring-tx + \ No newline at end of file