diff --git a/bonita-engine-standalone/src/main/java/org/bonitasoft/engine/BonitaEngine.java b/bonita-engine-standalone/src/main/java/org/bonitasoft/engine/BonitaEngine.java index b9233db3862..6985ce07836 100644 --- a/bonita-engine-standalone/src/main/java/org/bonitasoft/engine/BonitaEngine.java +++ b/bonita-engine-standalone/src/main/java/org/bonitasoft/engine/BonitaEngine.java @@ -115,7 +115,7 @@ private void initializeJNDI() throws NamingException { public void start() throws Exception { initializeEnvironment(); - PlatformSetup platformSetup = PlatformSetupAccessor.getPlatformSetup(); + PlatformSetup platformSetup = getPlatformSetup(); platformSetup.init(); PlatformSession platformSession = loginOnPlatform(); @@ -126,6 +126,10 @@ public void start() throws Exception { logoutFromPlatform(platformSession); } + protected PlatformSetup getPlatformSetup() throws NamingException { + return PlatformSetupAccessor.getInstance().getPlatformSetup(); + } + private void logoutFromPlatform(PlatformSession platformSession) throws PlatformLogoutException, SessionNotFoundException, BonitaHomeNotSetException, ServerAPIException, UnknownAPITypeException { diff --git a/bonita-integration-tests/bonita-integration-tests-client/src/main/java/org/bonitasoft/engine/CommonAPIIT.java b/bonita-integration-tests/bonita-integration-tests-client/src/main/java/org/bonitasoft/engine/CommonAPIIT.java index cb0b23b3240..af2866c2475 100644 --- a/bonita-integration-tests/bonita-integration-tests-client/src/main/java/org/bonitasoft/engine/CommonAPIIT.java +++ b/bonita-integration-tests/bonita-integration-tests-client/src/main/java/org/bonitasoft/engine/CommonAPIIT.java @@ -60,10 +60,6 @@ public void clean() throws Exception { } }; - /** - * @return warning list of unclean elements - * @throws BonitaException - */ private void clean() throws BonitaException { loginOnDefaultTenantWithDefaultTechnicalUser(); cleanCommands(); diff --git a/bonita-integration-tests/bonita-integration-tests-client/src/test/resources/logback-test.xml b/bonita-integration-tests/bonita-integration-tests-client/src/test/resources/logback-test.xml index 33746fcdf05..06243d23e57 100644 --- a/bonita-integration-tests/bonita-integration-tests-client/src/test/resources/logback-test.xml +++ b/bonita-integration-tests/bonita-integration-tests-client/src/test/resources/logback-test.xml @@ -10,6 +10,7 @@ + diff --git a/bonita-integration-tests/bonita-integration-tests-local/src/test/java/org/bonitasoft/engine/job/JobExecutionIT.java b/bonita-integration-tests/bonita-integration-tests-local/src/test/java/org/bonitasoft/engine/job/JobExecutionIT.java index b10b2dbc52e..8e6f1931a06 100644 --- a/bonita-integration-tests/bonita-integration-tests-local/src/test/java/org/bonitasoft/engine/job/JobExecutionIT.java +++ b/bonita-integration-tests/bonita-integration-tests-local/src/test/java/org/bonitasoft/engine/job/JobExecutionIT.java @@ -19,9 +19,7 @@ import static org.hamcrest.collection.IsCollectionWithSize.hasSize; import static org.junit.Assert.assertEquals; -import java.io.Serializable; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; @@ -147,13 +145,12 @@ private List searchJobDescriptors(final int nbOfExpectedJobDescr public void retryAJob_should_update_job_log_when_execution_fails_again() throws Exception { //given getCommandAPI().register("except", "Throws Exception when scheduling a job", AddJobCommand.class.getName()); - final Map parameters = new HashMap<>(); try { - getCommandAPI().execute("except", parameters); + getCommandAPI().execute("except", Map.of()); FailedJob failedJob = await().until(() -> getProcessAPI().getFailedJobs(0, 100), hasSize(1)).get(0); //when - getProcessAPI().replayFailedJob(failedJob.getJobDescriptorId(), emptyMap()); + getProcessAPI().replayFailedJob(failedJob.getJobDescriptorId()); //then failedJob = await().until(() -> getProcessAPI().getFailedJobs(0, 100), hasSize(1)).get(0); diff --git a/bonita-integration-tests/bonita-test-utils/build.gradle b/bonita-integration-tests/bonita-test-utils/build.gradle index 9fc8e92ec88..f745de90326 100644 --- a/bonita-integration-tests/bonita-test-utils/build.gradle +++ b/bonita-integration-tests/bonita-test-utils/build.gradle @@ -6,6 +6,9 @@ dependencies { api libs.commonsIO api "xmlunit:xmlunit:${Deps.xmlunitVersion}" api "org.assertj:assertj-core:${Deps.assertjVersion}" + + implementation project(':bpm:bonita-core:bonita-process-engine') + implementation libs.springTest } publishing { diff --git a/bonita-integration-tests/bonita-test-utils/src/main/java/org/bonitasoft/engine/test/EventUtil.java b/bonita-integration-tests/bonita-test-utils/src/main/java/org/bonitasoft/engine/test/EventUtil.java deleted file mode 100644 index cecd29a0fdb..00000000000 --- a/bonita-integration-tests/bonita-test-utils/src/main/java/org/bonitasoft/engine/test/EventUtil.java +++ /dev/null @@ -1,152 +0,0 @@ -/** - * Copyright (C) 2019 Bonitasoft S.A. - * Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble - * This library is free software; you can redistribute it and/or modify it under the terms - * of the GNU Lesser General Public License as published by the Free Software Foundation - * version 2.1 of the License. - * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * You should have received a copy of the GNU Lesser General Public License along with this - * program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth - * Floor, Boston, MA 02110-1301, USA. - **/ -package org.bonitasoft.engine.test; - -import java.io.IOException; -import java.io.Serializable; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.TimeoutException; - -import org.bonitasoft.engine.api.CommandAPI; -import org.bonitasoft.engine.api.TenantAPIAccessor; -import org.bonitasoft.engine.command.CommandExecutionException; -import org.bonitasoft.engine.command.CommandNotFoundException; -import org.bonitasoft.engine.command.CommandParameterizationException; -import org.bonitasoft.engine.exception.BonitaException; -import org.bonitasoft.engine.exception.RetrieveException; -import org.bonitasoft.engine.io.IOUtil; -import org.bonitasoft.engine.session.APISession; - -/** - * Utility methods to get maps that match process as wanted - * - * @author Baptiste Mesta - */ -public class EventUtil { - - private static final String FLOW_NODE = "flowNode"; - - private static final String PROCESS = "process"; - - private static final String TYPE = "type"; - - private static final String NAME = "name"; - - private static final String ROOT_CONTAINER_ID = "rootContainerId"; - - private static final String PARENT_CONTAINER_ID = "parentContainerId"; - - private static final String STATE_ID = "stateId"; - - private static final String ID = "id"; - - private static final String STATE = "state"; - - public static Map getReadyTaskEvent(final long processInstanceId, final String taskName) { - final Map map = new HashMap(5); - map.put(TYPE, FLOW_NODE); - map.put(ROOT_CONTAINER_ID, processInstanceId); - map.put(NAME, taskName); - map.put(STATE_ID, 4); - return map; - } - - public static Map getProcessInstanceFinishedEvent(final long processInstanceId) { - final Map map = new HashMap(4); - map.put(TYPE, PROCESS); - map.put(ID, processInstanceId); - map.put(STATE_ID, 6); - return map; - } - - public static void undeployCommand(final APISession apiSession) throws BonitaException { - final CommandAPI commandAPI = TenantAPIAccessor.getCommandAPI(apiSession); - commandAPI.unregister("waitServerCommand"); - commandAPI.removeDependency("commands"); - } - - public static void deployCommand(final APISession apiSession) throws BonitaException { - final CommandAPI commandAPI = TenantAPIAccessor.getCommandAPI(apiSession); - - byte[] commandJar; - try { - commandJar = IOUtil.getAllContentFrom(APITestUtil.class.getResourceAsStream("/server-command.bak")); - } catch (final IOException e) { - throw new RetrieveException(e); - } - commandAPI.addDependency("commands", commandJar); - - commandAPI.register("waitServerCommand", "waitServerCommand", - "org.bonitasoft.engine.test.synchro.WaitServerCommand"); - commandAPI.register("addHandlerCommand", "addHandlerCommand", - "org.bonitasoft.engine.test.synchro.AddHandlerCommand"); - - final Map parameters = Collections.emptyMap(); - commandAPI.execute("addHandlerCommand", parameters); - } - - static Long executeWaitServerCommand(final CommandAPI commandAPI, final Map event, - final int defaultTimeout) - throws CommandNotFoundException, CommandParameterizationException, CommandExecutionException, - TimeoutException { - final Map parameters = new HashMap(2); - parameters.put("event", (Serializable) event); - parameters.put("timeout", defaultTimeout); - try { - return (Long) commandAPI.execute("waitServerCommand", parameters); - } catch (final CommandExecutionException e) { - if (e.getMessage().toLowerCase().contains("timeout")) { - throw new TimeoutException("Timeout (" + defaultTimeout + "ms)looking for element: " + event); - } - throw e; - } - } - - public static void clearRepo(final CommandAPI commandAPI) throws BonitaException { - final Map parameters = new HashMap(); - parameters.put("clear", true); - commandAPI.execute("waitServerCommand", parameters); - } - - public static Map getTaskInState(final long processInstanceId, final String stateName) { - final Map map = new HashMap(3); - map.put(TYPE, FLOW_NODE); - map.put(ROOT_CONTAINER_ID, processInstanceId); - map.put(STATE, stateName); - return map; - } - - public static Map getTaskInState(final long processInstanceId, final String state, - final String flowNodeName) { - final Map map = new HashMap(3); - map.put(TYPE, FLOW_NODE); - map.put(ROOT_CONTAINER_ID, processInstanceId); - map.put(STATE, state); - map.put(NAME, flowNodeName); - return map; - } - - public static Map getTaskInStateWithParentId(final long processInstanceId, final String state, - final String flowNodeName) { - final Map map = new HashMap(3); - map.put(TYPE, FLOW_NODE); - map.put(PARENT_CONTAINER_ID, processInstanceId); - map.put(STATE, state); - map.put(NAME, flowNodeName); - return map; - } - -} diff --git a/bonita-integration-tests/bonita-test-utils/src/main/java/org/bonitasoft/engine/test/PlatformTestUtil.java b/bonita-integration-tests/bonita-test-utils/src/main/java/org/bonitasoft/engine/test/PlatformTestUtil.java index 90c86e66b8a..7ab58df7664 100644 --- a/bonita-integration-tests/bonita-test-utils/src/main/java/org/bonitasoft/engine/test/PlatformTestUtil.java +++ b/bonita-integration-tests/bonita-test-utils/src/main/java/org/bonitasoft/engine/test/PlatformTestUtil.java @@ -23,11 +23,16 @@ import org.bonitasoft.engine.exception.BonitaHomeNotSetException; import org.bonitasoft.engine.exception.ServerAPIException; import org.bonitasoft.engine.exception.UnknownAPITypeException; +import org.bonitasoft.engine.execution.ProcessStarterVerifier; import org.bonitasoft.engine.session.PlatformSession; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; /** * @author Celine Souchet */ +@ContextConfiguration(classes = PlatformTestUtil.TestConfiguration.class) public class PlatformTestUtil { public static final String DEFAULT_TECHNICAL_LOGGER_USERNAME = "install"; @@ -72,4 +77,17 @@ public void deployCommandsOnDefaultTenant() throws BonitaException { apiClient.logout(); } + /** + * Configuration class used to override bean definitions for test purposes. + */ + @Configuration + static class TestConfiguration { + + @Bean + ProcessStarterVerifier processStarterVerifierImpl() { + return processInstance -> { + // Override this bean to disable the process starter verifier + }; + } + } } diff --git a/bonita-test-api/build.gradle b/bonita-test-api/build.gradle index 2636a0b4a49..62e8a3ada06 100644 --- a/bonita-test-api/build.gradle +++ b/bonita-test-api/build.gradle @@ -4,6 +4,8 @@ dependencies { api "junit:junit:${Deps.junit4Version}" api(project(':platform:platform-resources')) + implementation libs.springTest + // for http tests: compileOnly("org.eclipse.jetty:jetty-server:${Deps.jettyVersion}") compileOnly("org.eclipse.jetty:jetty-servlet:${Deps.jettyVersion}") diff --git a/bonita-test-api/src/main/java/org/bonitasoft/engine/test/TestEngineImpl.java b/bonita-test-api/src/main/java/org/bonitasoft/engine/test/TestEngineImpl.java index 80b4d0c8815..010e8d4725a 100644 --- a/bonita-test-api/src/main/java/org/bonitasoft/engine/test/TestEngineImpl.java +++ b/bonita-test-api/src/main/java/org/bonitasoft/engine/test/TestEngineImpl.java @@ -16,15 +16,20 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.bonitasoft.engine.BonitaDatabaseConfiguration; +import org.bonitasoft.engine.execution.ProcessStarterVerifier; import org.bonitasoft.engine.test.http.BonitaHttpServer; import org.bonitasoft.engine.test.internal.EngineCommander; import org.bonitasoft.engine.test.internal.EngineStarter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; /** * @author Baptiste Mesta */ @Getter @Slf4j +@ContextConfiguration(classes = TestEngineImpl.TestConfiguration.class) public class TestEngineImpl implements TestEngine { private static TestEngineImpl INSTANCE = createTestEngine(); @@ -129,4 +134,17 @@ public void setBusinessDataDatabaseProperties(BonitaDatabaseConfiguration databa this.businessDataDatabaseConfiguration = database; } + /** + * Configuration class used to override bean definitions for test purposes. + */ + @Configuration + static class TestConfiguration { + + @Bean + ProcessStarterVerifier processStarterVerifierImpl() { + return processInstance -> { + // Override this bean to disable the process starter verifier + }; + } + } } diff --git a/bonita-test-api/src/main/java/org/bonitasoft/engine/test/internal/EngineStarter.java b/bonita-test-api/src/main/java/org/bonitasoft/engine/test/internal/EngineStarter.java index 294f2474c5a..608a22c3901 100644 --- a/bonita-test-api/src/main/java/org/bonitasoft/engine/test/internal/EngineStarter.java +++ b/bonita-test-api/src/main/java/org/bonitasoft/engine/test/internal/EngineStarter.java @@ -21,6 +21,8 @@ import java.nio.file.Path; import java.util.*; +import javax.naming.NamingException; + import org.apache.commons.io.FileUtils; import org.bonitasoft.engine.BonitaDatabaseConfiguration; import org.bonitasoft.engine.BonitaEngine; @@ -56,7 +58,7 @@ public class EngineStarter { private static boolean hasFailed = false; private boolean dropOnStart = true; - private BonitaEngine engine; + private final BonitaEngine engine; protected EngineStarter(BonitaEngine engine) { this.engine = engine; @@ -76,7 +78,7 @@ public void start() throws Exception { } try { LOGGER.info("====================================================="); - LOGGER.info("============ Starting Bonita Engine ==========="); + LOGGER.info("============== Starting Bonita Engine ============="); LOGGER.info("====================================================="); final long startTime = System.currentTimeMillis(); System.setProperty("com.arjuna.ats.arjuna.common.propertiesFile", "jbossts-properties.xml"); @@ -102,12 +104,16 @@ public void start() throws Exception { } protected void setupPlatform() throws Exception { - PlatformSetup platformSetup = PlatformSetupAccessor.getPlatformSetup(); + PlatformSetup platformSetup = getPlatformSetup(); if (isDropOnStart()) { platformSetup.destroy(); } } + protected PlatformSetup getPlatformSetup() throws NamingException { + return PlatformSetupAccessor.getInstance().getPlatformSetup(); + } + //-------------- engine life cycle methods protected void prepareEnvironment() throws Exception { diff --git a/bpm/bonita-api/bonita-server-api-http/src/main/java/org/bonitasoft/engine/api/internal/servlet/EngineInitializerListener.java b/bpm/bonita-api/bonita-server-api-http/src/main/java/org/bonitasoft/engine/api/internal/servlet/EngineInitializerListener.java index 13742c3e61a..5d207c95d25 100644 --- a/bpm/bonita-api/bonita-server-api-http/src/main/java/org/bonitasoft/engine/api/internal/servlet/EngineInitializerListener.java +++ b/bpm/bonita-api/bonita-server-api-http/src/main/java/org/bonitasoft/engine/api/internal/servlet/EngineInitializerListener.java @@ -13,11 +13,13 @@ **/ package org.bonitasoft.engine.api.internal.servlet; +import javax.naming.NamingException; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import org.bonitasoft.engine.EngineInitializer; import org.bonitasoft.engine.service.impl.ServiceAccessorFactory; +import org.bonitasoft.platform.setup.PlatformSetup; import org.bonitasoft.platform.setup.PlatformSetupAccessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,7 +58,7 @@ public void contextInitialized(final ServletContextEvent event) { AnnotationConfigWebApplicationContext initializeWebApplicationContext(ServletContextEvent event, EngineInitializer engineInitializer) throws Exception { - PlatformSetupAccessor.getPlatformSetup().init(); // init tables and default configuration + getPlatformSetup().init(); // init tables and default configuration engineInitializer.initializeEngine(); ApplicationContext engineContext = ServiceAccessorFactory.getInstance() .createServiceAccessor() @@ -67,6 +69,10 @@ AnnotationConfigWebApplicationContext initializeWebApplicationContext(ServletCon return webApplicationContext; } + protected PlatformSetup getPlatformSetup() throws NamingException { + return PlatformSetupAccessor.getInstance().getPlatformSetup(); + } + void exit(int code) { System.exit(code); } diff --git a/bpm/bonita-common/src/main/java/org/bonitasoft/engine/api/ApplicationAPI.java b/bpm/bonita-common/src/main/java/org/bonitasoft/engine/api/ApplicationAPI.java index 1c307fb7fc9..4dac4892cd5 100755 --- a/bpm/bonita-common/src/main/java/org/bonitasoft/engine/api/ApplicationAPI.java +++ b/bpm/bonita-common/src/main/java/org/bonitasoft/engine/api/ApplicationAPI.java @@ -100,7 +100,7 @@ default Application getApplication(final long applicationId) throws ApplicationN } else { throw new ApplicationNotFoundException(applicationId); } - }; + } /** * Retrieves an {@link IApplication} from its identifier. diff --git a/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/execution/ProcessExecutorImpl.java b/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/execution/ProcessExecutorImpl.java index fa3fa91ed58..982e11ce9d6 100644 --- a/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/execution/ProcessExecutorImpl.java +++ b/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/execution/ProcessExecutorImpl.java @@ -16,8 +16,14 @@ import static org.bonitasoft.engine.classloader.ClassLoaderIdentifier.identifier; import java.io.Serializable; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import lombok.extern.slf4j.Slf4j; import org.bonitasoft.engine.SArchivingException; @@ -56,15 +62,38 @@ import org.bonitasoft.engine.core.process.definition.ProcessDefinitionService; import org.bonitasoft.engine.core.process.definition.exception.SProcessDefinitionException; import org.bonitasoft.engine.core.process.definition.exception.SProcessDefinitionNotFoundException; -import org.bonitasoft.engine.core.process.definition.model.*; +import org.bonitasoft.engine.core.process.definition.model.SBusinessDataDefinition; +import org.bonitasoft.engine.core.process.definition.model.SConnectorDefinition; +import org.bonitasoft.engine.core.process.definition.model.SContractDefinition; +import org.bonitasoft.engine.core.process.definition.model.SDocumentDefinition; +import org.bonitasoft.engine.core.process.definition.model.SDocumentListDefinition; +import org.bonitasoft.engine.core.process.definition.model.SFlowElementContainerDefinition; +import org.bonitasoft.engine.core.process.definition.model.SFlowNodeDefinition; +import org.bonitasoft.engine.core.process.definition.model.SFlowNodeType; +import org.bonitasoft.engine.core.process.definition.model.SGatewayDefinition; +import org.bonitasoft.engine.core.process.definition.model.SGatewayType; +import org.bonitasoft.engine.core.process.definition.model.SProcessDefinition; +import org.bonitasoft.engine.core.process.definition.model.SProcessDefinitionDeployInfo; +import org.bonitasoft.engine.core.process.definition.model.STransitionDefinition; import org.bonitasoft.engine.core.process.definition.model.event.SEndEventDefinition; import org.bonitasoft.engine.core.process.instance.api.ActivityInstanceService; import org.bonitasoft.engine.core.process.instance.api.GatewayInstanceService; import org.bonitasoft.engine.core.process.instance.api.ProcessInstanceService; import org.bonitasoft.engine.core.process.instance.api.RefBusinessDataService; -import org.bonitasoft.engine.core.process.instance.api.exceptions.*; +import org.bonitasoft.engine.core.process.instance.api.exceptions.SActivityInstanceNotFoundException; +import org.bonitasoft.engine.core.process.instance.api.exceptions.SActivityReadException; +import org.bonitasoft.engine.core.process.instance.api.exceptions.SContractViolationException; +import org.bonitasoft.engine.core.process.instance.api.exceptions.SFlowNodeExecutionException; +import org.bonitasoft.engine.core.process.instance.api.exceptions.SProcessInstanceCreationException; +import org.bonitasoft.engine.core.process.instance.api.exceptions.SProcessInstanceModificationException; import org.bonitasoft.engine.core.process.instance.api.states.FlowNodeState; -import org.bonitasoft.engine.core.process.instance.model.*; +import org.bonitasoft.engine.core.process.instance.model.SActivityInstance; +import org.bonitasoft.engine.core.process.instance.model.SConnectorInstance; +import org.bonitasoft.engine.core.process.instance.model.SFlowElementsContainerType; +import org.bonitasoft.engine.core.process.instance.model.SFlowNodeInstance; +import org.bonitasoft.engine.core.process.instance.model.SGatewayInstance; +import org.bonitasoft.engine.core.process.instance.model.SProcessInstance; +import org.bonitasoft.engine.core.process.instance.model.SStateCategory; import org.bonitasoft.engine.core.process.instance.model.builder.business.data.SRefBusinessDataInstanceBuilderFactory; import org.bonitasoft.engine.core.process.instance.model.business.data.SRefBusinessDataInstance; import org.bonitasoft.engine.core.process.instance.model.event.SThrowEventInstance; @@ -78,7 +107,6 @@ import org.bonitasoft.engine.execution.event.EventsHandler; import org.bonitasoft.engine.execution.flowmerger.FlowNodeTransitionsWrapper; import org.bonitasoft.engine.execution.handler.SProcessInstanceHandler; -import org.bonitasoft.engine.execution.state.FlowNodeStateManager; import org.bonitasoft.engine.execution.work.BPMWorkFactory; import org.bonitasoft.engine.expression.Expression; import org.bonitasoft.engine.expression.ExpressionService; @@ -120,7 +148,7 @@ public class ProcessExecutorImpl implements ProcessExecutor { private final ProcessDefinitionService processDefinitionService; private final GatewayInstanceService gatewayInstanceService; private final OperationService operationService; - private ProcessResourcesService processResourcesService; + private final ProcessResourcesService processResourcesService; private final ConnectorInstanceService connectorInstanceService; private final TransitionEvaluator transitionEvaluator; private final ContractDataService contractDataService; @@ -129,6 +157,7 @@ public class ProcessExecutorImpl implements ProcessExecutor { private final DocumentHelper documentHelper; private final BPMWorkFactory workFactory; private final BPMArchiverService bpmArchiverService; + private final ProcessStarterVerifier processStarterVerifier; public ProcessExecutorImpl(final ActivityInstanceService activityInstanceService, final ProcessInstanceService processInstanceService, final FlowNodeExecutor flowNodeExecutor, @@ -142,11 +171,11 @@ public ProcessExecutorImpl(final ActivityInstanceService activityInstanceService final EventService eventService, final Map> handlers, final DocumentService documentService, final ContainerRegistry containerRegistry, final BPMInstancesCreator bpmInstancesCreator, - final EventsHandler eventsHandler, final FlowNodeStateManager flowNodeStateManager, + final EventsHandler eventsHandler, final BusinessDataRepository businessDataRepository, final RefBusinessDataService refBusinessDataService, final TransitionEvaluator transitionEvaluator, final ContractDataService contractDataService, BPMWorkFactory workFactory, - BPMArchiverService bpmArchiverService) { + BPMArchiverService bpmArchiverService, final ProcessStarterVerifier processStarterVerifier) { super(); this.activityInstanceService = activityInstanceService; this.processInstanceService = processInstanceService; @@ -169,6 +198,7 @@ public ProcessExecutorImpl(final ActivityInstanceService activityInstanceService this.contractDataService = contractDataService; this.workFactory = workFactory; this.bpmArchiverService = bpmArchiverService; + this.processStarterVerifier = processStarterVerifier; documentHelper = new DocumentHelper(documentService, processDefinitionService, processInstanceService); //FIXME There is responsibility issue the circular dependencies must be fixed next time. eventsHandler.setProcessExecutor(this); @@ -914,8 +944,7 @@ protected SProcessInstance start(final long starterId, final long starterSubstit final List operations, final Map context, final List connectors, final long callerId, final FlowNodeSelector selector, final Map processInputs) - throws SProcessInstanceCreationException, - SContractViolationException { + throws SProcessInstanceCreationException, SContractViolationException { final SProcessDefinition sProcessDefinition = selector.getProcessDefinition(); @@ -945,7 +974,9 @@ protected SProcessInstance start(final long starterId, final long starterSubstit // we stop execution here return sProcessInstance; } - return startElements(sProcessInstance, selector); + final SProcessInstance processInstance = startElements(sProcessInstance, selector); + processStarterVerifier.verify(processInstance); + return processInstance; } catch (final SProcessInstanceCreationException e) { throw e; } catch (final SBonitaException e) { diff --git a/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/execution/ProcessStarterVerifier.java b/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/execution/ProcessStarterVerifier.java new file mode 100644 index 00000000000..814af4fa309 --- /dev/null +++ b/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/execution/ProcessStarterVerifier.java @@ -0,0 +1,31 @@ +/** + * Copyright (C) 2024 Bonitasoft S.A. + * Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble + * This library is free software; you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Foundation + * version 2.1 of the License. + * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public License along with this + * program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth + * Floor, Boston, MA 02110-1301, USA. + **/ +package org.bonitasoft.engine.execution; + +import org.bonitasoft.engine.core.process.instance.api.exceptions.SProcessInstanceCreationException; +import org.bonitasoft.engine.core.process.instance.model.SProcessInstance; + +/** + * Define rules to be executed before the start of a process, right after its creation. + */ +public interface ProcessStarterVerifier { + + /** + * Verify that a process is ready to be started right after its creation. + * + * @param processInstance the process instance that is going to be started + * @throws SProcessInstanceCreationException if the process is not in a valid state to start + */ + void verify(SProcessInstance processInstance) throws SProcessInstanceCreationException; +} diff --git a/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/execution/ProcessStarterVerifierImpl.java b/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/execution/ProcessStarterVerifierImpl.java new file mode 100644 index 00000000000..6d56d989a51 --- /dev/null +++ b/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/execution/ProcessStarterVerifierImpl.java @@ -0,0 +1,179 @@ +/** + * Copyright (C) 2024 Bonitasoft S.A. + * Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble + * This library is free software; you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Foundation + * version 2.1 of the License. + * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public License along with this + * program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth + * Floor, Boston, MA 02110-1301, USA. + **/ +package org.bonitasoft.engine.execution; + +import static java.lang.String.format; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.bonitasoft.engine.core.process.instance.api.exceptions.SProcessInstanceCreationException; +import org.bonitasoft.engine.core.process.instance.model.SProcessInstance; +import org.bonitasoft.engine.platform.PlatformRetriever; +import org.bonitasoft.engine.platform.exception.SPlatformNotFoundException; +import org.bonitasoft.engine.platform.exception.SPlatformUpdateException; +import org.bonitasoft.engine.service.platform.PlatformInformationService; +import org.bonitasoft.engine.transaction.TransactionService; +import org.bonitasoft.platform.setup.SimpleEncryptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.stereotype.Component; + +@Component +@ConditionalOnSingleCandidate(ProcessStarterVerifier.class) +@Slf4j +public class ProcessStarterVerifierImpl implements ProcessStarterVerifier { + + public static final int LIMIT = 150; + protected static final int PERIOD_IN_DAYS = 30; + protected static final long PERIOD_IN_MILLIS = PERIOD_IN_DAYS * 24L * 60L * 60L * 1000L; + protected static final List THRESHOLDS_IN_PERCENT = List.of(80, 90); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private final PlatformRetriever platformRetriever; + private final PlatformInformationService platformInformationService; + + private final List counters = Collections.synchronizedList(new ArrayList<>()); + + ProcessStarterVerifierImpl(PlatformRetriever platformRetriever, + PlatformInformationService platformInformationService) { + this.platformRetriever = platformRetriever; + this.platformInformationService = platformInformationService; + } + + @Autowired + ProcessStarterVerifierImpl(PlatformRetriever platformRetriever, + PlatformInformationService platformInformationService, + TransactionService transactionService) throws Exception { + this(platformRetriever, platformInformationService); + counters.addAll(transactionService.executeInTransaction(this::readCounters)); + } + + protected List getCounters() { + return Collections.unmodifiableList(counters); + } + + protected void addCounter(long counter) { + synchronized (counters) { + counters.add(counter); + } + } + + @Override + public void verify(SProcessInstance processInstance) throws SProcessInstanceCreationException { + log.debug("Verifying process instance {}", processInstance.getId()); + cleanupOldValues(); + log.debug("Found {} cases already started in the last {} days", counters.size(), PERIOD_IN_DAYS); + if (counters.size() >= LIMIT) { + final String nextValidTime = getStringRepresentation(getNextResetTimestamp(counters)); + throw new SProcessInstanceCreationException( + format("Process start limit (%s cases during last %s days) reached. You are not allowed to start a new process until %s.", + LIMIT, PERIOD_IN_DAYS, nextValidTime)); + } + try { + synchronized (counters) { + counters.add(System.currentTimeMillis()); + } + final String information = encryptDataBeforeSendingToDatabase(counters); + // store in database: + storeNewValueInDatabase(information); + logCaseLimitProgressIfThresholdReached(); + } catch (IOException | SPlatformNotFoundException | SPlatformUpdateException e) { + log.trace(e.getMessage(), e); + throw new SProcessInstanceCreationException( + format("Unable to start the process instance %s", processInstance.getId())); + } + } + + void cleanupOldValues() { + log.trace("Cleaning up old values for the last {} days", PERIOD_IN_DAYS); + final long oldestValidTimestamp = System.currentTimeMillis() - PERIOD_IN_MILLIS; + synchronized (counters) { + counters.removeIf(timestamp -> timestamp < oldestValidTimestamp); + } + } + + void storeNewValueInDatabase(String information) throws SPlatformUpdateException, SPlatformNotFoundException { + platformInformationService.updatePlatformInfo(platformRetriever.getPlatform(), information); + } + + List readCounters() { + try { + String information = platformRetriever.getPlatform().getInformation(); + if (information == null || information.isBlank()) { + throw new IllegalStateException("Invalid database. Please reset it and restart."); + } + return decryptDataFromDatabase(information); + } catch (SPlatformNotFoundException | IOException e) { + throw new IllegalStateException("Cannot read from database table 'platform'", e); + } + } + + String encryptDataBeforeSendingToDatabase(List counters) throws IOException { + return encrypt(OBJECT_MAPPER.writeValueAsBytes(counters)); + } + + List decryptDataFromDatabase(String information) throws IOException { + return OBJECT_MAPPER.readValue(decrypt(information), new TypeReference<>() { + }); + } + + private static String encrypt(byte[] data) { + try { + return SimpleEncryptor.encrypt(data); + } catch (GeneralSecurityException e) { + throw new IllegalStateException("Cannot cipher information", e); + } + } + + private static byte[] decrypt(String information) { + try { + return SimpleEncryptor.decrypt(information); + } catch (GeneralSecurityException e) { + throw new IllegalStateException("Cannot decipher information", e); + } + } + + void logCaseLimitProgressIfThresholdReached() { + var percentBeforeThisNewCase = (float) ((getCounters().size() - 1) * 100) / LIMIT; + var percentWithThisNewCase = (float) ((getCounters().size()) * 100) / LIMIT; + for (Integer threshold : THRESHOLDS_IN_PERCENT) { + if (percentBeforeThisNewCase < threshold && percentWithThisNewCase >= threshold) { + log.warn("You have started {}% of your allowed cases." + + "If you need more volume, please consider subscribing to an Enterprise edition.", + threshold); + } + } + } + + /** + * Returns a timestamp to a human-readable format + */ + private String getStringRepresentation(long timestamp) { + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(timestamp)); + } + + private long getNextResetTimestamp(List timestamps) { + return Collections.min(timestamps) + PERIOD_IN_MILLIS; + } + +} diff --git a/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/service/platform/PlatformInformationService.java b/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/service/platform/PlatformInformationService.java new file mode 100644 index 00000000000..5b0113c0ac8 --- /dev/null +++ b/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/service/platform/PlatformInformationService.java @@ -0,0 +1,34 @@ +/** + * Copyright (C) 2019 Bonitasoft S.A. + * Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble + * This library is free software; you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Foundation + * version 2.1 of the License. + * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public License along with this + * program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth + * Floor, Boston, MA 02110-1301, USA. + **/ +package org.bonitasoft.engine.service.platform; + +import org.bonitasoft.engine.platform.exception.SPlatformUpdateException; +import org.bonitasoft.engine.platform.model.SPlatform; + +/** + * @author Elias Ricken de Medeiros + */ +public interface PlatformInformationService { + + /** + * Updates the platform information + * + * @param platform the platform to be updated + * @param platformInfo the new platform information + * @throws SPlatformUpdateException + * @since 7.1.0 + */ + void updatePlatformInfo(SPlatform platform, String platformInfo) throws SPlatformUpdateException; + +} diff --git a/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/service/platform/PlatformInformationServiceImpl.java b/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/service/platform/PlatformInformationServiceImpl.java new file mode 100644 index 00000000000..10fcc891cf1 --- /dev/null +++ b/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/service/platform/PlatformInformationServiceImpl.java @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2019 Bonitasoft S.A. + * Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble + * This library is free software; you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Foundation + * version 2.1 of the License. + * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public License along with this + * program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth + * Floor, Boston, MA 02110-1301, USA. + **/ +package org.bonitasoft.engine.service.platform; + +import org.bonitasoft.engine.platform.exception.SPlatformUpdateException; +import org.bonitasoft.engine.platform.model.SPlatform; +import org.bonitasoft.engine.services.PersistenceService; +import org.bonitasoft.engine.services.SPersistenceException; +import org.bonitasoft.engine.services.UpdateDescriptor; +import org.springframework.stereotype.Service; + +/** + * Updates the platform information using directly the {@link PersistenceService} + * + * @author Elias Ricken de Medeiros + */ +@Service("platformInformationService") +public class PlatformInformationServiceImpl implements PlatformInformationService { + + private final PersistenceService persistenceService; + + public PlatformInformationServiceImpl(final PersistenceService persistenceService) { + this.persistenceService = persistenceService; + } + + @Override + public void updatePlatformInfo(final SPlatform platform, final String platformInfo) + throws SPlatformUpdateException { + + UpdateDescriptor desc = new UpdateDescriptor(platform); + desc.addField(SPlatform.INFORMATION, platformInfo); + + try { + persistenceService.update(desc); + } catch (SPersistenceException e) { + throw new SPlatformUpdateException(e); + } + } + +} diff --git a/bpm/bonita-core/bonita-process-engine/src/main/resources/bonita-community.xml b/bpm/bonita-core/bonita-process-engine/src/main/resources/bonita-community.xml index 1517e211ddf..2bd72f8ffef 100644 --- a/bpm/bonita-core/bonita-process-engine/src/main/resources/bonita-community.xml +++ b/bpm/bonita-core/bonita-process-engine/src/main/resources/bonita-community.xml @@ -1226,12 +1226,12 @@ - + processInputs = new HashMap<>(0); @@ -334,8 +338,8 @@ public void startProcessWithOperationsAndContextAndExpressionContextAndConnector doReturn(sProcessDefinitionDeployInfo).when(processDefinitionService).getProcessDeploymentInfo(anyLong()); when(mockedProcessExecutorImpl.startElements(eq(sProcessInstance), eq(selector))).thenReturn(sProcessInstance); when(mockedProcessExecutorImpl.createProcessInstance(sProcessDefinition, starterId, starterSubstituteId, 1L)) - .thenReturn( - sProcessInstance); + .thenReturn(sProcessInstance); + doNothing().when(processStarterVerifier).verify(sProcessInstance); final Map processInputs = new HashMap<>(0); doNothing().when(mockedProcessExecutorImpl).validateContractInputs(processInputs, sProcessDefinition); diff --git a/bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/execution/ProcessStarterVerifierTest.java b/bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/execution/ProcessStarterVerifierTest.java new file mode 100644 index 00000000000..27f95430ffa --- /dev/null +++ b/bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/execution/ProcessStarterVerifierTest.java @@ -0,0 +1,199 @@ +/** + * Copyright (C) 2024 Bonitasoft S.A. + * Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble + * This library is free software; you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Foundation + * version 2.1 of the License. + * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public License along with this + * program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth + * Floor, Boston, MA 02110-1301, USA. + **/ +package org.bonitasoft.engine.execution; + +import static com.github.stefanbirkner.systemlambda.SystemLambda.tapSystemOut; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.bonitasoft.engine.execution.ProcessStarterVerifierImpl.LIMIT; +import static org.bonitasoft.engine.execution.ProcessStarterVerifierImpl.PERIOD_IN_MILLIS; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.List; + +import org.bonitasoft.engine.core.process.instance.api.exceptions.SProcessInstanceCreationException; +import org.bonitasoft.engine.core.process.instance.model.SProcessInstance; +import org.bonitasoft.engine.platform.PlatformRetriever; +import org.bonitasoft.engine.platform.model.SPlatform; +import org.bonitasoft.engine.service.platform.PlatformInformationService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ProcessStarterVerifierTest { + + @Mock + private PlatformRetriever platformRetriever; + + @Mock + private PlatformInformationService platformInformationService; + + ProcessStarterVerifierImpl processStarterVerifier; + + @BeforeEach + void setUp() { + processStarterVerifier = Mockito + .spy(new ProcessStarterVerifierImpl(platformRetriever, platformInformationService)); + } + + @Test + void should_be_able_to_decrypt_encrypted_value() throws Exception { + //given + final long currentTime = System.currentTimeMillis(); + final var originalValue = List.of(currentTime, currentTime + 1000L, currentTime + 2000L); + + //when + final List computedValue = processStarterVerifier + .decryptDataFromDatabase(processStarterVerifier.encryptDataBeforeSendingToDatabase(originalValue)); + + //then + assertThat(computedValue).isEqualTo(originalValue); + } + + @Test + void verify_should_not_remove_still_valid_values_from_counters() throws Exception { + //given + final long validTimestamp = System.currentTimeMillis() - PERIOD_IN_MILLIS + 86400000L; // plus 1 day + processStarterVerifier.addCounter(validTimestamp); + doNothing().when(processStarterVerifier).storeNewValueInDatabase(anyString()); + + //when + processStarterVerifier.verify(new SProcessInstance()); + + //then + assertThat(processStarterVerifier.getCounters()).size().isEqualTo(2); + assertThat(processStarterVerifier.getCounters()).contains(validTimestamp); + } + + @Test + void verify_should_remove_old_values_from_counters() throws Exception { + //given + final long obsoleteValue = System.currentTimeMillis() - PERIOD_IN_MILLIS - 86400000L; // minus 1 day + processStarterVerifier.addCounter(obsoleteValue); + doNothing().when(processStarterVerifier).storeNewValueInDatabase(anyString()); + + //when + processStarterVerifier.verify(new SProcessInstance()); + + //then + assertThat(processStarterVerifier.getCounters()).size().isEqualTo(1); + assertThat(processStarterVerifier.getCounters()).doesNotContain(obsoleteValue); + } + + @Test + void verify_should_throw_exception_if_limit_is_reached() throws Exception { + //given + for (int i = 0; i < LIMIT; i++) { + processStarterVerifier.addCounter(System.currentTimeMillis()); + } + + //when - then + assertThatExceptionOfType(SProcessInstanceCreationException.class) + .isThrownBy(() -> processStarterVerifier.verify(new SProcessInstance())) + .withMessageContaining("Process start limit"); + assertThat(processStarterVerifier.getCounters()).size().isEqualTo(LIMIT); + verify(processStarterVerifier, never()).storeNewValueInDatabase(anyString()); + } + + @Test + void should_log_when_80_percent_is_reached() throws Exception { + var counters = mock(List.class); + doReturn(LIMIT * 80 / 100).when(counters).size(); + doReturn(counters).when(processStarterVerifier).getCounters(); + + // when + final String log = tapSystemOut(() -> processStarterVerifier.logCaseLimitProgressIfThresholdReached()); + + // then + assertThat(log).containsPattern("WARN.*80%"); + } + + @Test + void should_log_when_90_percent_is_reached() throws Exception { + var counters = mock(List.class); + doReturn(LIMIT * 90 / 100).when(counters).size(); + doReturn(counters).when(processStarterVerifier).getCounters(); + + // when + final String log = tapSystemOut(() -> processStarterVerifier.logCaseLimitProgressIfThresholdReached()); + + // then + assertThat(log) + .containsPattern("WARN.*90%") + .doesNotContain("80%"); + } + + @Test + void should_not_log_when_80_percent_has_already_been_reached() throws Exception { + var counters = mock(List.class); + doReturn(121).when(counters).size(); + doReturn(counters).when(processStarterVerifier).getCounters(); + + // when + final String log = tapSystemOut(() -> processStarterVerifier.logCaseLimitProgressIfThresholdReached()); + + // then + assertThat(log).isBlank(); + } + + @Test + void readCounters_should_fail_if_counters_are_not_set() throws Exception { + // given + final SPlatform platform = new SPlatform(); + platform.setInformation(""); + doReturn(platform).when(platformRetriever).getPlatform(); + + // when - then + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> processStarterVerifier.readCounters()); + } + + @Test + void readCounters_should_fail_if_counters_cannot_be_decrypted_from_database() throws Exception { + // given + final SPlatform platform = new SPlatform(); + final String encryptedValue = "encrypted value"; + platform.setInformation(encryptedValue); + doReturn(platform).when(platformRetriever).getPlatform(); + doThrow(new IOException("Cannot decipher information")).when(processStarterVerifier) + .decryptDataFromDatabase(encryptedValue); + + // when - then + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> processStarterVerifier.readCounters()); + } + + @Test + void readCounters_should_succeed_if_counters_can_be_decrypted_from_database() throws Exception { + // given + final SPlatform platform = new SPlatform(); + final String encryptedValue = "encrypted value"; + platform.setInformation(encryptedValue); + doReturn(platform).when(platformRetriever).getPlatform(); + final List countersFromDatabase = List.of(System.currentTimeMillis()); + doReturn(countersFromDatabase).when(processStarterVerifier).decryptDataFromDatabase(encryptedValue); + + // when + final List counters = processStarterVerifier.readCounters(); + + // then + assertThat(counters).isEqualTo(countersFromDatabase); + } +} diff --git a/bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/service/platform/PlatformInformationServiceImplTest.java b/bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/service/platform/PlatformInformationServiceImplTest.java new file mode 100644 index 00000000000..e54a68d7618 --- /dev/null +++ b/bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/service/platform/PlatformInformationServiceImplTest.java @@ -0,0 +1,77 @@ +/** + * Copyright (C) 2019 Bonitasoft S.A. + * Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble + * This library is free software; you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Foundation + * version 2.1 of the License. + * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public License along with this + * program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth + * Floor, Boston, MA 02110-1301, USA. + **/ +package org.bonitasoft.engine.service.platform; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import org.bonitasoft.engine.platform.exception.SPlatformUpdateException; +import org.bonitasoft.engine.platform.model.SPlatform; +import org.bonitasoft.engine.services.PersistenceService; +import org.bonitasoft.engine.services.SPersistenceException; +import org.bonitasoft.engine.services.UpdateDescriptor; +import org.hamcrest.core.IsEqual; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class PlatformInformationServiceImplTest { + + @Mock + private PersistenceService persistenceService; + + @InjectMocks + private PlatformInformationServiceImpl platformInformationService; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void updatePlatformInfo() throws Exception { + //given + SPlatform platform = mock(SPlatform.class); + + //when + platformInformationService.updatePlatformInfo(platform, "info"); + + //then + ArgumentCaptor upDescCaptor = ArgumentCaptor.forClass(UpdateDescriptor.class); + verify(persistenceService).update(upDescCaptor.capture()); + UpdateDescriptor descriptor = upDescCaptor.getValue(); + assertThat(descriptor.getFields()).containsEntry(SPlatform.INFORMATION, "info"); + assertThat(descriptor.getEntity()).isEqualTo(platform); + } + + @Test + public void updatePlatformInfo_should_throw_exception_when_persistence_service_throws_exception() throws Exception { + //given + SPlatform platform = mock(SPlatform.class); + SPersistenceException toBeThrown = new SPersistenceException("exception"); + doThrow(toBeThrown).when(persistenceService).update(any(UpdateDescriptor.class)); + + //then + expectedException.expect(SPlatformUpdateException.class); + expectedException.expectCause(new IsEqual(toBeThrown)); + + //when + platformInformationService.updatePlatformInfo(platform, "info"); + } +} diff --git a/bpm/bonita-synchro-repository/bonita-synchro-register/build.gradle b/bpm/bonita-synchro-repository/bonita-synchro-register/build.gradle index 9807d2e4ad4..8a990f631b4 100644 --- a/bpm/bonita-synchro-repository/bonita-synchro-register/build.gradle +++ b/bpm/bonita-synchro-repository/bonita-synchro-register/build.gradle @@ -4,6 +4,8 @@ dependencies { api project(':bpm:bonita-core:bonita-process-engine') api project(':bpm:bonita-synchro-repository:bonita-synchro-service-impl') api project(':services:bonita-commons') + annotationProcessor(libs.lombok) + compileOnly(libs.lombok) compileOnly 'jakarta.jms:jakarta.jms-api:3.0.0' compileOnly 'org.apache.activemq:activemq-client:5.16.6' } diff --git a/bpm/bonita-synchro-repository/bonita-synchro-service-impl/src/main/java/org/bonitasoft/engine/synchro/AbstractSynchroService.java b/bpm/bonita-synchro-repository/bonita-synchro-service-impl/src/main/java/org/bonitasoft/engine/synchro/AbstractSynchroService.java index 1ce41ef9dfd..467b2f8d478 100644 --- a/bpm/bonita-synchro-repository/bonita-synchro-service-impl/src/main/java/org/bonitasoft/engine/synchro/AbstractSynchroService.java +++ b/bpm/bonita-synchro-repository/bonita-synchro-service-impl/src/main/java/org/bonitasoft/engine/synchro/AbstractSynchroService.java @@ -111,8 +111,6 @@ protected Serializable getFiredAndRemoveIt(final Map expec List firedEvents = cacheService.getKeys(SYNCHRO_SERVICE_CACHE); for (Map firedEvent : (List>) firedEvents) { if (matchedAtLeastAllExpectedEntries(expectedEvent, firedEvent)) { - // Serializable id = (Serializable) cacheService.get(SYNCHRO_SERVICE_CACHE, firedEvent); - // System.out.println("id=" + id); cacheService.remove(SYNCHRO_SERVICE_CACHE, firedEvent); return firedEvent.get("id"); } diff --git a/bpm/bonita-synchro-repository/bonita-synchro-service-impl/src/main/java/org/bonitasoft/engine/synchro/SynchroServiceImpl.java b/bpm/bonita-synchro-repository/bonita-synchro-service-impl/src/main/java/org/bonitasoft/engine/synchro/SynchroServiceImpl.java index da4ebece7b1..630ae42019e 100644 --- a/bpm/bonita-synchro-repository/bonita-synchro-service-impl/src/main/java/org/bonitasoft/engine/synchro/SynchroServiceImpl.java +++ b/bpm/bonita-synchro-repository/bonita-synchro-service-impl/src/main/java/org/bonitasoft/engine/synchro/SynchroServiceImpl.java @@ -34,7 +34,7 @@ */ public class SynchroServiceImpl extends AbstractSynchroService { - private Logger logger = LoggerFactory.getLogger(SynchroServiceImpl.class); + private final Logger logger = LoggerFactory.getLogger(SynchroServiceImpl.class); private final Map, String> waiters; private final Map eventKeyAndIdMap; @@ -49,9 +49,9 @@ public class SynchroServiceImpl extends AbstractSynchroService { */ private SynchroServiceImpl(final int initialCapacity, final CacheService cacheService) { super(cacheService); - waiters = new HashMap, String>(initialCapacity); - eventKeyAndIdMap = new HashMap(initialCapacity); - eventSemaphores = new HashMap(); + waiters = new HashMap<>(initialCapacity); + eventKeyAndIdMap = new HashMap<>(initialCapacity); + eventSemaphores = new HashMap<>(); } private static final class SynchroServiceImplReentrantLock extends ReentrantLock { @@ -89,7 +89,7 @@ protected void releaseWaiter(final String semaphoreKey) { @Override public Serializable waitForEvent(final Map event, final long timeout) throws InterruptedException, TimeoutException { - Serializable id = null; + Serializable id; String semaphoreKey = null; Semaphore semaphore = null; getServiceLock().lock(); @@ -122,7 +122,7 @@ public Serializable waitForEvent(final Map event, final lo private String getSemaphoreKey(final Map event) { final StringBuilder sb = new StringBuilder(); - final TreeMap orderedMap = new TreeMap(event); + final TreeMap orderedMap = new TreeMap<>(event); boolean first = true; for (final Map.Entry entry : orderedMap.entrySet()) { if (!first) { diff --git a/platform/platform-resources/build.gradle b/platform/platform-resources/build.gradle index 9b7d1a4e0e4..1884c9a71eb 100644 --- a/platform/platform-resources/build.gradle +++ b/platform/platform-resources/build.gradle @@ -8,6 +8,8 @@ group = 'org.bonitasoft.platform' description = '' dependencies { + implementation platform(libs.jacksonBom) + annotationProcessor libs.lombok compileOnly libs.lombok api libs.springTx @@ -15,6 +17,8 @@ dependencies { api libs.springContext api libs.slf4jApi api libs.commonsIO + api libs.springBootAutoconfigure + implementation "com.fasterxml.jackson.core:jackson-databind" testImplementation "junit:junit:${Deps.junit4Version}" testImplementation "org.assertj:assertj-core:${Deps.assertjVersion}" diff --git a/platform/platform-resources/src/main/java/org/bonitasoft/platform/setup/PlatformSetupAccessor.java b/platform/platform-resources/src/main/java/org/bonitasoft/platform/setup/PlatformSetupAccessor.java index 06270955e21..eb8724f7358 100644 --- a/platform/platform-resources/src/main/java/org/bonitasoft/platform/setup/PlatformSetupAccessor.java +++ b/platform/platform-resources/src/main/java/org/bonitasoft/platform/setup/PlatformSetupAccessor.java @@ -33,16 +33,26 @@ */ public class PlatformSetupAccessor { - private static PlatformSetup _INSTANCE; + private static final PlatformSetupAccessor _UNIQUE = new PlatformSetupAccessor(); - public static PlatformSetup getPlatformSetup() throws NamingException { - if (_INSTANCE == null) { - _INSTANCE = createPlatformSetup(); + private PlatformSetup instance; + + protected PlatformSetupAccessor() { + // Empty protected constructor to prevent instantiation + } + + public static PlatformSetupAccessor getInstance() { + return _UNIQUE; + } + + public PlatformSetup getPlatformSetup() throws NamingException { + if (instance == null) { + instance = createPlatformSetup(); } - return _INSTANCE; + return instance; } - private static PlatformSetup createPlatformSetup() throws NamingException { + private PlatformSetup createPlatformSetup() throws NamingException { final DataSource dataSource = lookupDataSource(); String dbVendor = System.getProperty("sysprop.bonita.db.vendor"); return createNewPlatformSetup(dataSource, dbVendor); @@ -55,15 +65,20 @@ private static PlatformSetup createPlatformSetup() throws NamingException { * @param dbVendor the Database vendor (default H2) to point at * @return a NEW instance of Platform Setup */ - public static PlatformSetup createNewPlatformSetup(DataSource dataSource, String dbVendor) { + public PlatformSetup createNewPlatformSetup(DataSource dataSource, String dbVendor) { JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); final DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(dataSource); TransactionTemplate transactionTemplate = new TransactionTemplate(dataSourceTransactionManager); VersionService versionService = new VersionServiceImpl(jdbcTemplate); - return new PlatformSetup(new ScriptExecutor(dbVendor, dataSource, versionService), + return new PlatformSetup(createScriptExecutor(dataSource, dbVendor, versionService), new ConfigurationServiceImpl(jdbcTemplate, transactionTemplate, dbVendor), versionService, dataSource); } + protected ScriptExecutor createScriptExecutor(DataSource dataSource, String dbVendor, + VersionService versionService) { + return new ScriptExecutor(dbVendor, dataSource, versionService); + } + private static DataSource lookupDataSource() throws NamingException { Context ctx = new InitialContext(); return (DataSource) ctx.lookup( diff --git a/platform/platform-resources/src/main/java/org/bonitasoft/platform/setup/ScriptExecutor.java b/platform/platform-resources/src/main/java/org/bonitasoft/platform/setup/ScriptExecutor.java index ff94c1f0c03..067648295e3 100644 --- a/platform/platform-resources/src/main/java/org/bonitasoft/platform/setup/ScriptExecutor.java +++ b/platform/platform-resources/src/main/java/org/bonitasoft/platform/setup/ScriptExecutor.java @@ -16,22 +16,28 @@ import static java.util.Arrays.asList; import static org.bonitasoft.platform.setup.PlatformSetup.BONITA_SETUP_FOLDER; import static org.bonitasoft.platform.setup.PlatformSetup.PLATFORM_CONF_FOLDER_NAME; +import static org.bonitasoft.platform.setup.SimpleEncryptor.encrypt; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.file.Path; import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.sql.DataSource; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.bonitasoft.platform.exception.PlatformException; import org.bonitasoft.platform.version.VersionService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; import org.springframework.context.annotation.PropertySource; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; @@ -46,6 +52,7 @@ */ @Slf4j @Component +@ConditionalOnSingleCandidate(ScriptExecutor.class) @PropertySource("classpath:/application.properties") public class ScriptExecutor { @@ -59,7 +66,7 @@ public class ScriptExecutor { private final String dbVendor; - private VersionService versionService; + private final VersionService versionService; @Autowired public ScriptExecutor(@Value("${db.vendor}") String dbVendor, DataSource datasource, @@ -69,7 +76,7 @@ public ScriptExecutor(@Value("${db.vendor}") String dbVendor, DataSource datasou } this.dbVendor = dbVendor; this.datasource = datasource; - log.info("configuration for Database vendor: " + dbVendor); + log.info("configuration for Database vendor: {}", dbVendor); this.sqlFolder = "/sql/" + dbVendor; this.versionService = versionService; } @@ -104,11 +111,21 @@ protected void insertPlatform() { String version = versionService.getPlatformSetupVersion(); String databaseSchemaVersion = versionService.getSupportedDatabaseSchemaVersion(); - final String sql = "INSERT INTO platform (id, version, initial_bonita_version, application_version, maintenance_message_active, created, created_by) " - + "VALUES (?, ?, ?, ?, ?, ?, ?)"; + final String sql = "INSERT INTO platform (id, version, initial_bonita_version, application_version, " + + "maintenance_message_active, created, created_by, information) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; new JdbcTemplate(datasource).update(sql, 1L, databaseSchemaVersion, version, "0.0.0", false, - System.currentTimeMillis(), "platformAdmin"); + System.currentTimeMillis(), "platformAdmin", getInformationInitialValue()); + } + + protected String getInformationInitialValue() { + try { + return encrypt(new ObjectMapper().writeValueAsBytes(new ArrayList())); + } catch (GeneralSecurityException | JsonProcessingException e) { + log.debug(e.getMessage(), e); + throw new IllegalStateException("Cannot properly setup Bonita platform"); + } } private void insertTenant() { diff --git a/platform/platform-resources/src/main/java/org/bonitasoft/platform/setup/SimpleEncryptor.java b/platform/platform-resources/src/main/java/org/bonitasoft/platform/setup/SimpleEncryptor.java new file mode 100644 index 00000000000..1a1e238ca40 --- /dev/null +++ b/platform/platform-resources/src/main/java/org/bonitasoft/platform/setup/SimpleEncryptor.java @@ -0,0 +1,65 @@ +/** + * Copyright (C) 2024 Bonitasoft S.A. + * Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble + * This library is free software; you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Foundation + * version 2.1 of the License. + * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public License along with this + * program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth + * Floor, Boston, MA 02110-1301, USA. + **/ +package org.bonitasoft.platform.setup; + +import java.security.GeneralSecurityException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.Base64; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * @author Emmanuel Duchastenier + */ +public final class SimpleEncryptor { + + private static final byte[] SALT = new byte[] { 0x47, 0x6f, 0x6f, 0x64, 0x4a, 0x6f, 0x62, 0x21 }; + private static final char[] PASSPHRASE = new String(new byte[] { 0x48, 0x34, 0x76, 0x33, 0x46, 0x75, 0x6e, + 0x57, 0x31, 0x37, 0x68, 0x38, 0x30, 0x6e, 0x31, 0x37, 0x34 }).toCharArray(); + + private static final SecretKey SECRET_KEY = SimpleEncryptor.generateKey(); + + private SimpleEncryptor() { + } + + public static SecretKey generateKey() { + try { + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + KeySpec spec = new PBEKeySpec(PASSPHRASE, SALT, 1000, 256); + return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES"); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new IllegalStateException(e); + } + } + + public static String encrypt(byte[] data) throws GeneralSecurityException { + Cipher cipher = Cipher.getInstance("AES"); + cipher.init(Cipher.ENCRYPT_MODE, SECRET_KEY); + byte[] encryptedBytes = cipher.doFinal(data); + return Base64.getEncoder().encodeToString(encryptedBytes); + } + + public static byte[] decrypt(String encryptedData) throws GeneralSecurityException { + Cipher cipher = Cipher.getInstance("AES"); + cipher.init(Cipher.DECRYPT_MODE, SECRET_KEY); + byte[] decodedBytes = Base64.getDecoder().decode(encryptedData); + return cipher.doFinal(decodedBytes); + } +} diff --git a/platform/platform-setup/src/main/java/org/bonitasoft/platform/setup/PlatformSetupApplication.java b/platform/platform-setup/src/main/java/org/bonitasoft/platform/setup/PlatformSetupApplication.java index 968d990dfe4..358676ee983 100644 --- a/platform/platform-setup/src/main/java/org/bonitasoft/platform/setup/PlatformSetupApplication.java +++ b/platform/platform-setup/src/main/java/org/bonitasoft/platform/setup/PlatformSetupApplication.java @@ -47,7 +47,7 @@ */ @SpringBootApplication @ComponentScan(basePackages = { "org.bonitasoft.platform.setup", "org.bonitasoft.platform.configuration", - "org.bonitasoft.platform.version" }) + "org.bonitasoft.platform.version", "com.bonitasoft.platform.setup" }) public class PlatformSetupApplication { // /!\ Leave this logger NON-STATIC, so that DEBUG property may be set before it is initialized: diff --git a/platform/platform-setup/src/test/java/org/bonitasoft/platform/setup/PlatformSetupIT.java b/platform/platform-setup/src/test/java/org/bonitasoft/platform/setup/PlatformSetupIT.java index c594e39df07..e7e10d53aaf 100644 --- a/platform/platform-setup/src/test/java/org/bonitasoft/platform/setup/PlatformSetupIT.java +++ b/platform/platform-setup/src/test/java/org/bonitasoft/platform/setup/PlatformSetupIT.java @@ -109,8 +109,9 @@ public void init_method_should_init_table_and_insert_conf() throws Exception { //then final Integer sequences = jdbcTemplate.queryForObject("select count(*) from sequence", Integer.class); assertThat(sequences).isGreaterThan(1); - final int platformRows = JdbcTestUtils.countRowsInTable(jdbcTemplate, "platform"); - assertThat(platformRows).isEqualTo(1); + final List platformRows = jdbcTemplate.queryForList("SELECT information FROM platform", String.class); + assertThat(platformRows).hasSize(1); + assertThat(platformRows.get(0)).isNotBlank(); // In Community, should contain the initial case counter value for information final int tenantRows = JdbcTestUtils.countRowsInTable(jdbcTemplate, "tenant"); assertThat(tenantRows).isEqualTo(1); final int configurationFiles = JdbcTestUtils.countRowsInTable(jdbcTemplate, "configuration"); diff --git a/services/bonita-platform/src/main/java/org/bonitasoft/engine/platform/impl/PlatformRetrieverImpl.java b/services/bonita-platform/src/main/java/org/bonitasoft/engine/platform/impl/PlatformRetrieverImpl.java index 2c978848a25..832cf617d9c 100644 --- a/services/bonita-platform/src/main/java/org/bonitasoft/engine/platform/impl/PlatformRetrieverImpl.java +++ b/services/bonita-platform/src/main/java/org/bonitasoft/engine/platform/impl/PlatformRetrieverImpl.java @@ -37,7 +37,7 @@ public PlatformRetrieverImpl(final PersistenceService platformPersistenceService public SPlatform getPlatform() throws SPlatformNotFoundException { try { SPlatform platform = platformPersistenceService - .selectOne(new SelectOneDescriptor(QUERY_GET_PLATFORM, null, SPlatform.class)); + .selectOne(new SelectOneDescriptor<>(QUERY_GET_PLATFORM, null, SPlatform.class)); if (platform == null) { throw new SPlatformNotFoundException("No platform found"); }