diff --git a/.gitignore b/.gitignore index d182ac6..25d85a4 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,5 @@ hs_err_pid* *.iml *.ipr *.iws -/crp-flowable-spock/target/ -/crp-flowable-groovy/target/ -/crp-flowable-shell/target/ +/*/target/ /.idea/ diff --git a/.travis.yml b/.travis.yml index f723ef1..862205e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,7 @@ before_script: - echo MAVEN_OPTS=\"-Xmx2048m -Xms1024m -Djava.awt.headless=true\" > ~/.mavenrc - chmod +x mvnw -script: ./mvnw verify -DskipTests=false -Pdocker -B +script: ./mvnw verify -DskipTests=false -Pintegration-test -B deploy: provider: script diff --git a/README.md b/README.md index 3cff142..de4046c 100644 --- a/README.md +++ b/README.md @@ -1 +1,9 @@ -# flowable-ex \ No newline at end of file +# crp-flowable-ex + +# gpg public key +``` +------------------------------------------------- +pub ed25519 2022-05-05 [SC] + 8F1F8EA5D80A19261D4225C151F1832CEE783D36 +uid [ultimate] crystal-processes +``` diff --git a/crp-bpm-sonarqube-plugin/pom.xml b/crp-bpm-sonarqube-plugin/pom.xml new file mode 100644 index 0000000..2e16fae --- /dev/null +++ b/crp-bpm-sonarqube-plugin/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + + crp-flowable-ex + io.github.crystal-processes + 0.0.4-SNAPSHOT + + + crp-bpm-sonarqube-plugin + sonar-plugin + 0.0.4-SNAPSHOT + + BPM Sonarqube plugin + Business process management plugin for Sonarqube + + + UTF-8 + 8.3.1.34397 + 1.8 + src/main/java + + + + + org.sonarsource.sonarqube + sonar-plugin-api + ${sonar.apiVersion} + provided + + + + commons-lang + commons-lang + 2.6 + + + + + org.sonarsource.sonarqube + sonar-testing-harness + ${sonar.apiVersion} + test + + + org.mockito + mockito-core + 3.3.3 + test + + + + + + + org.sonarsource.sonar-packaging-maven-plugin + sonar-packaging-maven-plugin + 1.18.0.372 + true + + BPM + org.crp.sonarsource.plugins.bpm.BpmPlugin + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${jdk.min.version} + ${jdk.min.version} + + + + + org.codehaus.mojo + native2ascii-maven-plugin + 2.0.1 + + + + resources + + + + + + + + diff --git a/crp-bpm-sonarqube-plugin/src/main/java/org/crp/sonarsource/plugins/bpm/BpmPlugin.java b/crp-bpm-sonarqube-plugin/src/main/java/org/crp/sonarsource/plugins/bpm/BpmPlugin.java new file mode 100644 index 0000000..874b184 --- /dev/null +++ b/crp-bpm-sonarqube-plugin/src/main/java/org/crp/sonarsource/plugins/bpm/BpmPlugin.java @@ -0,0 +1,22 @@ +package org.crp.sonarsource.plugins.bpm; + +import org.crp.sonarsource.plugins.bpm.languages.BpmnLanguage; +import org.crp.sonarsource.plugins.bpm.languages.BpmnQualityProfile; +import org.crp.sonarsource.plugins.bpm.rules.BpmnCoverageLoaderSensor; +import org.crp.sonarsource.plugins.bpm.settings.BpmnLanguageProperties; +import org.sonar.api.Plugin; + +/** + * Business process management plugin for sonarqube + */ +public class BpmPlugin implements Plugin { + + @Override + public void define(Context context) { + context.addExtensions(BpmnLanguage.class, BpmnQualityProfile.class); + context.addExtensions(BpmnLanguageProperties.getProperties()); + + context + .addExtension(BpmnCoverageLoaderSensor.class); + } +} diff --git a/crp-bpm-sonarqube-plugin/src/main/java/org/crp/sonarsource/plugins/bpm/languages/BpmnLanguage.java b/crp-bpm-sonarqube-plugin/src/main/java/org/crp/sonarsource/plugins/bpm/languages/BpmnLanguage.java new file mode 100644 index 0000000..48e30b6 --- /dev/null +++ b/crp-bpm-sonarqube-plugin/src/main/java/org/crp/sonarsource/plugins/bpm/languages/BpmnLanguage.java @@ -0,0 +1,27 @@ +package org.crp.sonarsource.plugins.bpm.languages; + +import org.crp.sonarsource.plugins.bpm.settings.BpmnLanguageProperties; +import org.sonar.api.config.Configuration; +import org.sonar.api.resources.AbstractLanguage; + +/** + * This class defines the bpmn language. + */ +public final class BpmnLanguage extends AbstractLanguage { + + public static final String NAME = "BPMN"; + public static final String KEY = "bpmn"; + + private final Configuration config; + + public BpmnLanguage(Configuration config) { + super(KEY, NAME); + this.config = config; + } + + @Override + public String[] getFileSuffixes() { + return config.getStringArray(BpmnLanguageProperties.FILE_SUFFIXES_KEY); + } + +} diff --git a/crp-bpm-sonarqube-plugin/src/main/java/org/crp/sonarsource/plugins/bpm/languages/BpmnQualityProfile.java b/crp-bpm-sonarqube-plugin/src/main/java/org/crp/sonarsource/plugins/bpm/languages/BpmnQualityProfile.java new file mode 100644 index 0000000..76f2c75 --- /dev/null +++ b/crp-bpm-sonarqube-plugin/src/main/java/org/crp/sonarsource/plugins/bpm/languages/BpmnQualityProfile.java @@ -0,0 +1,18 @@ +package org.crp.sonarsource.plugins.bpm.languages; + +import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition; + +/** + * Default, BuiltIn Quality Profile for the projects having bpmn files + */ +public final class BpmnQualityProfile implements BuiltInQualityProfilesDefinition { + + @Override + public void define(Context context) { + NewBuiltInQualityProfile profile = context.createBuiltInQualityProfile("BPMN Rules", BpmnLanguage.KEY); + profile.setDefault(true); + + profile.done(); + } + +} diff --git a/crp-bpm-sonarqube-plugin/src/main/java/org/crp/sonarsource/plugins/bpm/languages/ReportEvent.java b/crp-bpm-sonarqube-plugin/src/main/java/org/crp/sonarsource/plugins/bpm/languages/ReportEvent.java new file mode 100644 index 0000000..be40e98 --- /dev/null +++ b/crp-bpm-sonarqube-plugin/src/main/java/org/crp/sonarsource/plugins/bpm/languages/ReportEvent.java @@ -0,0 +1,106 @@ +package org.crp.sonarsource.plugins.bpm.languages; + +public class ReportEvent { + protected String fileName; + protected String processDefinitionKey; + protected String flowElementId; + protected int lineNumber; + protected int hits; + protected String event; + + private ReportEvent(String fileName, String processDefinitionKey, String flowElementId, int lineNumber, int hits, String event) { + this.fileName = fileName; + this.processDefinitionKey = processDefinitionKey; + this.flowElementId = flowElementId; + this.lineNumber = lineNumber; + this.hits = hits; + this.event = event; + } + + public String getFileName() { + return fileName; + } + + public String getProcessDefinitionKey() { + return processDefinitionKey; + } + + public String getFlowElementId() { + return flowElementId; + } + + public int getLineNumber() { + return lineNumber; + } + + public String getEvent() { + return event; + } + + public int getHits() { + return hits; + } + + @Override + public String toString() { + return fileName + '|' + + processDefinitionKey + '|' + + flowElementId + '|' + + lineNumber + '|' + + hits + '|' + + event + '|'; + } + + public void addHit() { + hits++; + } + + public void addHits(int hits) { + this.hits += hits; + } + + public static class Builder { + protected String fileName; + protected String processDefinitionKey; + protected String flowElementId; + protected int lineNumber = -1; + protected int hits = 0; + protected String event; + + public Builder() { + } + + public Builder fileName(String fileName) { + this.fileName = fileName; + return this; + } + + public Builder processDefinitionKey(String processDefinitionKey) { + this.processDefinitionKey = processDefinitionKey; + return this; + } + + public Builder flowElementId(String flowElementId) { + this.flowElementId = flowElementId; + return this; + } + + public Builder lineNumber(int lineNumber) { + this.lineNumber = lineNumber; + return this; + } + + public Builder event(String event) { + this.event = event; + return this; + } + + public ReportEvent build() { + return new ReportEvent(fileName, processDefinitionKey, flowElementId, lineNumber, hits, event); + } + public static ReportEvent build(String eventCSV) { + String[] eventCSVArray = eventCSV.split("\\|"); + return new ReportEvent(eventCSVArray[0], eventCSVArray[1], eventCSVArray[2], Integer.parseInt(eventCSVArray[3]), Integer.parseInt(eventCSVArray[4]), ""); + } + } +} diff --git a/crp-bpm-sonarqube-plugin/src/main/java/org/crp/sonarsource/plugins/bpm/rules/BpmnCoverageLoaderSensor.java b/crp-bpm-sonarqube-plugin/src/main/java/org/crp/sonarsource/plugins/bpm/rules/BpmnCoverageLoaderSensor.java new file mode 100644 index 0000000..dd06abc --- /dev/null +++ b/crp-bpm-sonarqube-plugin/src/main/java/org/crp/sonarsource/plugins/bpm/rules/BpmnCoverageLoaderSensor.java @@ -0,0 +1,136 @@ +package org.crp.sonarsource.plugins.bpm.rules; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Scanner; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.crp.sonarsource.plugins.bpm.languages.BpmnLanguage; +import org.crp.sonarsource.plugins.bpm.languages.ReportEvent; +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.Sensor; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.SensorDescriptor; +import org.sonar.api.batch.sensor.coverage.NewCoverage; +import org.sonar.api.config.Configuration; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +/** + * The goal of this Sensor is to load the results of an analysis performed by a fictive external tool named: FooLint + * Results are provided as an xml file and are corresponding to the rules defined in 'rules.xml'. + * To be very abstract, these rules are applied on source files made with the fictive language Foo. + */ +public class BpmnCoverageLoaderSensor implements Sensor { + + private static final Logger LOGGER = Loggers.get(BpmnCoverageLoaderSensor.class); + + public static final String REPORT_PATH_KEY = "crp.flowable.coverage.reportPath"; + + protected final Configuration config; + protected final FileSystem fileSystem; + protected SensorContext context; + + public BpmnCoverageLoaderSensor(final Configuration config, final FileSystem fileSystem) { + this.config = config; + this.fileSystem = fileSystem; + } + + @Override + public void describe(final SensorDescriptor descriptor) { + descriptor.name("Bpmn Coverage Loader Sensor"); + descriptor.onlyOnLanguage(BpmnLanguage.KEY); + } + + protected String reportPathKey() { + return REPORT_PATH_KEY; + } + + protected String getReportPath() { + return config.get(reportPathKey()).orElse(null); + } + + @Override + public void execute(final SensorContext context) { + String reportPath = getReportPath(); + if (reportPath != null) { + this.context = context; + File reportResultsFile = new File(reportPath); + try { + parseReportAndAddCoverage(reportResultsFile); + } catch (FileNotFoundException e) { + throw new IllegalStateException("Unable to parse the provided "+reportPath+" file", e); + } + } + } + + protected void parseReportAndAddCoverage(final File file) throws FileNotFoundException { + LOGGER.info("Parsing {} test coverage results", file.getName()); + Map bpmnFileMap = StreamSupport.stream(fileSystem.inputFiles(fileSystem.predicates().hasLanguage(BpmnLanguage.KEY)).spliterator(), false) + .collect( + Collectors.toMap(InputFile::filename, inputFile -> inputFile) + ); + Map> report = parseCoverageReport(file); + + bpmnFileMap.forEach((bpmnFileName, inputFile) -> { + LOGGER.debug("Adding coverage to file {}", bpmnFileName); + Map reportEvents = getReportEventsForFile(report, bpmnFileName); + Optional convertedInputFileName = bpmnFileMap.keySet().stream().filter(bpmnFileName::endsWith).findFirst(); + if (convertedInputFileName.isPresent()) { + NewCoverage coverage = context.newCoverage().onFile(inputFile); + reportEvents.values().forEach(reportEvent -> + coverage.lineHits(reportEvent.getLineNumber(), reportEvent.getHits()) + ); + coverage.save(); + } else { + LOGGER.warn("Unable to find file {} in the report events file", file); + } + }); + LOGGER.info("Parsing {} test coverage results - done ", file.getName()); + } + + protected Map getReportEventsForFile(Map> report, String bpmnFileName) { + for (String fileName : report.keySet()) { + if (fileName.endsWith(bpmnFileName)) { + return report.get(fileName); + } + } + return Collections.emptyMap(); + } + + protected Map> parseCoverageReport(File file) throws FileNotFoundException { + Map> coverageReport = new HashMap<>(); + try (Scanner scanner = new Scanner(file)) { + while (scanner.hasNextLine()) { + addEventToCoverageReport(coverageReport, scanner.nextLine()); + } + } + return coverageReport; + } + + protected void addEventToCoverageReport(Map> coverageReport, String eventLine) { + ReportEvent reportEvent = ReportEvent.Builder.build(eventLine); + if (!coverageReport.containsKey(reportEvent.getFileName())) { + coverageReport.put(reportEvent.getFileName(), new HashMap<>()); + } + + Map fileEvents = coverageReport.get(reportEvent.getFileName()); + if (fileEvents.containsKey(reportEvent.getFlowElementId())) { + fileEvents.get(reportEvent.getFlowElementId()).addHits(reportEvent.getHits()); + } else { + fileEvents.put(reportEvent.getFlowElementId(), reportEvent); + } + } + + @Override + public String toString() { + return "Bpmn coverage loader sensor"; + } + +} diff --git a/crp-bpm-sonarqube-plugin/src/main/java/org/crp/sonarsource/plugins/bpm/settings/BpmnLanguageProperties.java b/crp-bpm-sonarqube-plugin/src/main/java/org/crp/sonarsource/plugins/bpm/settings/BpmnLanguageProperties.java new file mode 100644 index 0000000..47d0529 --- /dev/null +++ b/crp-bpm-sonarqube-plugin/src/main/java/org/crp/sonarsource/plugins/bpm/settings/BpmnLanguageProperties.java @@ -0,0 +1,39 @@ +package org.crp.sonarsource.plugins.bpm.settings; + +import java.util.List; + +import org.sonar.api.config.PropertyDefinition; +import org.sonar.api.resources.Qualifiers; + +import static java.util.Arrays.asList; +import static org.crp.sonarsource.plugins.bpm.rules.BpmnCoverageLoaderSensor.REPORT_PATH_KEY; + +public class BpmnLanguageProperties { + + public static final String FILE_SUFFIXES_KEY = "sonar.bpmn.file.suffixes"; + public static final String FILE_SUFFIXES_DEFAULT_VALUE = ".bpmn"; + + private BpmnLanguageProperties() { + } + + public static List getProperties() { + return asList(PropertyDefinition.builder(FILE_SUFFIXES_KEY) + .multiValues(true) + .defaultValue(FILE_SUFFIXES_DEFAULT_VALUE) + .category("BPMN") + .name("File Suffixes") + .description("List of suffixes for BPMN files to analyze.") + .onQualifiers(Qualifiers.PROJECT) + .build(), + PropertyDefinition.builder(REPORT_PATH_KEY) + .multiValues(false) + .defaultValue("target/bpmn-coverage-report.csv") + .category("BPMN") + .name("BPMN coverage report") + .description("BPMN coverage report file path.") + .onQualifiers(Qualifiers.PROJECT) + .build() + ); + } + +} diff --git a/crp-bpm-sonarqube-plugin/src/test/java/org/crp/sonarsource/plugins/bpm/rules/BpmnCoverageLoaderSensorTest.java b/crp-bpm-sonarqube-plugin/src/test/java/org/crp/sonarsource/plugins/bpm/rules/BpmnCoverageLoaderSensorTest.java new file mode 100644 index 0000000..caedb9b --- /dev/null +++ b/crp-bpm-sonarqube-plugin/src/test/java/org/crp/sonarsource/plugins/bpm/rules/BpmnCoverageLoaderSensorTest.java @@ -0,0 +1,65 @@ +package org.crp.sonarsource.plugins.bpm.rules; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.crp.sonarsource.plugins.bpm.languages.BpmnLanguage; +import org.junit.jupiter.api.Test; +import org.sonar.api.batch.fs.FilePredicate; +import org.sonar.api.batch.fs.FilePredicates; +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.SensorDescriptor; +import org.sonar.api.batch.sensor.coverage.NewCoverage; +import org.sonar.api.config.Configuration; + +class BpmnCoverageLoaderSensorTest { + + private final Configuration configuration = mock(Configuration.class); + private final FileSystem fileSystem = mock(FileSystem.class); + + private final BpmnCoverageLoaderSensor sensor = new BpmnCoverageLoaderSensor(configuration, fileSystem); + + @Test + void description() { + SensorDescriptor descriptor = mock(SensorDescriptor.class); + sensor.describe(descriptor); + verify(descriptor).name("Bpmn Coverage Loader Sensor"); + verify(descriptor).onlyOnLanguage(BpmnLanguage.KEY); + reset(descriptor); + } + + @Test + void parse_events_and_sets_coverage() { + SensorContext context = mock(SensorContext.class); + FilePredicates predicates = mock(FilePredicates.class); + when(configuration.get("crp.flowable.coverage.reportPath")).thenReturn(Optional.of("src/test/resources/eventReport.txt")); + when(fileSystem.predicates()).thenReturn(predicates); + InputFile inputBpmnFile = mock(InputFile.class); + when(inputBpmnFile.filename()).thenReturn("org/crp/flowable/test/oneTaskProcess.bpmn20.xml"); + NewCoverage newCoverage = mock(NewCoverage.class); + when(context.newCoverage()).thenReturn(newCoverage); + List files = Collections.singletonList(inputBpmnFile); + when(fileSystem.inputFiles(any())).thenReturn(files); + FilePredicate acceptAllPredicate = mock(FilePredicate.class); + when(acceptAllPredicate.apply(any())).thenReturn(true); + when(predicates.hasType(any())).thenReturn(acceptAllPredicate); + when(predicates.and(any(), any())).thenReturn(acceptAllPredicate); + when(newCoverage.onFile(inputBpmnFile)).thenReturn(newCoverage); + + sensor.execute(context); + + verify(newCoverage, times(1)).lineHits(7,1); + + reset(configuration, fileSystem); + } +} \ No newline at end of file diff --git a/crp-bpm-sonarqube-plugin/src/test/resources/eventReport.txt b/crp-bpm-sonarqube-plugin/src/test/resources/eventReport.txt new file mode 100644 index 0000000..e1e24ec --- /dev/null +++ b/crp-bpm-sonarqube-plugin/src/test/resources/eventReport.txt @@ -0,0 +1,2 @@ +org/crp/flowable/test/oneTaskProcess.bpmn20.xml|oneTaskProcess|theStart|7|0|| +org/crp/flowable/test/oneTaskProcess.bpmn20.xml|oneTaskProcess|theStart|7|1|| diff --git a/crp-bpm-sonarqube-plugin/src/test/resources/org/crp/flowable/test/oneTaskProcess.bpmn20.xml b/crp-bpm-sonarqube-plugin/src/test/resources/org/crp/flowable/test/oneTaskProcess.bpmn20.xml new file mode 100644 index 0000000..20e6e0c --- /dev/null +++ b/crp-bpm-sonarqube-plugin/src/test/resources/org/crp/flowable/test/oneTaskProcess.bpmn20.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/crp-flowable-coverage/pom.xml b/crp-flowable-coverage/pom.xml new file mode 100644 index 0000000..9eb0be1 --- /dev/null +++ b/crp-flowable-coverage/pom.xml @@ -0,0 +1,102 @@ + + + + crp-flowable-ex + io.github.crystal-processes + 0.0.4-SNAPSHOT + + 4.0.0 + + crp-flowable-coverage + crp-flowable-coverage + + + UTF-8 + + + + + io.github.crystal-processes + crp-flowable-spock + test + ${project.parent.version} + + + + org.flowable + flowable-engine + ${flowable.version} + provided + + + org.flowable + flowable-engine-common + ${flowable.version} + provided + + + org.springframework.boot + spring-boot-test + test + 2.6.2 + + + org.flowable + flowable-spring-boot-starter + ${flowable.version} + test + + + ch.qos.logback + logback-classic + + + + + org.flowable + flowable-spring + 6.7.2 + provided + + + org.flowable + flowable-spring-boot-autoconfigure + 6.7.2 + provided + + + org.springframework.boot + spring-boot-autoconfigure + 2.6.2 + provided + + + org.springframework + spring-test + 5.3.14 + test + + + + + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + + + compile + compileTests + + + + + + + + \ No newline at end of file diff --git a/crp-flowable-coverage/src/main/java/org/crp/flowable/coverage/bpmn/BpmnCoverageAutoConfiguration.java b/crp-flowable-coverage/src/main/java/org/crp/flowable/coverage/bpmn/BpmnCoverageAutoConfiguration.java new file mode 100644 index 0000000..b904b3c --- /dev/null +++ b/crp-flowable-coverage/src/main/java/org/crp/flowable/coverage/bpmn/BpmnCoverageAutoConfiguration.java @@ -0,0 +1,26 @@ +package org.crp.flowable.coverage.bpmn; + +import org.flowable.spring.SpringProcessEngineConfiguration; +import org.flowable.spring.boot.EngineConfigurationConfigurer; +import org.flowable.spring.boot.ProcessEngineAutoConfiguration; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Profile; + +@SpringBootConfiguration +@AutoConfigureAfter(ProcessEngineAutoConfiguration.class) +@EnableConfigurationProperties(BpmnCoverageProperties.class) +@ConditionalOnClass(SpringProcessEngineConfiguration.class) +@ConditionalOnProperty(prefix = "crp.flowable", name = "coverage.enabled", havingValue = "true") +public class BpmnCoverageAutoConfiguration { + + @Bean + EngineConfigurationConfigurer setReportFlowableEngineAgendaFactory(BpmnCoverageProperties properties) { + return processEngineConfiguration -> processEngineConfiguration.setAgendaFactory(new ReportFlowableEngineAgendaFactory(properties.getReportPath())); + } + +} diff --git a/crp-flowable-coverage/src/main/java/org/crp/flowable/coverage/bpmn/BpmnCoverageProperties.java b/crp-flowable-coverage/src/main/java/org/crp/flowable/coverage/bpmn/BpmnCoverageProperties.java new file mode 100644 index 0000000..9545bab --- /dev/null +++ b/crp-flowable-coverage/src/main/java/org/crp/flowable/coverage/bpmn/BpmnCoverageProperties.java @@ -0,0 +1,27 @@ +package org.crp.flowable.coverage.bpmn; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "crp.flowable.coverage") +public class BpmnCoverageProperties { + // is coverage scan enabled + private boolean enabled; + // where to store coverage data + private String reportPath = "target/bpmn-coverage-report.csv"; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getReportPath() { + return reportPath; + } + + public void setReportPath(String reportPath) { + this.reportPath = reportPath; + } +} diff --git a/crp-flowable-coverage/src/main/java/org/crp/flowable/coverage/bpmn/ReportEvent.java b/crp-flowable-coverage/src/main/java/org/crp/flowable/coverage/bpmn/ReportEvent.java new file mode 100644 index 0000000..9920c45 --- /dev/null +++ b/crp-flowable-coverage/src/main/java/org/crp/flowable/coverage/bpmn/ReportEvent.java @@ -0,0 +1,106 @@ +package org.crp.flowable.coverage.bpmn; + +public class ReportEvent { + protected String fileName; + protected String processDefinitionKey; + protected String flowElementId; + protected int lineNumber; + protected int hits; + protected String event; + + private ReportEvent(String fileName, String processDefinitionKey, String flowElementId, int lineNumber, int hits, String event) { + this.fileName = fileName; + this.processDefinitionKey = processDefinitionKey; + this.flowElementId = flowElementId; + this.lineNumber = lineNumber; + this.hits = hits; + this.event = event; + } + + public String getFileName() { + return fileName; + } + + public String getProcessDefinitionKey() { + return processDefinitionKey; + } + + public String getFlowElementId() { + return flowElementId; + } + + public int getLineNumber() { + return lineNumber; + } + + public String getEvent() { + return event; + } + + public int getHits() { + return hits; + } + + @Override + public String toString() { + return fileName + '|' + + processDefinitionKey + '|' + + flowElementId + '|' + + lineNumber + '|' + + hits + '|' + + event + '|'; + } + + public void addHit() { + hits++; + } + + public void addHits(int hits) { + this.hits += hits; + } + + public static class Builder { + protected String fileName; + protected String processDefinitionKey; + protected String flowElementId; + protected int lineNumber = -1; + protected int hits = 0; + protected String event; + + public Builder() { + } + + public Builder fileName(String fileName) { + this.fileName = fileName; + return this; + } + + public Builder processDefinitionKey(String processDefinitionKey) { + this.processDefinitionKey = processDefinitionKey; + return this; + } + + public Builder flowElementId(String flowElementId) { + this.flowElementId = flowElementId; + return this; + } + + public Builder lineNumber(int lineNumber) { + this.lineNumber = lineNumber; + return this; + } + + public Builder event(String event) { + this.event = event; + return this; + } + + public ReportEvent build() { + return new ReportEvent(fileName, processDefinitionKey, flowElementId, lineNumber, hits, event); + } + public static ReportEvent build(String eventCSV) { + String[] eventCSVArray = eventCSV.split("\\|"); + return new ReportEvent(eventCSVArray[0], eventCSVArray[1], eventCSVArray[2], Integer.parseInt(eventCSVArray[3]), Integer.parseInt(eventCSVArray[4]), ""); + } + } +} diff --git a/crp-flowable-coverage/src/main/java/org/crp/flowable/coverage/bpmn/ReportFlowableEngineAgenda.java b/crp-flowable-coverage/src/main/java/org/crp/flowable/coverage/bpmn/ReportFlowableEngineAgenda.java new file mode 100644 index 0000000..09f549b --- /dev/null +++ b/crp-flowable-coverage/src/main/java/org/crp/flowable/coverage/bpmn/ReportFlowableEngineAgenda.java @@ -0,0 +1,98 @@ +package org.crp.flowable.coverage.bpmn; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.io.FileUtils; +import org.flowable.bpmn.model.FlowElement; +import org.flowable.common.engine.impl.interceptor.CommandContext; +import org.flowable.engine.impl.agenda.DefaultFlowableEngineAgenda; +import org.flowable.engine.impl.persistence.entity.ExecutionEntity; +import org.flowable.engine.impl.util.ProcessDefinitionUtil; +import org.flowable.engine.repository.ProcessDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ReportFlowableEngineAgenda extends DefaultFlowableEngineAgenda { + + private static final Logger LOGGER = LoggerFactory.getLogger(ReportFlowableEngineAgenda.class); + + protected Map reportEvents; + private String reportFileName; + + public ReportFlowableEngineAgenda(CommandContext commandContext, String reportFileName) { + super(commandContext); + this.reportFileName = reportFileName; + reportEvents = new HashMap<>(); + } + + /** + * Generic method to plan a {@link Runnable}. + */ + @Override + public void planOperation(Runnable operation, ExecutionEntity executionEntity) { + ReportEvent reportEvent = createReportEvent(executionEntity, reportEvents); + LOGGER.debug("Adding report event {}", reportEvent); + + super.planOperation(operation, executionEntity); + } + + protected ReportEvent createReportEvent(ExecutionEntity executionEntity, Map reportEvents) { + ProcessDefinition processDefinition = ProcessDefinitionUtil.getProcessDefinition(executionEntity.getProcessDefinitionId()); + FlowElement currentFlowElement = executionEntity.getCurrentFlowElement(); + String reportEventKey = getReportEventKey(processDefinition.getResourceName(), processDefinition.getKey(), currentFlowElement.getId()); + if (!reportEvents.containsKey(reportEventKey)) { + ProcessDefinitionUtil.getProcess(processDefinition.getId()).getFlowElements().forEach( + element -> { + String reportEventKey1 = getReportEventKey(processDefinition.getResourceName(), processDefinition.getKey(), element.getId()); + reportEvents.put(reportEventKey1, + new ReportEvent.Builder(). + fileName(processDefinition.getResourceName()).processDefinitionKey(processDefinition.getKey()). + flowElementId(element.getId()).lineNumber(element.getXmlRowNumber()). + event("").build() + ); + }); + } + reportEvents.get(reportEventKey).addHit(); + return null; + } + + protected String getReportEventKey(String resourceName, String key, String flowElementId) { + return resourceName+"-"+key+"-"+flowElementId; + } + + @Override + public void close() { + if (!reportEvents.isEmpty()) { + LOGGER.debug("Storing [{}] report events into file [{}]", reportEvents.size(), reportFileName); + try { + File reportFile = new File(reportFileName); + if (!reportFile.exists()) { + FileUtils.createParentDirectories(reportFile); + } + try (FileWriter fileWriter = new FileWriter(reportFile, true)) { + try (BufferedWriter bufferedWriter = new BufferedWriter(fileWriter)) { + reportEvents.values().forEach( + event -> { + try { + bufferedWriter.write(event.toString()); + bufferedWriter.newLine(); + } catch (IOException e) { + LOGGER.error("Unable to add report event to file ["+reportFileName+"]", e); + throw new RuntimeException("Unable to add report event to file ["+reportFileName+"]", e); + } + } + ); + } + } + } catch (IOException e) { + LOGGER.error("Unable to add report events to file ["+reportFileName+"]", e); + throw new RuntimeException("Unable to add report events to file ["+reportFileName+"]", e); + } + } + } +} diff --git a/crp-flowable-coverage/src/main/java/org/crp/flowable/coverage/bpmn/ReportFlowableEngineAgendaFactory.java b/crp-flowable-coverage/src/main/java/org/crp/flowable/coverage/bpmn/ReportFlowableEngineAgendaFactory.java new file mode 100644 index 0000000..e4c33b3 --- /dev/null +++ b/crp-flowable-coverage/src/main/java/org/crp/flowable/coverage/bpmn/ReportFlowableEngineAgendaFactory.java @@ -0,0 +1,20 @@ +package org.crp.flowable.coverage.bpmn; + +import org.flowable.common.engine.impl.interceptor.CommandContext; +import org.flowable.engine.FlowableEngineAgenda; +import org.flowable.engine.FlowableEngineAgendaFactory; + +public class ReportFlowableEngineAgendaFactory implements FlowableEngineAgendaFactory { + + private String reportFileName; + + public ReportFlowableEngineAgendaFactory(String reportFileName) { + this.reportFileName = reportFileName; + } + + @Override + public FlowableEngineAgenda createAgenda(CommandContext commandContext) { + return new ReportFlowableEngineAgenda(commandContext, reportFileName); + } + +} diff --git a/crp-flowable-coverage/src/main/resources/META-INF/spring.factories b/crp-flowable-coverage/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..5fd964c --- /dev/null +++ b/crp-flowable-coverage/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +# BPM Flowable process test coverage auto-configurations + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + org.crp.flowable.coverage.bpmn.BpmnCoverageAutoConfiguration \ No newline at end of file diff --git a/crp-flowable-coverage/src/test/java/org/crp/flowable/bpmn/GenerateEventsTest.java b/crp-flowable-coverage/src/test/java/org/crp/flowable/bpmn/GenerateEventsTest.java new file mode 100644 index 0000000..a4b9429 --- /dev/null +++ b/crp-flowable-coverage/src/test/java/org/crp/flowable/bpmn/GenerateEventsTest.java @@ -0,0 +1,47 @@ +package org.crp.flowable.bpmn; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; + +import org.apache.commons.io.IOUtils; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.TaskService; +import org.flowable.engine.test.Deployment; +import org.flowable.engine.test.FlowableTest; +import org.flowable.task.api.Task; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +@FlowableTest +public class GenerateEventsTest { + + public static final String EVENT_REPORT_FILE_NAME = "target/bpmn-coverage-report.csv"; + + @AfterEach + void deleteGeneratedEventsFile() { + File file = new File(EVENT_REPORT_FILE_NAME); + if (file.exists()) { + file.delete(); + } + } + + @Test + @Deployment(resources = "org/crp/flowable/test/oneTaskProcess.bpmn20.xml") + void oneTaskProcessReport(RuntimeService runtimeService, TaskService taskService) throws IOException { + // when + String processId = runtimeService.createProcessInstanceBuilder().processDefinitionKey("oneTaskProcess").start().getId(); + Task task = taskService.createTaskQuery().processInstanceId(processId).singleResult(); + taskService.complete(task.getId()); + + // then + try (BufferedReader brGenerated = new BufferedReader(new FileReader(EVENT_REPORT_FILE_NAME))) { + try (BufferedReader brExpected = new BufferedReader(new FileReader("src/test/resources/eventReport.txt"))) { + assertThat(IOUtils.contentEquals(brGenerated, brExpected)); + } + } + } +} diff --git a/crp-flowable-coverage/src/test/java/org/crp/flowable/bpmn/SpringBootGenerateEventsTest.java b/crp-flowable-coverage/src/test/java/org/crp/flowable/bpmn/SpringBootGenerateEventsTest.java new file mode 100644 index 0000000..053fc03 --- /dev/null +++ b/crp-flowable-coverage/src/test/java/org/crp/flowable/bpmn/SpringBootGenerateEventsTest.java @@ -0,0 +1,54 @@ +package org.crp.flowable.bpmn; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; + +import org.apache.commons.io.IOUtils; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.TaskService; +import org.flowable.engine.test.Deployment; +import org.flowable.spring.impl.test.FlowableSpringExtension; +import org.flowable.task.api.Task; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Profile; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ExtendWith(FlowableSpringExtension.class) +@ActiveProfiles("test") +public class SpringBootGenerateEventsTest { + + public static final String EVENT_REPORT_FILE_NAME = "target/bpmn-coverage-report.csv"; + + @AfterEach + void deleteGeneratedEventsFile() { + File file = new File(EVENT_REPORT_FILE_NAME); + if (file.exists()) { + file.delete(); + } + } + + @Test + @Deployment(resources = "org/crp/flowable/test/oneTaskProcess.bpmn20.xml") + void oneTaskProcessReport(RuntimeService runtimeService, TaskService taskService) throws IOException { + // when + String processId = runtimeService.createProcessInstanceBuilder().processDefinitionKey("oneTaskProcess").start().getId(); + Task task = taskService.createTaskQuery().processInstanceId(processId).singleResult(); + taskService.complete(task.getId()); + + // then + try (BufferedReader brGenerated = new BufferedReader(new FileReader(EVENT_REPORT_FILE_NAME))) { + try (BufferedReader brExpected = new BufferedReader(new FileReader("src/test/resources/eventReport.txt"))) { + assertThat(IOUtils.contentEquals(brGenerated, brExpected)); + } + } + } + +} diff --git a/crp-flowable-coverage/src/test/java/org/crp/flowable/bpmn/TrainingApplication.java b/crp-flowable-coverage/src/test/java/org/crp/flowable/bpmn/TrainingApplication.java new file mode 100644 index 0000000..0594bc3 --- /dev/null +++ b/crp-flowable-coverage/src/test/java/org/crp/flowable/bpmn/TrainingApplication.java @@ -0,0 +1,13 @@ +package org.crp.flowable.bpmn; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class TrainingApplication { + + public static void main(String[] args) { + SpringApplication.run(TrainingApplication.class, args); + } + +} diff --git a/crp-flowable-coverage/src/test/resources/application-test.properties b/crp-flowable-coverage/src/test/resources/application-test.properties new file mode 100644 index 0000000..9d84f57 --- /dev/null +++ b/crp-flowable-coverage/src/test/resources/application-test.properties @@ -0,0 +1 @@ +crp.flowable.coverage.enabled=true \ No newline at end of file diff --git a/crp-flowable-coverage/src/test/resources/eventReport.txt b/crp-flowable-coverage/src/test/resources/eventReport.txt new file mode 100644 index 0000000..d4d891c --- /dev/null +++ b/crp-flowable-coverage/src/test/resources/eventReport.txt @@ -0,0 +1,14 @@ +|org/crp/flowable/test/oneTaskProcess.bpmn20.xml|oneTaskProcess|theStart|7|ContinueProcessOperation| +|org/crp/flowable/test/oneTaskProcess.bpmn20.xml|oneTaskProcess|theStart|7|TakeOutgoingSequenceFlowsOperation| +|org/crp/flowable/test/oneTaskProcess.bpmn20.xml|oneTaskProcess|flow1|8|ContinueProcessOperation| +|org/crp/flowable/test/oneTaskProcess.bpmn20.xml|oneTaskProcess|task|9|ContinueProcessOperation| +|org/crp/flowable/test/oneTaskProcess.bpmn20.xml|oneTaskProcess|theStart|7|ContinueProcessOperation| +|org/crp/flowable/test/oneTaskProcess.bpmn20.xml|oneTaskProcess|theStart|7|TakeOutgoingSequenceFlowsOperation| +|org/crp/flowable/test/oneTaskProcess.bpmn20.xml|oneTaskProcess|flow1|8|ContinueProcessOperation| +|org/crp/flowable/test/oneTaskProcess.bpmn20.xml|oneTaskProcess|task|9|ContinueProcessOperation| +|org/crp/flowable/test/oneTaskProcess.bpmn20.xml|oneTaskProcess|task|9|TriggerExecutionOperation| +|org/crp/flowable/test/oneTaskProcess.bpmn20.xml|oneTaskProcess|task|9|TakeOutgoingSequenceFlowsOperation| +|org/crp/flowable/test/oneTaskProcess.bpmn20.xml|oneTaskProcess|flow2|11|ContinueProcessOperation| +|org/crp/flowable/test/oneTaskProcess.bpmn20.xml|oneTaskProcess|theEnd|12|ContinueProcessOperation| +|org/crp/flowable/test/oneTaskProcess.bpmn20.xml|oneTaskProcess|theEnd|12|TakeOutgoingSequenceFlowsOperation| +|org/crp/flowable/test/oneTaskProcess.bpmn20.xml|oneTaskProcess|theEnd|12|EndExecutionOperation| diff --git a/crp-flowable-coverage/src/test/resources/flowable.cfg.xml b/crp-flowable-coverage/src/test/resources/flowable.cfg.xml new file mode 100644 index 0000000..c1c8968 --- /dev/null +++ b/crp-flowable-coverage/src/test/resources/flowable.cfg.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/crp-flowable-coverage/src/test/resources/log4j.properties b/crp-flowable-coverage/src/test/resources/log4j.properties new file mode 100644 index 0000000..3cb5809 --- /dev/null +++ b/crp-flowable-coverage/src/test/resources/log4j.properties @@ -0,0 +1,9 @@ +log4j.rootLogger=INFO, CA + +# ConsoleAppender +log4j.appender.CA=org.apache.log4j.ConsoleAppender +log4j.appender.CA.layout=org.apache.log4j.PatternLayout +log4j.appender.CA.layout.ConversionPattern= %d{hh:mm:ss,SSS} [%t] %-5p %c %x - %m%n + +log4j.logger.org.apache.ibatis=INFO +log4j.logger.javax.activation=INFO diff --git a/crp-flowable-coverage/src/test/resources/org/crp/flowable/test/oneTaskProcess.bpmn20.xml b/crp-flowable-coverage/src/test/resources/org/crp/flowable/test/oneTaskProcess.bpmn20.xml new file mode 100644 index 0000000..20e6e0c --- /dev/null +++ b/crp-flowable-coverage/src/test/resources/org/crp/flowable/test/oneTaskProcess.bpmn20.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/crp-flowable-groovy/pom.xml b/crp-flowable-groovy/pom.xml index 88287e3..5dba1ec 100644 --- a/crp-flowable-groovy/pom.xml +++ b/crp-flowable-groovy/pom.xml @@ -3,7 +3,7 @@ 4.0.0 crp-flowable-ex - com.github.crystal-processes + io.github.crystal-processes 0.0.4-SNAPSHOT diff --git a/crp-flowable-shell/pom.xml b/crp-flowable-shell/pom.xml index 6bde090..703cc49 100644 --- a/crp-flowable-shell/pom.xml +++ b/crp-flowable-shell/pom.xml @@ -3,22 +3,33 @@ 4.0.0 - org.springframework.boot - spring-boot-starter-parent - 2.3.3.RELEASE - + io.github.crystal-processes + crp-flowable-ex + 0.0.4-SNAPSHOT - com.github.crystal-processes + io.github.crystal-processes crp-flowable-shell 0.0.4-SNAPSHOT crp-flowable-shell flowable shell application - 6.6.0 + 6.7.2 + + + + org.springframework.boot + spring-boot-dependencies + 2.3.3.RELEASE + pom + import + + + + org.springframework.boot @@ -29,6 +40,11 @@ spring-shell-starter 2.0.0.RELEASE + + org.springframework.boot + spring-boot-starter-freemarker + 2.3.3.RELEASE + org.apache.httpcomponents httpclient @@ -68,6 +84,12 @@ + + net.javacrumbs.json-unit + json-unit-assertj + 2.19.0 + test + @@ -81,6 +103,20 @@ ZIP + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + true + + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.22.1 + @@ -90,9 +126,17 @@ - docker + integration-test + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + true + + org.apache.maven.plugins maven-failsafe-plugin diff --git a/crp-flowable-shell/src/main/java/org/crp/flowable/shell/commands/Deployment.java b/crp-flowable-shell/src/main/java/org/crp/flowable/shell/commands/Deployment.java index 479b101..22b7d7b 100644 --- a/crp-flowable-shell/src/main/java/org/crp/flowable/shell/commands/Deployment.java +++ b/crp-flowable-shell/src/main/java/org/crp/flowable/shell/commands/Deployment.java @@ -17,6 +17,7 @@ import org.springframework.shell.standard.ShellOption; import org.springframework.util.StringUtils; +import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Paths; @@ -38,7 +39,7 @@ public JsonNode deploy(String pathToApplication, String mandatoryFileName = StringUtils.isEmpty(deploymentName) ? Paths.get(pathToApplication).getFileName().toString() : deploymentName; return executeWithClient(client -> { - HttpPost httpPost = new HttpPost(configuration.getRestURL() + properties.getDeploymentDeploy()); + HttpPost httpPost = new HttpPost(shellProperties.getRestURL() + properties.getDeploymentDeploy()); return uploadFile(client, pathToApplication, mandatoryFileName, mandatoryFileName, tenantId, httpPost); }); } @@ -57,13 +58,12 @@ public JsonNode listDeployments(@ShellOption(defaultValue = "") String name, @Sh protected void deleteDeployment(CloseableHttpClient client, String deploymentId){ try { LOGGER.info("Deleting deployment id {}.", deploymentId); - URIBuilder uriBuilder = new URIBuilder(configuration.getRestURL() + properties.getDeploymentDeploy() + "/" + deploymentId); + URIBuilder uriBuilder = new URIBuilder(shellProperties.getRestURL() + properties.getDeploymentDeploy() + "/" + deploymentId); HttpDelete httpDelete = new HttpDelete(uriBuilder.build()); - CloseableHttpResponse response = executeBinaryRequest(client, httpDelete, false); - try { + try (CloseableHttpResponse response = executeBinaryRequest(client, httpDelete, false)) { LOGGER.info("Delete response {}", response.getStatusLine()); - } finally { - closeResponse(response); + } catch (IOException e) { + LOGGER.error("Unable to deleteDeployment.", e); } } catch (URISyntaxException e) { LOGGER.error("Unable to deleteDeployment.", e); @@ -96,7 +96,7 @@ protected JsonNode getDeployments(CloseableHttpClient client, String name, Strin URIBuilder uriBuilder; HttpGet httpGet; try { - uriBuilder = new URIBuilder(configuration.getRestURL() + properties.getDeploymentDeploy()). + uriBuilder = new URIBuilder(shellProperties.getRestURL() + properties.getDeploymentDeploy()). addParameter("sort", "deployTime"). addParameter("order", "desc"); if (!StringUtils.isEmpty(name)) { @@ -111,11 +111,12 @@ protected JsonNode getDeployments(CloseableHttpClient client, String name, Strin } LOGGER.info("Calling flowable rest api {} to get deployments", httpGet.getURI().toString()); - CloseableHttpResponse response = executeBinaryRequest(client, httpGet, true); - - JsonNode responseNode = readContent(response); - closeResponse(response); - return responseNode; + try (CloseableHttpResponse response = executeBinaryRequest(client, httpGet, true)) { + return readContent(response); + } catch (IOException e) { + LOGGER.error("Unable get deployments", e); + throw new RuntimeException(e); + } } } diff --git a/crp-flowable-shell/src/main/java/org/crp/flowable/shell/commands/Model.java b/crp-flowable-shell/src/main/java/org/crp/flowable/shell/commands/Model.java index 9135c19..ab2bc84 100644 --- a/crp-flowable-shell/src/main/java/org/crp/flowable/shell/commands/Model.java +++ b/crp-flowable-shell/src/main/java/org/crp/flowable/shell/commands/Model.java @@ -93,16 +93,12 @@ protected JsonNode saveModelToBarFile(CloseableHttpClient client, String modelId protected ObjectNode saveModelFromUrlToFile(CloseableHttpClient client, String url, String outputFileName) { try { - URIBuilder uriBuilder = new URIBuilder(configuration.getRestURL() + url); + URIBuilder uriBuilder = new URIBuilder(shellProperties.getRestURL() + url); HttpGet httpGet = new HttpGet(uriBuilder.build()); LOGGER.info("Getting model from url {} to file {}.", uriBuilder.getPath(), outputFileName); - CloseableHttpResponse response = executeBinaryRequest(client, httpGet, false); - - try { + try (CloseableHttpResponse response = executeBinaryRequest(client, httpGet, false)) { InputStream content = response.getEntity().getContent(); FileUtils.copyInputStreamToFile(content, new File(outputFileName)); - } finally { - closeResponse(response); } } catch (IOException | URISyntaxException e) { LOGGER.error("Unable to save file.", e); @@ -112,19 +108,16 @@ protected ObjectNode saveModelFromUrlToFile(CloseableHttpClient client, String u protected JsonNode deleteModel(CloseableHttpClient client, String modelId){ try { - URIBuilder uriBuilder = new URIBuilder(configuration.getRestURL() + properties.getModelerModels() + modelId); + URIBuilder uriBuilder = new URIBuilder(shellProperties.getRestURL() + properties.getModelerModels() + modelId); uriBuilder.addParameter("cascade", "true"); HttpDelete httpDelete = new HttpDelete(uriBuilder.build()); LOGGER.info("Deleting model id {}.", modelId); - CloseableHttpResponse response = executeBinaryRequest(client, httpDelete, false); - JsonNode responseNode = readContent(response); - try { + try (CloseableHttpResponse response = executeBinaryRequest(client, httpDelete, false)) { + JsonNode responseNode = readContent(response); LOGGER.info("Delete response {}", response.getStatusLine()); - } finally { - closeResponse(response); + return responseNode; } - return responseNode; - } catch (URISyntaxException e) { + } catch (URISyntaxException | IOException e) { LOGGER.error("Unable to deleteModel.", e); } return null; @@ -132,7 +125,7 @@ protected JsonNode deleteModel(CloseableHttpClient client, String modelId){ protected JsonNode importApp(CloseableHttpClient client, String pathToFile, String fileName, String tenantId){ try { - URIBuilder uriBuilder = new URIBuilder(configuration.getRestURL() + properties.getModelerAppDefinitions() + properties.getModelerImport()); + URIBuilder uriBuilder = new URIBuilder(shellProperties.getRestURL() + properties.getModelerAppDefinitions() + properties.getModelerImport()); HttpPost httpPost = new HttpPost(uriBuilder.build()); loginToApp(client); return uploadFile(client, pathToFile, "file", fileName, tenantId, httpPost); @@ -172,7 +165,7 @@ protected JsonNode getModels(CloseableHttpClient client, String type, String nam URIBuilder uriBuilder; HttpGet httpGet; try { - uriBuilder = new URIBuilder(configuration.getRestURL() + properties.getModelerEditorModels()). + uriBuilder = new URIBuilder(shellProperties.getRestURL() + properties.getModelerEditorModels()). addParameter("modelType", getModelType(type)). addParameter("filterText", name). addParameter("sort", "modifiedDesc"); @@ -182,11 +175,12 @@ protected JsonNode getModels(CloseableHttpClient client, String type, String nam } LOGGER.info("Calling flowable rest api {} to get models", httpGet.getURI().toString()); - CloseableHttpResponse response = executeBinaryRequest(client, httpGet, true); - - JsonNode responseNode = readContent(response); - closeResponse(response); - return responseNode; + try (CloseableHttpResponse response = executeBinaryRequest(client, httpGet, true)) { + return readContent(response); + } catch (IOException e) { + LOGGER.error("Unable to get models", e); + throw new RuntimeException("Unable to get models", e); + } } protected String getModelType(String type) { diff --git a/crp-flowable-shell/src/main/java/org/crp/flowable/shell/commands/RawRest.java b/crp-flowable-shell/src/main/java/org/crp/flowable/shell/commands/RawRest.java new file mode 100644 index 0000000..ba18d9b --- /dev/null +++ b/crp-flowable-shell/src/main/java/org/crp/flowable/shell/commands/RawRest.java @@ -0,0 +1,101 @@ +package org.crp.flowable.shell.commands; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.Arrays; + +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.StringEntity; +import org.crp.flowable.shell.configuration.FlowableShellProperties; +import org.crp.flowable.shell.utils.RestCommand; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.shell.standard.EnumValueProvider; +import org.springframework.shell.standard.ShellComponent; +import org.springframework.shell.standard.ShellMethod; +import org.springframework.shell.standard.ShellOption; +import org.springframework.util.StringUtils; + +import com.fasterxml.jackson.databind.JsonNode; + +@ShellComponent +public class RawRest extends RestCommand { + private static final Logger LOGGER = LoggerFactory.getLogger(RawRest.class); + + private final FlowableShellProperties properties; + + public RawRest(FlowableShellProperties properties) { + this.properties = properties; + } + + @ShellMethod(value = "execute url with logged in client.", key = {"exl", "execute-logged"}) + public JsonNode executeLoggedIn(@ShellOption(defaultValue = "GET", valueProvider = EnumValueProvider.class) RequestMethod method, String url, + @ShellOption(defaultValue = "") String body) { + + return executeWithLoggedInClient(client -> { + try { + HttpUriRequest httpUriRequest = createHttpUriRequest(url, method, body); + try (CloseableHttpResponse closeableHttpResponse = executeBinaryRequest(client, httpUriRequest, true)) { + return readContent(closeableHttpResponse); + } catch (IOException e) { + LOGGER.error("Unable to execute request", e); + throw new RuntimeException(e); + } + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + }); + } + + @ShellMethod(value = "execute url.", key = {"ex", "execute"}) + public JsonNode execute(@ShellOption(defaultValue = "GET", valueProvider = EnumValueProvider.class) RequestMethod method, String url, + @ShellOption(defaultValue = "") String body) { + return executeWithClient(client -> { + try { + HttpUriRequest httpUriRequest = createHttpUriRequest(url, method, body); + try (CloseableHttpResponse closeableHttpResponse = executeBinaryRequest(client, httpUriRequest, true)) { + return readContent(closeableHttpResponse); + } catch (IOException e) { + LOGGER.error("Unable to execute request", e); + throw new RuntimeException(e); + } + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + }); + } + + private HttpUriRequest createHttpUriRequest(String url, RequestMethod method, String body) throws UnsupportedEncodingException { + + switch (method) { + case GET : + return new HttpGet(shellProperties.getRestURL() + url); + case DELETE : + return new HttpDelete(shellProperties.getRestURL() + url); + case POST : + HttpPost httpPost = new HttpPost(shellProperties.getRestURL() + url); + if (StringUtils.hasText(body)) { + httpPost.setEntity(new StringEntity(body)); + } + return httpPost; + case PUT : + HttpPut httpPut = new HttpPut(shellProperties.getRestURL() + url); + if (StringUtils.hasText(body)) { + httpPut.setEntity(new StringEntity(body)); + } + return httpPut; + default : + LOGGER.error(""); + throw new RuntimeException("Unsupported method " + method + ", allowed method are " + Arrays.toString(RequestMethod.values())); + } + } + + public enum RequestMethod { + GET, POST, PUT, DELETE + } +} diff --git a/crp-flowable-shell/src/main/java/org/crp/flowable/shell/commands/TemplateProcessor.java b/crp-flowable-shell/src/main/java/org/crp/flowable/shell/commands/TemplateProcessor.java new file mode 100644 index 0000000..e6498fe --- /dev/null +++ b/crp-flowable-shell/src/main/java/org/crp/flowable/shell/commands/TemplateProcessor.java @@ -0,0 +1,132 @@ +package org.crp.flowable.shell.commands; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.utils.URIBuilder; +import org.crp.flowable.shell.configuration.FlowableShellProperties; +import org.crp.flowable.shell.utils.RestCommand; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.shell.standard.ShellCommandGroup; +import org.springframework.shell.standard.ShellComponent; +import org.springframework.shell.standard.ShellMethod; +import org.springframework.shell.standard.ShellOption; +import org.springframework.ui.freemarker.FreeMarkerConfigurationFactoryBean; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; + +import freemarker.template.TemplateException; + +@ShellCommandGroup +@ShellComponent +public class TemplateProcessor extends RestCommand { + private static final Logger LOGGER = LoggerFactory.getLogger(TemplateProcessor.class); + + private final FlowableShellProperties properties; + private final ObjectMapper objectMapper; + private final freemarker.template.Configuration freeMarkerConfiguration; + + public TemplateProcessor(FlowableShellProperties properties, ObjectMapper objectMapper, FreeMarkerConfigurationFactoryBean freeMarkerConfiguration, + freemarker.template.Configuration configuration) { + this.properties = properties; + this.objectMapper = objectMapper; + this.freeMarkerConfiguration = configuration; + } + + @ShellMethod(value = "Generate test from flowable history.", key = {"gt", "generate-test"}) + public void generateTest(String processInstanceId, String className, + @ShellOption(defaultValue = "java") String type, @ShellOption String sourceDir) { + + ObjectNode processInstanceModel = createProcessInstanceModel(processInstanceId, className); + + try(Writer pageWriter = new OutputStreamWriter(new FileOutputStream(getFullFileName(sourceDir, className)), StandardCharsets.UTF_8)) { + freeMarkerConfiguration.getTemplate("FlowableExjUnitTest.ftl").process(processInstanceModel, pageWriter); + } catch (IOException |TemplateException e) { + LOGGER.error("Unable to generate test ["+className+"]["+processInstanceId+"].", e); + } + } + + private String getFullFileName(String sourceDir, String className) { + String dir = StringUtils.hasText(sourceDir) ? sourceDir : shellProperties.getSourceTestJavaDir(); + return dir+ClassUtils.convertClassNameToResourcePath(className)+".java"; + } + + private ObjectNode createProcessInstanceModel(String processInstanceId, String className) { + JsonNode activities = getActivities(processInstanceId); + JsonNode startActivity = getStartActivity(activities); + ObjectNode model = objectMapper.createObjectNode(); + model.put("className", ClassUtils.getShortName(className)); + String packageName = ClassUtils.getPackageName(className); + if (StringUtils.hasText(packageName)) { + model.put("package", packageName); + } + model.set("startActivity", startActivity); + model.put("processDefinitionKey", getProcessDefinitionKey(startActivity.get("processDefinitionId").asText())); + model.set("taskActivities", getTaskActivities(activities)); + return model; + } + + private ArrayNode getTaskActivities(JsonNode activities) { + ArrayNode tasks = objectMapper.createArrayNode(); + for (JsonNode activity : activities.get("data")) { + JsonNode activityType = activity.get("activityType"); + if (activityType != null && "userTask".equals(activityType.asText())) { + tasks.add(activity); + } + } + return tasks; + } + + private JsonNode getStartActivity(JsonNode activities) { + ArrayNode data = (ArrayNode) activities.get("data"); + for (JsonNode activity : data) { + if ("startEvent".equals(activity.get("activityType").asText())) { + return activity; + } + } + throw new RuntimeException("StartEvent was not found."); + } + + private JsonNode getActivities(String processInstanceId) { + return executeWithClient(client -> { + try { + URIBuilder uriBuilder = new URIBuilder(shellProperties.getRestURL() + properties.getHistoricActivityInstances()); + uriBuilder.addParameter("processInstanceId", processInstanceId); + HttpGet httpGet = new HttpGet(uriBuilder.build()); + try (CloseableHttpResponse closeableHttpResponse = executeBinaryRequest(client, httpGet, true)) { + return readContent(closeableHttpResponse); + } + } catch (IOException | URISyntaxException e) { + LOGGER.error("Unable to get history.", e); + throw new RuntimeException(e); + } + }); + } + + private String getProcessDefinitionKey(String processDefinitionId) { + return executeWithLoggedInClient(client -> { + try { + HttpGet httpGet = new HttpGet(shellProperties.getRestURL() + properties.getProcessDefinitions() +"/" +processDefinitionId); + try (CloseableHttpResponse closeableHttpResponse = executeBinaryRequest(client, httpGet, true)) { + return readContent(closeableHttpResponse); + } + } catch (IOException e) { + LOGGER.error("Unable to get process definition.", e); + throw new RuntimeException(e); + } + }).get("key").asText(); + } + +} diff --git a/crp-flowable-shell/src/main/java/org/crp/flowable/shell/commands/Utils.java b/crp-flowable-shell/src/main/java/org/crp/flowable/shell/commands/Utils.java index f839848..cf1df91 100644 --- a/crp-flowable-shell/src/main/java/org/crp/flowable/shell/commands/Utils.java +++ b/crp-flowable-shell/src/main/java/org/crp/flowable/shell/commands/Utils.java @@ -1,7 +1,7 @@ package org.crp.flowable.shell.commands; import org.apache.commons.io.IOUtils; -import org.crp.flowable.shell.utils.Configuration; +import org.crp.flowable.shell.configuration.FlowableShellProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -23,7 +23,7 @@ public class Utils { private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class); @Autowired - private Configuration configuration; + private FlowableShellProperties configuration; @ShellMethod("Configure flowable rest endpoint.") public String configure(@ShellOption(defaultValue = "") String login, diff --git a/crp-flowable-shell/src/main/java/org/crp/flowable/shell/configuration/FlowableShellConfiguration.java b/crp-flowable-shell/src/main/java/org/crp/flowable/shell/configuration/FlowableShellConfiguration.java index a6eac26..f63215e 100644 --- a/crp-flowable-shell/src/main/java/org/crp/flowable/shell/configuration/FlowableShellConfiguration.java +++ b/crp-flowable-shell/src/main/java/org/crp/flowable/shell/configuration/FlowableShellConfiguration.java @@ -1,7 +1,5 @@ package org.crp.flowable.shell.configuration; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.crp.flowable.shell.FlowableCliApplicationRunner; import org.crp.flowable.shell.utils.JsonNodeResultHandler; import org.jline.utils.AttributedString; @@ -12,6 +10,9 @@ import org.springframework.shell.ResultHandler; import org.springframework.shell.jline.PromptProvider; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + @Configuration public class FlowableShellConfiguration { @@ -20,16 +21,6 @@ ObjectMapper objectMapper() { return new ObjectMapper(); } - @Bean - org.crp.flowable.shell.utils.Configuration configuration() { - org.crp.flowable.shell.utils.Configuration configuration = new org.crp.flowable.shell.utils.Configuration(); - configuration.setLogin("admin"); - configuration.setPassword("test"); - configuration.setRestURL("http://localhost:8080/flowable-ui/"); - configuration.setIdmURL("http://localhost:8080/flowable-ui/"); - return configuration; - } - @Bean ResultHandler jsonNodeResultHandler() { return new JsonNodeResultHandler(); @@ -44,4 +35,5 @@ public PromptProvider flowableShellPromptProvider() { public CommandLineRunner commandLineRunner() { return new FlowableCliApplicationRunner(); } + } diff --git a/crp-flowable-shell/src/main/java/org/crp/flowable/shell/configuration/FlowableShellProperties.java b/crp-flowable-shell/src/main/java/org/crp/flowable/shell/configuration/FlowableShellProperties.java index 4f42bd5..4dc1420 100644 --- a/crp-flowable-shell/src/main/java/org/crp/flowable/shell/configuration/FlowableShellProperties.java +++ b/crp-flowable-shell/src/main/java/org/crp/flowable/shell/configuration/FlowableShellProperties.java @@ -6,6 +6,13 @@ @Configuration @ConfigurationProperties(prefix = "crp.flowable.shell") public class FlowableShellProperties { + + private String login; + private String password; + private String restURL; + private String idmURL; + private String sourceTestJavaDir; + /** * prefix for modeler app definitions rest call */ @@ -25,6 +32,9 @@ public class FlowableShellProperties { private String modelerImport; private String modelerEditorModels; + private String historicActivityInstances; + private String processDefinitions; + /** * Deployment deploy rest endpoint */ @@ -85,4 +95,60 @@ public String getDeploymentDeploy() { public void setDeploymentDeploy(String deploymentDeploy) { this.deploymentDeploy = deploymentDeploy; } + + public String getHistoricActivityInstances() { + return historicActivityInstances; + } + + public void setHistoricActivityInstances(String historicActivityInstances) { + this.historicActivityInstances = historicActivityInstances; + } + + public String getProcessDefinitions() { + return processDefinitions; + } + + public void setProcessDefinitions(String processDefinitions) { + this.processDefinitions = processDefinitions; + } + + public String getRestURL() { + return restURL; + } + + public void setRestURL(String restURL) { + this.restURL = restURL; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + public String getIdmURL() { + return idmURL; + } + + public void setIdmURL(String idmURL) { + this.idmURL = idmURL; + } + + public String getSourceTestJavaDir() { + return sourceTestJavaDir; + } + + public void setSourceTestJavaDir(String sourceTestJavaDir) { + this.sourceTestJavaDir = sourceTestJavaDir; + } } diff --git a/crp-flowable-shell/src/main/java/org/crp/flowable/shell/utils/Configuration.java b/crp-flowable-shell/src/main/java/org/crp/flowable/shell/utils/Configuration.java deleted file mode 100644 index 3a5c160..0000000 --- a/crp-flowable-shell/src/main/java/org/crp/flowable/shell/utils/Configuration.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.crp.flowable.shell.utils; - -public class Configuration { - private String login; - private String password; - private String restURL; - private String idmURL; - - public String getRestURL() { - return restURL; - } - - public void setRestURL(String restURL) { - this.restURL = restURL; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public String getLogin() { - return login; - } - - public void setLogin(String login) { - this.login = login; - } - - public String getIdmURL() { - return idmURL; - } - - public void setIdmURL(String idmURL) { - this.idmURL = idmURL; - } -} diff --git a/crp-flowable-shell/src/main/java/org/crp/flowable/shell/utils/RestCommand.java b/crp-flowable-shell/src/main/java/org/crp/flowable/shell/utils/RestCommand.java index 6729f27..feaaaa7 100644 --- a/crp-flowable-shell/src/main/java/org/crp/flowable/shell/utils/RestCommand.java +++ b/crp-flowable-shell/src/main/java/org/crp/flowable/shell/utils/RestCommand.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; + import org.apache.http.HttpHeaders; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; @@ -14,6 +15,7 @@ import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.message.BasicHeader; +import org.crp.flowable.shell.configuration.FlowableShellProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -28,13 +30,13 @@ public class RestCommand { private static final Logger LOGGER = LoggerFactory.getLogger(RestCommand.class); @Autowired - protected Configuration configuration; + protected FlowableShellProperties shellProperties; @Autowired protected ObjectMapper objectMapper; protected CloseableHttpClient createClient() { CredentialsProvider provider = new BasicCredentialsProvider(); - UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(configuration.getLogin(), configuration.getPassword()); + UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(shellProperties.getLogin(), shellProperties.getPassword()); provider.setCredentials(AuthScope.ANY, credentials); return HttpClientBuilder.create().setDefaultCredentialsProvider(provider).build(); } @@ -46,11 +48,12 @@ protected JsonNode uploadFile(CloseableHttpClient client, String pathToApplicati httpPost.setEntity( HttpMultipartHelper.getMultiPartEntity(file, fileName, "application/zip", bis, Collections.singletonMap("tenantId", tenantId))); LOGGER.info("Calling flowable rest api {} to deploy {} into tenantId {}", httpPost.getURI().toString(), pathToApplication, tenantId); - CloseableHttpResponse response = executeBinaryRequest(client, httpPost, false); - - JsonNode responseNode = readContent(response); - closeResponse(response); - return responseNode; + try (CloseableHttpResponse closeableHttpResponse = executeBinaryRequest(client, httpPost, false)) { + return readContent(closeableHttpResponse); + } catch (IOException e) { + LOGGER.error("Unable to execute request", e); + throw new RuntimeException(e); + } } catch (IOException e) { throw new UncheckedIOException(e); } @@ -66,20 +69,21 @@ private InputStream getApplicationInputStream(String pathToApplication) throws F } protected boolean loginToApp(CloseableHttpClient client) throws URISyntaxException { - URIBuilder idmUriBuilder = new URIBuilder(configuration.getIdmURL() + "/app/authentication"). - addParameter("j_username", configuration.getLogin()).addParameter("j_password", configuration.getPassword()). + URIBuilder idmUriBuilder = new URIBuilder(shellProperties.getIdmURL() + "/app/authentication"). + addParameter("j_username", shellProperties.getLogin()).addParameter("j_password", shellProperties.getPassword()). addParameter("_spring_security_remember_me", "true").addParameter("submit", "Login"); HttpPost appLogin = new HttpPost(idmUriBuilder.build()); - CloseableHttpResponse idmResponse = executeBinaryRequest(client, appLogin, false); - try { + try (CloseableHttpResponse idmResponse = executeBinaryRequest(client, appLogin, false)) { if (idmResponse.getStatusLine().getStatusCode() != HTTP_OK) { LOGGER.error("Unable to establish connection to modeler app {}", idmResponse.getStatusLine()); return false; + } else { + return true; } - } finally { - closeResponse(idmResponse); + } catch (IOException e) { + LOGGER.error("unable to establish connection to modeler", e); + throw new RuntimeException(e); } - return true; } protected JsonNode executeWithClient(ExecuteWithClient exec) { @@ -99,6 +103,23 @@ protected void closeClient(CloseableHttpClient client) { } } + protected JsonNode executeWithLoggedInClient(ExecuteWithClient exec) { + CloseableHttpClient client = createClient(); + try { + if (loginToApp(client)) { + return exec.execute(client); + } else { + LOGGER.error("Unable to login."); + throw new RuntimeException("Unable to login"); + } + } catch (URISyntaxException e) { + LOGGER.error("Login failed.", e); + throw new RuntimeException(e); + } finally { + closeClient(client); + } + } + protected CloseableHttpResponse executeBinaryRequest(CloseableHttpClient client, HttpUriRequest request, boolean addJsonContentType) { try { if (addJsonContentType && request.getFirstHeader(HttpHeaders.CONTENT_TYPE) == null) { @@ -112,10 +133,17 @@ protected CloseableHttpResponse executeBinaryRequest(CloseableHttpClient client, } protected JsonNode readContent(CloseableHttpResponse response) { - try { - return objectMapper.readTree(response.getEntity().getContent()); - } catch (IOException e) { - throw new UncheckedIOException(e); + if (response.getStatusLine().getStatusCode() < 300 && response.getStatusLine().getStatusCode() >199) { + try { + if (response.getEntity() != null) { + return objectMapper.readTree(response.getEntity().getContent()); + } + return objectMapper.createObjectNode(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } else { + throw new RuntimeException("The response is not correct " + response.getStatusLine()); } } diff --git a/crp-flowable-shell/src/main/resources/application.properties b/crp-flowable-shell/src/main/resources/application.properties index d55f1c2..4078be8 100644 --- a/crp-flowable-shell/src/main/resources/application.properties +++ b/crp-flowable-shell/src/main/resources/application.properties @@ -1,3 +1,8 @@ +crp.flowable.shell.login=admin +crp.flowable.shell.password=test +crp.flowable.shell.restURL=http://localhost:8080/flowable-ui +crp.flowable.shell.idmURL=http://localhost:8080/flowable-ui + crp.flowable.shell.modelerAppDefinitions=/modeler-app/rest/app-definitions/ crp.flowable.shell.modelerExport=/export @@ -7,3 +12,7 @@ crp.flowable.shell.modelerImport=import?renewIdmEntries=false crp.flowable.shell.modelerEditorModels=/api/editor/models crp.flowable.shell.deploymentDeploy=/app-api/app-repository/deployments + +crp.flowable.shell.historicActivityInstances=/process-api/history/historic-activity-instances +crp.flowable.shell.processDefinitions=/process-api/repository/process-definitions +crp.flowable.shell.sourceTestJavaDir=src/test/java/ \ No newline at end of file diff --git a/crp-flowable-shell/src/main/resources/templates/FlowableExjUnitTest.ftl b/crp-flowable-shell/src/main/resources/templates/FlowableExjUnitTest.ftl new file mode 100644 index 0000000..5154a9e --- /dev/null +++ b/crp-flowable-shell/src/main/resources/templates/FlowableExjUnitTest.ftl @@ -0,0 +1,49 @@ +<#if package??>package ${package.asText()} + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collections; +import java.util.List; + +import org.flowable.engine.HistoryService; +import org.flowable.engine.ManagementService; +import org.flowable.engine.RepositoryService; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.engine.TaskService; +import org.flowable.engine.event.EventLogEntry; +import org.flowable.engine.history.HistoricActivityInstance; +import org.flowable.engine.test.FlowableTest; +import org.flowable.task.api.Task; +import org.flowable.task.api.history.HistoricTaskLogEntry; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@FlowableTest +class ${className.asText()} { + + @Test + void case1(RuntimeService runtimeService, TaskService taskService) { + ProcessInstance processInstance = null; + try { + // start process instance ${processDefinitionKey.asText()} + processInstance = runtimeService.createProcessInstanceBuilder().processDefinitionKey("${processDefinitionKey.asText()}").start(); + assertThat(processInstance).isNotNull(); +<#list taskActivities.iterator() as taskActivity> + + // complete task ${taskActivity.get("activityName").asText()} + Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()). + taskDefinitionKey("${taskActivity.get('activityId').asText()}"). + singleResult(); + assertThat(task.getName()).isEqualTo("${taskActivity.get('activityName').asText()}"); + taskService.complete(task.getId()); + + } finally { + if (processInstance != null) { + runtimeService.deleteProcessInstance(processInstance.getId(), "test cleanup"); + } + } + } + +} diff --git a/crp-flowable-shell/src/test/java/org/crp/flowable/shell/DeploymentIT.java b/crp-flowable-shell/src/test/java/org/crp/flowable/shell/DeploymentIT.java index 99f4279..de67421 100644 --- a/crp-flowable-shell/src/test/java/org/crp/flowable/shell/DeploymentIT.java +++ b/crp-flowable-shell/src/test/java/org/crp/flowable/shell/DeploymentIT.java @@ -1,5 +1,7 @@ package org.crp.flowable.shell; +import static org.assertj.core.api.Assertions.assertThat; + import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -7,8 +9,6 @@ import org.springframework.shell.Shell; import org.springframework.shell.jline.InteractiveShellApplicationRunner; -import static org.assertj.core.api.Assertions.assertThat; - @SpringBootTest(properties = { InteractiveShellApplicationRunner.SPRING_SHELL_INTERACTIVE_ENABLED + "=" + false }) public class DeploymentIT { @Autowired diff --git a/crp-flowable-shell/src/test/java/org/crp/flowable/shell/RawRestIT.java b/crp-flowable-shell/src/test/java/org/crp/flowable/shell/RawRestIT.java new file mode 100644 index 0000000..d55517e --- /dev/null +++ b/crp-flowable-shell/src/test/java/org/crp/flowable/shell/RawRestIT.java @@ -0,0 +1,50 @@ +package org.crp.flowable.shell; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.IOException; + +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.shell.Shell; +import org.springframework.shell.jline.InteractiveShellApplicationRunner; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +@SpringBootTest(properties = { InteractiveShellApplicationRunner.SPRING_SHELL_INTERACTIVE_ENABLED + "=" + false }) +public class RawRestIT { + @Autowired + private Shell shell; + + @Test + void deployModel() throws IOException { + // given: deploy app + assertThat(shell.evaluate(() -> "deploy src/test/resources/app.bar --deployment-name testFileName.bar").toString()). + contains("\"name\":\"testFileName\""); + + + // get + ObjectNode deploymentsQueryResult = (ObjectNode) shell.evaluate(() -> "ex GET /app-api/app-repository/deployments"); + assertThat(deploymentsQueryResult.toString()). + contains("\"name\":\"testFileName\""); + String deploymentId = deploymentsQueryResult.get("data").get(0).get("id").asText(); + + // post + ObjectNode startProcessResult = (ObjectNode) shell.evaluate( + () -> "ex POST /process-api/runtime/process-instances {\"processDefinitionKey\":\"oneTaskProcess\",\"businessKey\":\"myBusinessKey\"}" + ); + String processId = startProcessResult.get("id").asText(); + assertThat(processId).isNotEmpty(); + + // put + assertThat(shell.evaluate(() -> "exl PUT /process-api/runtime/process-instances/"+processId+" {\"name\":\"nameone\"}").toString()).contains("nameone"); + + // delete + assertThat(shell.evaluate(() -> "ex DELETE /app-api/app-repository/deployments/"+deploymentId).toString()).isEqualTo("{}"); + + } + +} diff --git a/crp-flowable-shell/src/test/java/org/crp/flowable/shell/TemplateProcessorIT.java b/crp-flowable-shell/src/test/java/org/crp/flowable/shell/TemplateProcessorIT.java new file mode 100644 index 0000000..990a6b8 --- /dev/null +++ b/crp-flowable-shell/src/test/java/org/crp/flowable/shell/TemplateProcessorIT.java @@ -0,0 +1,73 @@ +package org.crp.flowable.shell; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; + +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.shell.Shell; +import org.springframework.shell.jline.InteractiveShellApplicationRunner; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +@SpringBootTest(properties = { InteractiveShellApplicationRunner.SPRING_SHELL_INTERACTIVE_ENABLED + "=" + false }) +public class TemplateProcessorIT { + @Autowired + private Shell shell; + @Autowired + private ObjectMapper objectMapper; + + @Test + void generateJUnitTest() throws IOException { + JsonNode deployment = createDeployment(); + + String processInstanceId = generateHistory(); + + shell.evaluate(() -> "gt "+ processInstanceId +" OneTaskProcessTest --sourceDir target/"); + + assertThat(IOUtils.contentEquals(getReader("src/test/resources/jUnitTest.java"), getReader("target/OneTaskProcessTest.java"))); + + // cleanup + (new File("target/OneTaskProcessTest.java")).delete(); + assertThat(shell.evaluate(() -> "exl DELETE /app-api/app-repository/deployments/"+deployment.get("id").asText()).toString()).isEqualTo("{}"); + + } + + private JsonNode createDeployment() { + JsonNode deployment = (JsonNode) shell.evaluate(() -> "deploy src/test/resources/app.bar --deployment-name testFileName.bar"); + assertThat(deployment.toString()). + contains("\"name\":\"testFileName\""); + return deployment; + } + + private Reader getReader(String s) throws FileNotFoundException { + return new BufferedReader(new FileReader(s)); + } + + private String generateHistory() { + // start process + ObjectNode startProcessResult = (ObjectNode) shell.evaluate( + () -> "ex POST /process-api/runtime/process-instances {\"processDefinitionKey\":\"oneTaskProcess\",\"businessKey\":\"myBusinessKey\"}" + ); + String processId = startProcessResult.get("id").asText(); + assertThat(processId).isNotEmpty(); + ObjectNode tasks = (ObjectNode) shell.evaluate(() -> "exl POST /app/rest/query/tasks {\"processInstanceId\":\""+processId+"\"}"); + String taskId = tasks.get("data").get(0).get("id").asText(); + shell.evaluate(() -> "exl PUT /app/rest/tasks/"+taskId+"/action/complete"); + + return processId; + } +} diff --git a/crp-flowable-shell/src/test/resources/jUnitTest.java b/crp-flowable-shell/src/test/resources/jUnitTest.java new file mode 100644 index 0000000..12901a9 --- /dev/null +++ b/crp-flowable-shell/src/test/resources/jUnitTest.java @@ -0,0 +1,32 @@ +import org.flowable.engine.RuntimeService; +import org.flowable.engine.TaskService; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.engine.test.FlowableTest; +import org.flowable.task.api.Task; +import org.junit.jupiter.api.Test; + +@FlowableTest +class jUnitTest { + + @Test + void case1(RuntimeService runtimeService, TaskService taskService) { + ProcessInstance processInstance = null; + try { + // start process instance oneTaskProcess + processInstance = runtimeService.createProcessInstanceBuilder().processDefinitionKey("oneTaskProcess").start(); + assertThat(processInstance).isNotNull(); + + // complete task oneTask + Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()). + taskDefinitionKey("sid-D1E992D9-4806-408B-8589-9D56DF95E121"). + singleResult(); + assertThat(task.getName()).isEqualTo("oneTask"); + taskService.complete(task.getId()); + } finally { + if (processInstance != null) { + runtimeService.deleteProcessInstance(processInstance.getId(), "test cleanup"); + } + } + } + +} diff --git a/crp-flowable-spock/pom.xml b/crp-flowable-spock/pom.xml index 97bab0e..1604d56 100644 --- a/crp-flowable-spock/pom.xml +++ b/crp-flowable-spock/pom.xml @@ -2,7 +2,7 @@ 4.0.0 - com.github.crystal-processes + io.github.crystal-processes crp-flowable-ex 0.0.4-SNAPSHOT