From 447aa9ccfad1e25a4524c34c9dbfd3e8aabd8089 Mon Sep 17 00:00:00 2001 From: Romain Bioteau Date: Fri, 6 Oct 2023 14:22:02 +0200 Subject: [PATCH] feat(installer): BDM comparison (#2747) Add a new service method `isDeployed(byte[] bdmArchive)` to compare a given bdm archive to install with the current deployed bdm. To perform the comparison, we uses the generated server jar. This jar contains the generated classes + the bom.xml file. Before generating the server jar, we set the productVersion in the bom.xml stored to match the current platform version. This way we ensure that the result of the generation + schema update is equivalent for a given xml model. BDM comparison is used at application install/update. If the bdm are equivalent when updating, the tenant is not paused and no bdm update is performed. This should limit the platform downtime (especially in cluster) when updating an SCA without updating the bonita version. To also ensure that the bdm server jar can be compared with another equivalent version, the utility method to create jar file in IOUtils now forces the JarEntry creation/lastUpdate date and time. To be able to generate a bdm server jar to do the comparison, a `disableRuntimeClassesValidation` flag has been added to the CodeGenerator. The flag is true by default. The application version and its semver implementation are now part of the ApplicationArchive. Closes DEV-552 --------- Co-authored-by: Bonita CI Co-authored-by: Adrien Kantcheff <5028967+akantcheff@users.noreply.github.com> --- .../installer/ApplicationInstallerIT.java | 11 +- .../ApplicationInstallerUpdateIT.java | 15 +- .../installer/ApplicationArchive.java | 47 +++++- .../installer/ApplicationInstaller.java | 106 ++++++------- .../CustomOrDefaultApplicationInstaller.java | 142 +++++------------- .../detector/ArtifactTypeDetector.java | 49 ++++-- .../MandatoryLivingApplicationImporter.java | 4 +- .../platform/PlatformVersionChecker.java | 2 +- .../src/main/resources/bonita-community.xml | 7 - .../bonita-tenant-community.properties | 2 +- .../installer/ApplicationArchiveTest.java | 72 +++++++++ .../installer/ApplicationInstallerTest.java | 31 +++- .../ApplicationInstallerUpdateTest.java | 32 ++-- ...stomOrDefaultApplicationInstallerTest.java | 90 +++-------- .../detector/ArtifactTypeDetectorTest.java | 56 +++++++ .../data/BusinessDataModelRepository.java | 14 +- .../generator/AbstractBDMCodeGenerator.java | 8 +- .../data/generator/CodeGenerator.java | 65 ++++++-- .../data/generator/EntityCodeGenerator.java | 33 ---- .../generator/server/ServerBDMJarBuilder.java | 10 +- .../generator/ForeignKeyAnnotatorTest.java | 2 +- .../server/ServerBDMJarBuilderITest.java | 13 +- .../server/ServerBDMJarBuilderTest.java | 2 +- .../bonita-business-data-impl/build.gradle | 3 + .../impl/BusinessDataModelRepositoryImpl.java | 87 +++++++++-- .../BusinessDataModelRepositoryImplTest.java | 72 ++++++++- .../business/data/impl/ConcurrencyTest.java | 8 +- .../JPABusinessDataRepositoryImplITest.java | 3 +- .../dependency/model/AbstractSDependency.java | 5 +- .../bonitasoft/engine/commons/io/IOUtil.java | 42 +++--- 30 files changed, 633 insertions(+), 400 deletions(-) create mode 100644 bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/api/impl/application/installer/ApplicationArchiveTest.java create mode 100644 bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/api/impl/application/installer/detector/ArtifactTypeDetectorTest.java diff --git a/bonita-integration-tests/bonita-integration-tests-local/src/test/java/org/bonitasoft/engine/application/installer/ApplicationInstallerIT.java b/bonita-integration-tests/bonita-integration-tests-local/src/test/java/org/bonitasoft/engine/application/installer/ApplicationInstallerIT.java index 688bd9d6b70..aa92909d0c9 100644 --- a/bonita-integration-tests/bonita-integration-tests-local/src/test/java/org/bonitasoft/engine/application/installer/ApplicationInstallerIT.java +++ b/bonita-integration-tests/bonita-integration-tests-local/src/test/java/org/bonitasoft/engine/application/installer/ApplicationInstallerIT.java @@ -17,7 +17,6 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import java.io.File; -import java.util.Optional; import org.bonitasoft.engine.CommonAPIIT; import org.bonitasoft.engine.api.impl.application.installer.ApplicationArchive; @@ -77,7 +76,7 @@ public void custom_application_should_be_deployed_entirely() throws Exception { // when: try (var applicationAsStream = ApplicationInstallerIT.class.getResourceAsStream("/customer-application.zip")) { var applicationArchive = applicationArchiveReader.read(applicationAsStream); - applicationInstallerImpl.install(applicationArchive, "1.0.0"); + applicationInstallerImpl.install(applicationArchive); } // then: @@ -120,9 +119,9 @@ public void custom_application_should_be_installed_with_configuration() throws E try (var applicationAsStream = ApplicationInstallerIT.class .getResourceAsStream("/simple-app-1.0.0-SNAPSHOT-local.zip")) { var applicationArchive = applicationArchiveReader.read(applicationAsStream); - applicationArchive.setConfigurationFile(Optional.of(new File(ApplicationInstallerIT.class - .getResource("/simple-app-1.0.0-SNAPSHOT-local.bconf").getFile()))); - applicationInstallerImpl.install(applicationArchive, "1.0.0-SNAPSHOT"); + applicationArchive.setConfigurationFile(new File(ApplicationInstallerIT.class + .getResource("/simple-app-1.0.0-SNAPSHOT-local.bconf").getFile())); + applicationInstallerImpl.install(applicationArchive); } final long processDefinitionId = getProcessAPI().getProcessDefinitionId("Pool", "1.0"); @@ -150,7 +149,7 @@ public void empty_custom_application_should_throw_an_exception() throws Exceptio // then: assertThatExceptionOfType(ApplicationInstallationException.class) - .isThrownBy(() -> applicationInstaller.install(applicationArchive, "1.0.0")) + .isThrownBy(() -> applicationInstaller.install(applicationArchive)) .withMessage("The Application Archive contains no valid artifact to install"); } } diff --git a/bonita-integration-tests/bonita-integration-tests-local/src/test/java/org/bonitasoft/engine/application/installer/ApplicationInstallerUpdateIT.java b/bonita-integration-tests/bonita-integration-tests-local/src/test/java/org/bonitasoft/engine/application/installer/ApplicationInstallerUpdateIT.java index f8bcd761467..3825f0be09d 100644 --- a/bonita-integration-tests/bonita-integration-tests-local/src/test/java/org/bonitasoft/engine/application/installer/ApplicationInstallerUpdateIT.java +++ b/bonita-integration-tests/bonita-integration-tests-local/src/test/java/org/bonitasoft/engine/application/installer/ApplicationInstallerUpdateIT.java @@ -84,7 +84,7 @@ private void initFirstInstall() throws Exception { final InputStream applicationAsStream = this.getClass().getResourceAsStream("/customer-application.zip"); // when: - applicationInstaller.install(applicationArchiveReader.read(applicationAsStream), "1.0.0"); + applicationInstaller.install(applicationArchiveReader.read(applicationAsStream)); // then: @@ -120,7 +120,7 @@ public void empty_custom_application_should_throw_an_exception() throws Exceptio // then: assertThatExceptionOfType(ApplicationInstallationException.class) - .isThrownBy(() -> applicationInstaller.update(applicationArchive, "1.0.1")) + .isThrownBy(() -> applicationInstaller.update(applicationArchive)) .withMessage("The Application Archive contains no valid artifact to install"); } @@ -141,7 +141,7 @@ public void process_update_custom_application_with_same_installed_version() thro try (var applicationAsStream = this.getClass().getResourceAsStream("/customer-application.zip")) { // Avoid test failure due to instant update. Awaitility.await().timeout(Duration.of(1, ChronoUnit.SECONDS)); - applicationInstaller.update(applicationArchiveReader.read(applicationAsStream), "1.0.0"); + applicationInstaller.update(applicationArchiveReader.read(applicationAsStream)); } // then: TenantResource updatedBdm = getTenantAdministrationAPI().getBusinessDataModelResource(); @@ -152,8 +152,9 @@ public void process_update_custom_application_with_same_installed_version() thro final ProcessDeploymentInfo deploymentInfoAfterUpdate = getProcessAPI() .getProcessDeploymentInfo(processDefinitionId); + // check that bdm resource has NOT been updated (same content) + assertThat(updatedBdm.getLastUpdateDate()).isEqualTo(bdm.getLastUpdateDate()); // check that resources has been updated - assertThat(updatedBdm.getLastUpdateDate().toEpochSecond() > bdm.getLastUpdateDate().toEpochSecond()).isTrue(); assertThat(updatedApplication.getLastUpdateDate().after(application.getLastUpdateDate())).isTrue(); assertThat( updatedProcessStarterAPI.getLastModificationDate().after(processStarterAPI.getLastModificationDate())) @@ -181,7 +182,7 @@ public void process_update_custom_application_with_new_version() // when: final InputStream applicationAsStream = this.getClass().getResourceAsStream("/customer-application-v2.zip"); - applicationInstaller.update(applicationArchiveReader.read(applicationAsStream), "1.0.1"); + applicationInstaller.update(applicationArchiveReader.read(applicationAsStream)); // then: TenantResource updatedBdm = getTenantAdministrationAPI().getBusinessDataModelResource(); @@ -193,7 +194,7 @@ public void process_update_custom_application_with_new_version() .getProcessDeploymentInfo(processDefinitionId); // check that resources has been updated - assertThat(updatedBdm.getLastUpdateDate().toEpochSecond() > bdm.getLastUpdateDate().toEpochSecond()).isTrue(); + assertThat(updatedBdm.getLastUpdateDate()).isAfter(bdm.getLastUpdateDate()); // check installed apps assertThat(updatedApplication.getLastUpdateDate().after(application.getLastUpdateDate())).isTrue(); // fetch application menus @@ -219,6 +220,6 @@ public void process_update_custom_application_with_new_version() // CallHealthCheck Process must not be updated assertThat(deploymentInfoAfterUpdate.getLastUpdateDate().after(deploymentInfo.getLastUpdateDate())).isTrue(); - assertThat(deploymentInfoAfterUpdate.getActivationState().name()).isEqualTo("DISABLED"); + assertThat(deploymentInfoAfterUpdate.getActivationState()).isEqualTo(ActivationState.DISABLED); } } diff --git a/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/api/impl/application/installer/ApplicationArchive.java b/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/api/impl/application/installer/ApplicationArchive.java index 85cd97d7e39..9cd4db78f7d 100644 --- a/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/api/impl/application/installer/ApplicationArchive.java +++ b/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/api/impl/application/installer/ApplicationArchive.java @@ -20,10 +20,8 @@ import java.util.List; import java.util.Optional; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.Singular; +import com.vdurmont.semver4j.Semver; +import lombok.*; import lombok.extern.slf4j.Slf4j; @Slf4j @@ -32,6 +30,7 @@ @AllArgsConstructor public class ApplicationArchive implements AutoCloseable { + private String fileName; private File organization; private File bdm; private List processes = new ArrayList<>(); @@ -41,11 +40,15 @@ public class ApplicationArchive implements AutoCloseable { private List themes = new ArrayList<>(); private List applications = new ArrayList<>(); private List applicationIcons = new ArrayList<>(); - private List ignoredFiles = new ArrayList<>(); - @Singular - private Optional configurationFile = Optional.empty(); + private File configurationFile; + + private String version; + + @Setter(AccessLevel.NONE) + @Getter(AccessLevel.NONE) + private Semver semver; public ApplicationArchive addPage(File page) { pages.add(page); @@ -92,6 +95,35 @@ public ApplicationArchive setBdm(File bdm) { return this; } + public Optional getConfigurationFile() { + return Optional.ofNullable(configurationFile); + } + + public void setVersion(String version) { + this.version = version; + if (version != null) { + this.semver = new Semver(version, Semver.SemverType.LOOSE); + } + } + + public boolean hasVersionGreaterThan(String version) { + if (semver == null) { + return false; + } + return semver.isGreaterThan(version); + } + + public boolean hasVersionEquivalentTo(String version) { + if (semver == null) { + return false; + } + return semver.isEquivalentTo(version); + } + + public boolean hasVersion() { + return semver != null; + } + /** * @return true if the application archive has no artifact */ @@ -133,4 +165,5 @@ protected void deletePhysicalFilesFromList(List list) throws IOException { Files.deleteIfExists(f.toPath()); } } + } diff --git a/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/api/impl/application/installer/ApplicationInstaller.java b/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/api/impl/application/installer/ApplicationInstaller.java index c4d69bf139e..f5e79df63da 100644 --- a/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/api/impl/application/installer/ApplicationInstaller.java +++ b/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/api/impl/application/installer/ApplicationInstaller.java @@ -24,21 +24,10 @@ import static org.bonitasoft.engine.bpm.process.ConfigurationState.RESOLVED; import static org.bonitasoft.engine.business.application.ApplicationImportPolicy.FAIL_ON_DUPLICATES; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.Serializable; +import java.io.*; import java.net.URLConnection; import java.nio.file.Files; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Properties; +import java.util.*; import java.util.concurrent.Callable; import java.util.stream.Collectors; @@ -62,31 +51,15 @@ import org.bonitasoft.engine.bpm.bar.BusinessArchive; import org.bonitasoft.engine.bpm.bar.BusinessArchiveFactory; import org.bonitasoft.engine.bpm.bar.InvalidBusinessArchiveFormatException; -import org.bonitasoft.engine.bpm.process.ActivationState; -import org.bonitasoft.engine.bpm.process.Problem; -import org.bonitasoft.engine.bpm.process.ProcessDefinitionNotFoundException; -import org.bonitasoft.engine.bpm.process.ProcessDeployException; -import org.bonitasoft.engine.bpm.process.ProcessDeploymentInfo; -import org.bonitasoft.engine.bpm.process.ProcessEnablementException; +import org.bonitasoft.engine.bpm.process.*; import org.bonitasoft.engine.business.application.ApplicationImportPolicy; import org.bonitasoft.engine.business.application.exporter.ApplicationNodeContainerConverter; import org.bonitasoft.engine.business.application.importer.ApplicationImporter; import org.bonitasoft.engine.business.application.importer.StrategySelector; import org.bonitasoft.engine.business.application.xml.ApplicationNode; -import org.bonitasoft.engine.business.data.BusinessDataModelRepository; -import org.bonitasoft.engine.business.data.BusinessDataRepositoryDeploymentException; -import org.bonitasoft.engine.business.data.InvalidBusinessDataModelException; -import org.bonitasoft.engine.business.data.SBusinessDataRepositoryDeploymentException; -import org.bonitasoft.engine.business.data.SBusinessDataRepositoryException; +import org.bonitasoft.engine.business.data.*; import org.bonitasoft.engine.commons.exceptions.SBonitaException; -import org.bonitasoft.engine.exception.AlreadyExistsException; -import org.bonitasoft.engine.exception.ApplicationInstallationException; -import org.bonitasoft.engine.exception.BonitaException; -import org.bonitasoft.engine.exception.BonitaRuntimeException; -import org.bonitasoft.engine.exception.CreationException; -import org.bonitasoft.engine.exception.ImportException; -import org.bonitasoft.engine.exception.SearchException; -import org.bonitasoft.engine.exception.UpdateException; +import org.bonitasoft.engine.exception.*; import org.bonitasoft.engine.identity.ImportPolicy; import org.bonitasoft.engine.identity.OrganizationImportException; import org.bonitasoft.engine.io.FileOperations; @@ -94,6 +67,7 @@ import org.bonitasoft.engine.page.PageCreator; import org.bonitasoft.engine.page.PageSearchDescriptor; import org.bonitasoft.engine.page.PageUpdater; +import org.bonitasoft.engine.platform.model.STenant; import org.bonitasoft.engine.search.SearchOptionsBuilder; import org.bonitasoft.engine.service.InstallationFailedException; import org.bonitasoft.engine.service.InstallationService; @@ -106,7 +80,6 @@ import org.bonitasoft.engine.transaction.UserTransactionService; import org.bonitasoft.platform.exception.PlatformException; import org.bonitasoft.platform.version.ApplicationVersionService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; @@ -143,7 +116,6 @@ public class ApplicationInstaller { private final Long tenantId; private final ApplicationNodeContainerConverter appXmlConverter = new ApplicationNodeContainerConverter(); - @Autowired public ApplicationInstaller(InstallationService installationService, @Qualifier("businessDataModelRepository") BusinessDataModelRepository bdmRepository, UserTransactionService transactionService, @Value("${tenantId}") Long tenantId, @@ -171,7 +143,7 @@ private OrganizationAPIDelegate getOrganizationImporter() { return OrganizationAPIDelegate.getInstance(); } - public void install(ApplicationArchive applicationArchive, String version) throws ApplicationInstallationException { + public void install(ApplicationArchive applicationArchive) throws ApplicationInstallationException { if (applicationArchive.isEmpty()) { throw new ApplicationInstallationException("The Application Archive contains no valid artifact to install"); } @@ -183,11 +155,11 @@ public void install(ApplicationArchive applicationArchive, String version) throw inSession(() -> inTransaction(() -> { var newlyInstalledProcessIds = installArtifacts(applicationArchive, executionResult); enableResolvedProcesses(newlyInstalledProcessIds, executionResult); - updateApplicationVersion(version); + updateApplicationVersion(applicationArchive.getVersion()); return null; })); log.info("The Application Archive (version {}) has been installed successfully in {} ms.", - version, (System.currentTimeMillis() - startPoint)); + applicationArchive.getVersion(), (System.currentTimeMillis() - startPoint)); } catch (Exception e) { throw new ApplicationInstallationException("The Application Archive install operation has been aborted", e); } finally { @@ -212,13 +184,12 @@ protected List installArtifacts(ApplicationArchive applicationArchive, Exe return installedProcessIds; } - public void update(ApplicationArchive applicationArchive, String version) throws ApplicationInstallationException { + public void update(ApplicationArchive applicationArchive) throws ApplicationInstallationException { if (applicationArchive.isEmpty()) { throw new ApplicationInstallationException("The Application Archive contains no valid artifact to install"); } final ExecutionResult executionResult = new ExecutionResult(); try { - pauseTenantInSession(); final long startPoint = System.currentTimeMillis(); log.info("Starting Application Archive installation..."); installBusinessDataModel(applicationArchive); @@ -226,24 +197,18 @@ public void update(ApplicationArchive applicationArchive, String version) throws List newlyInstalledProcessIds = updateArtifacts(applicationArchive, executionResult); disableOldProcesses(newlyInstalledProcessIds, executionResult); enableResolvedProcesses(newlyInstalledProcessIds, executionResult); - updateApplicationVersion(version); + updateApplicationVersion(applicationArchive.getVersion()); return null; })); log.info("The Application Archive has been installed successfully in {} ms.", (System.currentTimeMillis() - startPoint)); } catch (Exception e) { - throw new ApplicationInstallationException("The Application Archive install operation has been aborted", e); + throw new ApplicationInstallationException("The Application Archive update operation has been aborted", e); } finally { logInstallationResult(executionResult); - try { - resumeTenantInSession(); - } catch (Exception e) { - log.error("Error when resuming the tenant after installation"); - log.error(e.getMessage()); - } } if (executionResult.hasErrors()) { - throw new ApplicationInstallationException("The Application Archive install operation has been aborted"); + throw new ApplicationInstallationException("The Application Archive update operation has been aborted"); } } @@ -251,11 +216,13 @@ public void update(ApplicationArchive applicationArchive, String version) throws public void resumeTenantInSession() throws Exception { inSession(() -> { try { - tenantStateManager.resume(); - transactionService.executeInTransaction(() -> { - businessArchiveArtifactsManager.resolveDependenciesForAllProcesses(getServiceAccessor()); - return null; - }); + if (Objects.equals(STenant.PAUSED, tenantStateManager.getStatus())) { + tenantStateManager.resume(); + transactionService.executeInTransaction(() -> { + businessArchiveArtifactsManager.resolveDependenciesForAllProcesses(getServiceAccessor()); + return null; + }); + } } catch (Exception e) { throw new UpdateException(e); } @@ -268,11 +235,9 @@ public void pauseTenantInSession() throws Exception { inSession(() -> { try { String status = tenantStateManager.getStatus(); - if (status.equals("ACTIVATED")) { + if (STenant.ACTIVATED.equals(status)) { tenantStateManager.pause(); - } else if (status.equals("PAUSED")) { - // do nothing, tenant already paused - } else { + } else if (!STenant.PAUSED.equals(status)) { throw new UpdateException( "The default tenant is in state " + status + " and cannot be paused. Aborting."); } @@ -450,12 +415,31 @@ protected void updateOrganization(ApplicationArchive applicationArchive, Executi protected void installBusinessDataModel(ApplicationArchive applicationArchive) throws Exception { if (applicationArchive.getBdm() != null) { - final String bdmVersion = inSession( - () -> inTransaction(() -> updateBusinessDataModel(applicationArchive))); - log.info("BDM successfully installed (version({})", bdmVersion); + var alreadyDeployed = sameBdmContentDeployed(applicationArchive.getBdm()); + if (alreadyDeployed) { + log.info("Installed and current BDM are equivalent. No BDM update required."); + return; + } + log.info("BDM must be installed or updated..."); + pauseTenantInSession(); + try { + final String bdmVersion = inSession( + () -> inTransaction(() -> updateBusinessDataModel(applicationArchive))); + log.info("BDM successfully installed (version({})", bdmVersion); + } finally { + resumeTenantInSession(); + } } } + boolean sameBdmContentDeployed(File bdmArchive) throws Exception { + return inSession(() -> inTransaction(() -> { + log.info("Comparing BDM to install with current BDM..."); + return bdmRepository + .isDeployed(Files.readAllBytes(bdmArchive.toPath())); + })); + } + protected String updateBusinessDataModel(ApplicationArchive applicationArchive) throws InvalidBusinessDataModelException, BusinessDataRepositoryDeploymentException { String bdmVersion; @@ -830,7 +814,7 @@ ProcessManagementAPIImplDelegate getProcessManagementAPIDelegate() { public T inSession(Callable callable) throws Exception { final SSession session = sessionService.createSession(tenantId, SessionService.SYSTEM); final long sessionId = session.getId(); - log.info("Created new session with id {}", sessionId); + log.trace("New session created with id {}", sessionId); try { sessionAccessor.setSessionInfo(sessionId, tenantId); return callable.call(); diff --git a/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/api/impl/application/installer/CustomOrDefaultApplicationInstaller.java b/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/api/impl/application/installer/CustomOrDefaultApplicationInstaller.java index 6985a97965d..502b9703a7d 100644 --- a/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/api/impl/application/installer/CustomOrDefaultApplicationInstaller.java +++ b/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/api/impl/application/installer/CustomOrDefaultApplicationInstaller.java @@ -16,14 +16,9 @@ import static java.lang.String.format; import java.io.IOException; -import java.io.InputStream; import java.io.UncheckedIOException; import java.util.Optional; -import java.util.Properties; -import java.util.zip.ZipFile; -import com.vdurmont.semver4j.Semver; -import com.vdurmont.semver4j.Semver.SemverType; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -53,12 +48,8 @@ @RequiredArgsConstructor public class CustomOrDefaultApplicationInstaller { - private static final String VERSION_PROPERTY = "version"; - public static final String CUSTOM_APPLICATION_DEFAULT_FOLDER = "my-application"; - public static final String VERSION_FILE_NAME = "application.properties"; - @Value("${bonita.runtime.custom-application.install-folder:" + CUSTOM_APPLICATION_DEFAULT_FOLDER + "}") @Getter protected String applicationInstallFolder; @@ -90,58 +81,36 @@ public void autoDeployDetectedCustomApplication(PlatformStartedEvent event) return; } - var newVersion = readApplicationVersion(customApplication) - .orElseThrow(() -> new ApplicationInstallationException( - "Application version not found. Abort installation.")); - log.info("Custom application detected with name '{}' under folder '{}'", - customApplication.getFilename(), - applicationInstallFolder); - if (isPlatformFirstInitialization()) { - // install application if it exists and if it is the first init of the platform - log.info("Bonita now tries to install it automatically..."); - installCustomApplication(customApplication, newVersion); - } else { - var currentVersion = new Semver(applicationVersionService.retrieveApplicationVersion(), SemverType.LOOSE); - log.info("Detected application version: '{}'; Current deployed version: '{}'", - newVersion, - currentVersion); - if (newVersion.isGreaterThan(currentVersion)) { - log.info("Updating the application..."); - updateCustomApplication(customApplication, newVersion); - } else if (newVersion.isEquivalentTo(currentVersion)) { - log.info("Updating process configuration only..."); - findAndUpdateConfiguration(); - } else { - throw new ApplicationInstallationException("An application has been detected, but its newVersion " - + newVersion + " is inferior to the one deployed: " + currentVersion - + ". Nothing will be updated, and the Bonita engine startup has been aborted."); + try (var applicationArchive = createApplicationArchive(customApplication)) { + if (!applicationArchive.hasVersion()) { + throw new ApplicationInstallationException( + "Application version not found. Abort installation."); } - } - } - - @VisibleForTesting - Optional readApplicationVersion(Resource customApplication) throws IOException { - if (customApplication != null) { - try (var customApplicationZip = new ZipFile(customApplication.getFile())) { - var applicationPropertiesEntry = customApplicationZip.getEntry(VERSION_FILE_NAME); - if (applicationPropertiesEntry == null) { - return Optional.empty(); + log.info("Custom application detected with name '{}' under folder '{}'", + customApplication.getFilename(), + applicationInstallFolder); + if (isPlatformFirstInitialization()) { + // install application if it exists and if it is the first init of the platform + log.info("Bonita now tries to install it automatically..."); + applicationInstaller.install(applicationArchive); + } else { + var currentVersion = applicationVersionService.retrieveApplicationVersion(); + log.info("Detected application version: '{}'; Current deployed version: '{}'", + applicationArchive.getVersion(), + currentVersion); + if (applicationArchive.hasVersionGreaterThan(applicationVersionService.retrieveApplicationVersion())) { + log.info("Updating the application..."); + applicationInstaller.update(applicationArchive); + } else if (applicationArchive.hasVersionEquivalentTo(currentVersion)) { + log.info("Updating process configuration only..."); + findAndUpdateConfiguration(); + } else { + throw new ApplicationInstallationException("An application has been detected, but its newVersion " + + applicationArchive.getVersion() + " is inferior to the one deployed: " + currentVersion + + ". Nothing will be updated, and the Bonita engine startup has been aborted."); } - var properties = new Properties(); - var applicationPropertiesInputStream = customApplicationZip.getInputStream(applicationPropertiesEntry); - properties.load(applicationPropertiesInputStream); - return Optional.ofNullable(toSemver(properties.getProperty(VERSION_PROPERTY))); } } - return Optional.empty(); - } - - @VisibleForTesting - Semver toSemver(String version) { - if (version == null) { - return null; - } - return new Semver(version, SemverType.LOOSE); } boolean isPlatformFirstInitialization() { @@ -155,7 +124,7 @@ boolean isPlatformFirstInitialization() { */ @VisibleForTesting Resource detectCustomApplication() throws IOException, ApplicationInstallationException { - log.info("Trying to detect custom application (.zip file from folder {})", applicationInstallFolder); + log.debug("Trying to detect custom application (.zip file from folder {})", applicationInstallFolder); return getResourceFromClasspath(getCustomAppResourcesFromClasspath(), "application zip"); } @@ -170,7 +139,7 @@ protected static Resource getResourceFromClasspath(Resource[] resources, String nbZipApplication++; customRsource = resource; } else { - log.info("A custom resource file '{}' is found but it cannot be read. It will be ignored.", + log.warn("A custom resource file '{}' is found but it cannot be read. It will be ignored.", resource.getFilename()); } } @@ -190,57 +159,26 @@ Resource[] getCustomAppResourcesFromClasspath() throws IOException { ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + "/" + applicationInstallFolder + "/*.zip"); } - /** - * @param customApplication custom application resource - * @param version version of the custom application - * @throws ApplicationInstallationException if unable to install the application - */ - protected void updateCustomApplication(final Resource customApplication, Semver version) - throws Exception { - try (final InputStream applicationZipFileStream = customApplication.getInputStream(); - ApplicationArchive applicationArchive = getApplicationArchive(applicationZipFileStream)) { - setConfigurationFile(applicationArchive); - applicationInstaller.update(applicationArchive, version.getValue()); - } catch (IOException | ApplicationInstallationException e) { - throw new ApplicationInstallationException( - "Unable to update the application " + customApplication.getFilename(), e); - } - } - private void setConfigurationFile(ApplicationArchive applicationArchive) - throws IOException, ApplicationInstallationException { + throws IOException { detectConfigurationFile().ifPresent(resource -> { try { - log.info("Found application configuration file " + resource.getFilename()); - applicationArchive.setConfigurationFile(Optional.of(resource.getFile())); + log.debug("Found application configuration file " + resource.getFilename()); + applicationArchive.setConfigurationFile(resource.getFile()); } catch (IOException e) { throw new UncheckedIOException(e); } }); } - /** - * @param customApplication custom application resource - * @param version version of the custom application - * @throws ApplicationInstallationException if unable to install the application - */ - protected void installCustomApplication(final Resource customApplication, Semver version) throws Exception { - try (final InputStream applicationZipFileStream = customApplication.getInputStream(); - ApplicationArchive applicationArchive = getApplicationArchive(applicationZipFileStream)) { + protected ApplicationArchive createApplicationArchive(Resource customApplication) { + try (var applicationZipFileStream = customApplication.getInputStream()) { + var applicationArchive = applicationArchiveReader.read(applicationZipFileStream); setConfigurationFile(applicationArchive); - applicationInstaller.install(applicationArchive, version.getValue()); - } catch (IOException | ApplicationInstallationException e) { - throw new ApplicationInstallationException( - "Unable to install the application " + customApplication.getFilename(), e); - } - } - - protected ApplicationArchive getApplicationArchive(InputStream applicationZipFileStream) - throws ApplicationInstallationException { - try { - return applicationArchiveReader.read(applicationZipFileStream); + return applicationArchive; } catch (IOException e) { - throw new ApplicationInstallationException("Unable to read application archive", e); + throw new ApplicationInstallationException( + String.format("Unable to read the %s application archive.", customApplication.getFilename()), e); } } @@ -257,8 +195,8 @@ void installDefaultProvidedApplications() throws ApplicationInstallationExceptio } } - protected Optional detectConfigurationFile() throws IOException, ApplicationInstallationException { - log.info("Trying to detect configuration file (.bconf file from folder {})", applicationInstallFolder); + protected Optional detectConfigurationFile() throws IOException { + log.debug("Trying to detect configuration file (.bconf file from folder {})", applicationInstallFolder); return Optional.ofNullable( getResourceFromClasspath(getConfigurationFileResourcesFromClasspath(), "configuration file .bconf")); } @@ -274,7 +212,7 @@ protected void findAndUpdateConfiguration() throws ApplicationInstallationExcept final ExecutionResult executionResult = new ExecutionResult(); detectConfigurationFile().ifPresent(resource -> { try { - log.info("Found application configuration file " + resource.getFilename()); + log.debug("Found application configuration file " + resource.getFilename()); applicationInstaller.updateConfiguration(resource.getFile(), executionResult); } catch (Exception e) { throw new ApplicationInstallationException("Failed to update configuration.", e); diff --git a/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/api/impl/application/installer/detector/ArtifactTypeDetector.java b/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/api/impl/application/installer/detector/ArtifactTypeDetector.java index 6fc9507f719..f11b4800ca4 100644 --- a/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/api/impl/application/installer/detector/ArtifactTypeDetector.java +++ b/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/api/impl/application/installer/detector/ArtifactTypeDetector.java @@ -13,19 +13,24 @@ **/ package org.bonitasoft.engine.api.impl.application.installer.detector; -import java.io.*; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Objects; +import java.util.Properties; +import lombok.extern.slf4j.Slf4j; import org.bonitasoft.engine.api.impl.application.installer.ApplicationArchive; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; import org.springframework.stereotype.Component; @Component @ConditionalOnSingleCandidate(ArtifactTypeDetector.class) +@Slf4j public class ArtifactTypeDetector { - private static final Logger logger = LoggerFactory.getLogger(ArtifactTypeDetector.class); + private static final String VERSION_PROPERTY = "version"; + private static final String APPLICATION_PROPERTIES_FILENAME = "application.properties"; private static final String REST_API_EXTENSION_CONTENT_TYPE = "apiExtension"; @@ -91,47 +96,61 @@ public boolean isProcess(File file) throws IOException { return processDetector.isCompliant(file); } + private boolean isApplicationProperties(File file) { + return file.isFile() && Objects.equals(APPLICATION_PROPERTIES_FILENAME, file.getName()); + } + public void checkFileAndAddToArchive(File file, ApplicationArchive applicationArchive) throws IOException { if (isApplication(file)) { - logger.info("Found application file: '{}'. ", file.getName()); + log.debug("Found application file: '{}'. ", file.getName()); applicationArchive.addApplication(file); } else if (isApplicationIcon(file)) { - logger.info("Found icon file: '{}'. ", file.getName()); + log.debug("Found icon file: '{}'. ", file.getName()); applicationArchive.addApplicationIcon(file); } else if (isProcess(file)) { - logger.info("Found process file: '{}'. ", file.getName()); + log.debug("Found process file: '{}'. ", file.getName()); applicationArchive.addProcess(file); } else if (isOrganization(file)) { - logger.info("Found organization file: '{}'. ", file.getName()); + log.debug("Found organization file: '{}'. ", file.getName()); if (applicationArchive.getOrganization() != null) { - logger.warn("An organization file has already been set. Using {}", + log.warn("An organization file has already been set. Using {}", applicationArchive.getOrganization()); ignoreFile(file, applicationArchive); } else { applicationArchive.setOrganization(file); } } else if (isPage(file)) { - logger.info("Found page file: '{}'. ", file.getName()); + log.debug("Found page file: '{}'. ", file.getName()); applicationArchive.addPage(file); } else if (isLayout(file)) { - logger.info("Found layout file: '{}'. ", file.getName()); + log.debug("Found layout file: '{}'. ", file.getName()); applicationArchive.addLayout(file); } else if (isTheme(file)) { - logger.info("Found theme file: '{}'. ", file.getName()); + log.debug("Found theme file: '{}'. ", file.getName()); applicationArchive.addTheme(file); } else if (isRestApiExtension(file)) { - logger.info("Found rest api extension file: '{}'. ", file.getName()); + log.debug("Found rest api extension file: '{}'. ", file.getName()); applicationArchive.addRestAPIExtension(file); } else if (isBdm(file)) { - logger.info("Found business data model file: '{}'. ", file.getName()); + log.debug("Found business data model file: '{}'. ", file.getName()); applicationArchive.setBdm(file); + } else if (isApplicationProperties(file)) { + applicationArchive.setVersion(readVersion(file)); } else { ignoreFile(file, applicationArchive); } } + String readVersion(File applicationPropertiesFile) throws IOException { + try (var is = Files.newInputStream(applicationPropertiesFile.toPath())) { + var properties = new Properties(); + properties.load(is); + return properties.getProperty(VERSION_PROPERTY, null); + } + } + private void ignoreFile(File file, ApplicationArchive applicationArchive) { - logger.warn("Ignoring file '{}'.", file.getName()); + log.debug("Ignoring file '{}'.", file.getName()); applicationArchive.addIgnoredFile(file); } diff --git a/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/business/application/importer/MandatoryLivingApplicationImporter.java b/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/business/application/importer/MandatoryLivingApplicationImporter.java index 66827d9eab4..dea8af27d0c 100644 --- a/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/business/application/importer/MandatoryLivingApplicationImporter.java +++ b/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/business/application/importer/MandatoryLivingApplicationImporter.java @@ -83,9 +83,9 @@ private void importMandatoryPages() { List createdOrReplaced = getNonSkippedImportedResources(importStatuses); if (createdOrReplaced.isEmpty()) { - log.info("No mandatory pages updated"); + log.debug("No mandatory pages updated"); } else { - log.info("Mandatory pages updated or created: {}", createdOrReplaced); + log.debug("Mandatory pages updated or created: {}", createdOrReplaced); } } catch (BonitaException | IOException e) { log.error(ExceptionUtils.printLightWeightStacktrace(e)); diff --git a/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/platform/PlatformVersionChecker.java b/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/platform/PlatformVersionChecker.java index 198a6b5abee..3bdf3455244 100644 --- a/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/platform/PlatformVersionChecker.java +++ b/bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/platform/PlatformVersionChecker.java @@ -44,7 +44,7 @@ public class PlatformVersionChecker { private final PlatformService platformService; private final BroadcastService broadcastService; - private TransactionService transactionService; + private final TransactionService transactionService; private String errorMessage; 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 4c4dcb3c53a..53abd580bd2 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 @@ -2177,13 +2177,6 @@ - - - - - - diff --git a/bpm/bonita-core/bonita-process-engine/src/main/resources/bonita-tenant-community.properties b/bpm/bonita-core/bonita-process-engine/src/main/resources/bonita-tenant-community.properties index 02389b650be..db8ea00ca59 100644 --- a/bpm/bonita-core/bonita-process-engine/src/main/resources/bonita-tenant-community.properties +++ b/bpm/bonita-core/bonita-process-engine/src/main/resources/bonita-tenant-community.properties @@ -145,7 +145,7 @@ bonita.tenant.bdm.repository.validator.apply_to_ddl=false bonita.tenant.bdm.repository.javax.persistence.validation.mode=NONE # Business Data Schema manager -bonita.tenant.bdm.schemamanager.show_sql=true +bonita.tenant.bdm.schemamanager.show_sql=false bonita.tenant.bdm.schemamanager.format_sql=true bonita.tenant.bdm.schemamanager.validator.autoregister_listeners=false bonita.tenant.bdm.schemamanager.validator.apply_to_ddl=false diff --git a/bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/api/impl/application/installer/ApplicationArchiveTest.java b/bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/api/impl/application/installer/ApplicationArchiveTest.java new file mode 100644 index 00000000000..55598fa0db4 --- /dev/null +++ b/bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/api/impl/application/installer/ApplicationArchiveTest.java @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2023 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.api.impl.application.installer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThrows; + +import com.vdurmont.semver4j.SemverException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ApplicationArchiveTest { + + @ParameterizedTest + @ValueSource(strings = { "SNAPSHOT", "", ".0" }) + void unsupportedApplicationVersions(String version) + throws Exception { + try (var applicationArchive = new ApplicationArchive()) { + assertThrows(SemverException.class, () -> applicationArchive.setVersion(version)); + } + } + + @ParameterizedTest + @ValueSource(strings = { "9-SNAPSHOT", "5", "2.0", "1.0.0", "2.1-alpha", "3.3.2.beta1" }) + void supportedApplicationVersions(String version) + throws Exception { + try (var applicationArchive = new ApplicationArchive()) { + applicationArchive.setVersion(version); + assertThat(applicationArchive.hasVersion()).isTrue(); + } + } + + @Test + void hasGreaterVersionThan() throws Exception { + try (var applicationArchive = new ApplicationArchive()) { + applicationArchive.setVersion("2.0"); + assertThat(applicationArchive.hasVersionGreaterThan("1.0.0")).isTrue(); + + applicationArchive.setVersion("0.0.2"); + assertThat(applicationArchive.hasVersionGreaterThan("1.0.0")).isFalse(); + + applicationArchive.setVersion(null); + assertThat(applicationArchive.hasVersionGreaterThan("1.0.0")).isFalse(); + } + } + + @Test + void hasEquivalentVersionTo() throws Exception { + try (var applicationArchive = new ApplicationArchive()) { + applicationArchive.setVersion("2.0"); + assertThat(applicationArchive.hasVersionEquivalentTo("2.0")).isTrue(); + + applicationArchive.setVersion("0.0.2"); + assertThat(applicationArchive.hasVersionEquivalentTo("1.0.0")).isFalse(); + + applicationArchive.setVersion(null); + assertThat(applicationArchive.hasVersionEquivalentTo("1.0.0")).isFalse(); + } + } +} diff --git a/bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/api/impl/application/installer/ApplicationInstallerTest.java b/bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/api/impl/application/installer/ApplicationInstallerTest.java index 4139d1496e2..f7a14694281 100644 --- a/bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/api/impl/application/installer/ApplicationInstallerTest.java +++ b/bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/api/impl/application/installer/ApplicationInstallerTest.java @@ -69,6 +69,7 @@ import org.bonitasoft.engine.io.FileOperations; import org.bonitasoft.engine.page.Page; import org.bonitasoft.engine.service.InstallationService; +import org.bonitasoft.engine.tenant.TenantStateManager; import org.bonitasoft.engine.transaction.UserTransactionService; import org.junit.Before; import org.junit.Rule; @@ -99,6 +100,9 @@ public class ApplicationInstallerTest { @Mock private InstallationService installationService; + @Mock + private TenantStateManager tenantStateManager; + @Captor ArgumentCaptor> callableCaptor; @@ -133,13 +137,14 @@ public void should_install_application_containing_all_kind_of_custom_pages() thr .addLayout(layout) .addTheme(theme) .addRestAPIExtension(restAPI); + applicationArchive.setVersion("1.0.0"); doNothing().when(applicationInstaller).enableResolvedProcesses(any(), any()); doNothing().when(applicationInstaller).installOrganization(any(), any()); doReturn(mock(Page.class)).when(applicationInstaller).createPage(any(), any()); doReturn(null).when(applicationInstaller).getPageIfExist(any()); - applicationInstaller.install(applicationArchive, "1.0.0"); + applicationInstaller.install(applicationArchive); } var captor = ArgumentCaptor.forClass(Properties.class); verify(applicationInstaller, times(4)).createPage(any(), captor.capture()); @@ -160,7 +165,8 @@ public void should_install_application_containing_living_applications() throws E doNothing().when(applicationInstaller).enableResolvedProcesses(any(), any()); try (var applicationArchive = new ApplicationArchive()) { applicationArchive.addApplication(application); - applicationInstaller.install(applicationArchive, "1.0.0"); + applicationArchive.setVersion("1.0.0"); + applicationInstaller.install(applicationArchive); } verify(applicationInstaller).importApplication(any(), any(), eq(FAIL_ON_DUPLICATES)); @@ -218,7 +224,8 @@ public void should_install_and_enable_resolved_process() throws Exception { // when try (var applicationArchive = new ApplicationArchive()) { applicationArchive.addProcess(process); - applicationInstaller.install(applicationArchive, "1.0.0"); + applicationArchive.setVersion("1.0.0"); + applicationInstaller.install(applicationArchive); } // then verify(applicationInstaller).deployProcess(any(), any()); @@ -235,12 +242,17 @@ public void should_install_bdm() throws Exception { applicationArchive.setBdm(bdm); doNothing().when(applicationInstaller).installOrganization(any(), any()); doReturn("1.0").when(applicationInstaller).updateBusinessDataModel(applicationArchive); + doReturn(false).when(applicationInstaller).sameBdmContentDeployed(any()); doReturn(Collections.emptyList()).when(applicationInstaller).installProcesses(any(), any()); doNothing().when(applicationInstaller).enableResolvedProcesses(any(), any()); + doNothing().when(applicationInstaller).pauseTenantInSession(); + doNothing().when(applicationInstaller).resumeTenantInSession(); + applicationArchive.setVersion("1.0.0"); + applicationInstaller.install(applicationArchive); - applicationInstaller.install(applicationArchive, "1.0.0"); - + verify(applicationInstaller).pauseTenantInSession(); verify(applicationInstaller).updateBusinessDataModel(applicationArchive); + verify(applicationInstaller).resumeTenantInSession(); } } @@ -260,7 +272,8 @@ public void should_call_enable_resolved_processes() throws Exception { try (var applicationArchive = new ApplicationArchive()) { applicationArchive.addProcess(process); - applicationInstaller.install(applicationArchive, "1.0.0"); + applicationArchive.setVersion("1.0.0"); + applicationInstaller.install(applicationArchive); } verify(applicationInstaller).deployProcess(any(), any()); @@ -344,8 +357,9 @@ public void should_skip_install_process_if_already_existing() throws Exception { @Test public void should_throw_exception_if_application_archive_is_empty() throws Exception { try (ApplicationArchive applicationArchive = new ApplicationArchive()) { + applicationArchive.setVersion("1.0.0"); assertThatExceptionOfType(ApplicationInstallationException.class) - .isThrownBy(() -> applicationInstaller.install(applicationArchive, "1.0.0")) + .isThrownBy(() -> applicationInstaller.install(applicationArchive)) .withMessage("The Application Archive contains no valid artifact to install"); } } @@ -357,7 +371,8 @@ public void should_install_organization() throws Exception { doNothing().when(applicationInstaller).enableResolvedProcesses(any(), any()); try (var applicationArchive = new ApplicationArchive()) { applicationArchive.setOrganization(organization); - applicationInstaller.install(applicationArchive, "1.0.0"); + applicationArchive.setVersion("1.0.0"); + applicationInstaller.install(applicationArchive); } verify(applicationInstaller).importOrganization(organization, ImportPolicy.FAIL_ON_DUPLICATES); diff --git a/bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/api/impl/application/installer/ApplicationInstallerUpdateTest.java b/bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/api/impl/application/installer/ApplicationInstallerUpdateTest.java index ee9e0c7f7f0..968b2164b3c 100644 --- a/bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/api/impl/application/installer/ApplicationInstallerUpdateTest.java +++ b/bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/api/impl/application/installer/ApplicationInstallerUpdateTest.java @@ -45,6 +45,7 @@ import org.bonitasoft.engine.business.application.xml.ApplicationNode; import org.bonitasoft.engine.business.application.xml.ApplicationNodeBuilder.ApplicationBuilder; import org.bonitasoft.engine.business.application.xml.ApplicationNodeContainer; +import org.bonitasoft.engine.business.data.BusinessDataModelRepository; import org.bonitasoft.engine.exception.ApplicationInstallationException; import org.bonitasoft.engine.identity.ImportPolicy; import org.bonitasoft.engine.io.FileOperations; @@ -75,6 +76,9 @@ public class ApplicationInstallerUpdateTest { @Captor ArgumentCaptor> callableCaptor; + @Mock + private BusinessDataModelRepository bdmRepository; + @InjectMocks @Spy private ApplicationInstaller applicationInstaller; @@ -115,10 +119,11 @@ public void should_update_application_containing_all_kind_of_custom_pages() thro .addLayout(layout) .addTheme(theme) .addRestAPIExtension(restAPI); + applicationArchive.setVersion("1.0.1"); doNothing().when(applicationInstaller).disableOldProcesses(any(), any()); doNothing().when(applicationInstaller).enableResolvedProcesses(any(), any()); - applicationInstaller.update(applicationArchive, "1.0.1"); + applicationInstaller.update(applicationArchive); } var captor = ArgumentCaptor.forClass(File.class); verify(applicationInstaller, times(4)).updatePageContent(captor.capture(), eq(1L)); @@ -137,8 +142,9 @@ public void should_update_application_containing_living_applications() throws Ex doNothing().when(applicationInstaller).disableOldProcesses(any(), any()); doNothing().when(applicationInstaller).enableResolvedProcesses(any(), any()); try (var applicationArchive = new ApplicationArchive()) { + applicationArchive.setVersion("1.0.1"); applicationArchive.addApplication(application); - applicationInstaller.update(applicationArchive, "1.0.1"); + applicationInstaller.update(applicationArchive); } verify(applicationInstaller).importApplication(any(), any(), eq(REPLACE_DUPLICATES)); @@ -175,7 +181,8 @@ public void should_install_and_enable_resolved_process() throws Exception { // when try (var applicationArchive = new ApplicationArchive()) { applicationArchive.addProcess(process); - applicationInstaller.update(applicationArchive, "1.0.1"); + applicationArchive.setVersion("1.0.1"); + applicationInstaller.update(applicationArchive); } // then verify(applicationInstaller).deployProcess( @@ -200,7 +207,8 @@ public void should_not_install_process_if_process_with_same_version_exists() thr // when try (var applicationArchive = new ApplicationArchive()) { applicationArchive.addProcess(process); - applicationInstaller.update(applicationArchive, "1.0.1"); + applicationArchive.setVersion("1.0.1"); + applicationInstaller.update(applicationArchive); } // then verify(applicationInstaller, never()).deployProcess(any(), any()); @@ -239,7 +247,8 @@ public void should_install_and_enable_resolved_process_and_disable_previous_vers // when try (var applicationArchive = new ApplicationArchive()) { applicationArchive.addProcess(process); - applicationInstaller.update(applicationArchive, "1.0.1"); + applicationArchive.setVersion("1.0.1"); + applicationInstaller.update(applicationArchive); } // then verify(applicationInstaller).deployProcess(any(), any()); @@ -255,9 +264,11 @@ public void should_update_bdm() throws Exception { applicationArchive.setBdm(bdm); doReturn("1.0").when(applicationInstaller).updateBusinessDataModel(applicationArchive); doNothing().when(applicationInstaller).enableResolvedProcesses(any(), any()); + doReturn(false).when(bdmRepository).isDeployed(any(byte[].class)); doNothing().when(applicationInstaller).disableOldProcesses(any(), any()); + applicationArchive.setVersion("1.0.1"); - applicationInstaller.update(applicationArchive, "1.0.1"); + applicationInstaller.update(applicationArchive); verify(applicationInstaller).updateBusinessDataModel(applicationArchive); } @@ -279,7 +290,8 @@ public void should_call_enable_resolved_processes() throws Exception { try (var applicationArchive = new ApplicationArchive()) { applicationArchive.addProcess(process); - applicationInstaller.update(applicationArchive, "1.0.1"); + applicationArchive.setVersion("1.0.1"); + applicationInstaller.update(applicationArchive); } verify(applicationInstaller).deployProcess(any(), any()); @@ -290,8 +302,9 @@ public void should_call_enable_resolved_processes() throws Exception { @Test public void should_throw_exception_if_application_archive_is_empty() throws Exception { try (ApplicationArchive applicationArchive = new ApplicationArchive()) { + applicationArchive.setVersion("1.0.1"); assertThatExceptionOfType(ApplicationInstallationException.class) - .isThrownBy(() -> applicationInstaller.update(applicationArchive, "1.0.1")) + .isThrownBy(() -> applicationInstaller.update(applicationArchive)) .withMessage("The Application Archive contains no valid artifact to install"); } } @@ -304,7 +317,8 @@ public void should_update_organisation() throws Exception { doNothing().when(applicationInstaller).disableOldProcesses(any(), any()); try (var applicationArchive = new ApplicationArchive()) { applicationArchive.setOrganization(organization); - applicationInstaller.update(applicationArchive, "1.0.1"); + applicationArchive.setVersion("1.0.1"); + applicationInstaller.update(applicationArchive); } verify(applicationInstaller).importOrganization(organization, ImportPolicy.IGNORE_DUPLICATES); diff --git a/bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/api/impl/application/installer/CustomOrDefaultApplicationInstallerTest.java b/bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/api/impl/application/installer/CustomOrDefaultApplicationInstallerTest.java index 80d1855b7fb..a772852efe7 100644 --- a/bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/api/impl/application/installer/CustomOrDefaultApplicationInstallerTest.java +++ b/bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/api/impl/application/installer/CustomOrDefaultApplicationInstallerTest.java @@ -15,17 +15,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.*; import java.io.File; import java.io.IOException; @@ -34,8 +25,6 @@ import java.util.concurrent.Callable; import java.util.stream.Stream; -import com.vdurmont.semver4j.Semver; -import com.vdurmont.semver4j.SemverException; import org.bonitasoft.engine.business.application.importer.DefaultLivingApplicationImporter; import org.bonitasoft.engine.exception.ApplicationInstallationException; import org.bonitasoft.engine.tenant.TenantServicesManager; @@ -47,20 +36,12 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.InOrder; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Spy; +import org.mockito.*; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; -import org.springframework.core.io.support.PathMatchingResourcePatternResolver; -import org.springframework.core.io.support.ResourcePatternResolver; /** * @author Emmanuel Duchastenier @@ -80,6 +61,9 @@ class CustomOrDefaultApplicationInstallerTest { TenantServicesManager tenantServicesManager; @Mock ApplicationVersionService applicationVersionService; + + @Mock + private ApplicationArchiveReader applicationArchiveReader; @InjectMocks @Spy private CustomOrDefaultApplicationInstaller listener; @@ -148,20 +132,6 @@ private static Stream ignoredApplicationResources() { ); } - @ParameterizedTest - @ValueSource(strings = { "SNAPSHOT", "", ".0" }) - void unsupportedApplicationVersions(String version) - throws Exception { - assertThrows(SemverException.class, () -> listener.toSemver(version)); - } - - @ParameterizedTest - @ValueSource(strings = { "9-SNAPSHOT", "5", "2.0", "1.0.0", "2.1-alpha", "3.3.2.beta1" }) - void supportedApplicationVersions(String version) - throws Exception { - assertThat(listener.toSemver(version)).isNotNull(); - } - @Test void should_install_custom_application_if_detected_and_platform_first_init() throws Exception { @@ -170,16 +140,16 @@ void should_install_custom_application_if_detected_and_platform_first_init() InputStream resourceStream1 = mock(InputStream.class); doReturn(resource1).when(listener).detectCustomApplication(); - doReturn(Optional.of(new Semver("1.0.0"))).when(listener).readApplicationVersion(resource1); doReturn(resourceStream1).when(resource1).getInputStream(); - final ApplicationArchive applicationArchive = mock(ApplicationArchive.class); - doReturn(applicationArchive).when(listener).getApplicationArchive(resourceStream1); + final ApplicationArchive applicationArchive = new ApplicationArchive(); + applicationArchive.setVersion("1.0.0"); + doReturn(applicationArchive).when(applicationArchiveReader).read(resourceStream1); doReturn(true).when(listener).isPlatformFirstInitialization(); //when listener.autoDeployDetectedCustomApplication(any()); //then - verify(applicationInstaller).install(applicationArchive, "1.0.0"); + verify(applicationInstaller).install(applicationArchive); verify(defaultLivingApplicationImporter, never()).execute(); } @@ -191,10 +161,10 @@ void should_update_custom_application_if_detected_version_superior_to_deployed_v InputStream resourceStream1 = mock(InputStream.class); doReturn(resource1).when(listener).detectCustomApplication(); - doReturn(Optional.of(new Semver("1.0.1"))).when(listener).readApplicationVersion(resource1); doReturn(resourceStream1).when(resource1).getInputStream(); - final ApplicationArchive applicationArchive = mock(ApplicationArchive.class); - doReturn(applicationArchive).when(listener).getApplicationArchive(resourceStream1); + final ApplicationArchive applicationArchive = new ApplicationArchive(); + applicationArchive.setVersion("1.0.1"); + doReturn(applicationArchive).when(applicationArchiveReader).read(resourceStream1); doReturn(false).when(listener).isPlatformFirstInitialization(); doReturn("1.0.0").when(applicationVersionService).retrieveApplicationVersion(); //when @@ -202,7 +172,7 @@ void should_update_custom_application_if_detected_version_superior_to_deployed_v //then InOrder inOrder = inOrder(defaultLivingApplicationImporter, applicationInstaller); - inOrder.verify(applicationInstaller).update(applicationArchive, "1.0.1"); + inOrder.verify(applicationInstaller).update(applicationArchive); verify(defaultLivingApplicationImporter, never()).execute(); } @@ -212,7 +182,9 @@ void should_update_conf_if_detected_version_equal_to_deployed_version() //given Resource resource1 = mockResource("resource1", true, true, 0L); doReturn(resource1).when(listener).detectCustomApplication(); - doReturn(Optional.of(new Semver("1.0.0"))).when(listener).readApplicationVersion(resource1); + var applicationArchive = new ApplicationArchive(); + applicationArchive.setVersion("1.0.0"); + doReturn(applicationArchive).when(listener).createApplicationArchive(resource1); doReturn(false).when(listener).isPlatformFirstInitialization(); doReturn("1.0.0").when(applicationVersionService).retrieveApplicationVersion(); //when @@ -220,7 +192,7 @@ void should_update_conf_if_detected_version_equal_to_deployed_version() //then InOrder inOrder = inOrder(defaultLivingApplicationImporter, applicationInstaller); - inOrder.verify(applicationInstaller, never()).update(any(), any()); + inOrder.verify(applicationInstaller, never()).update(any()); verify(listener, times(1)).findAndUpdateConfiguration(); verify(defaultLivingApplicationImporter, never()).execute(); } @@ -232,7 +204,9 @@ void should_throw_an_exception_if_detected_version_inferior_to_deployed_version( Resource resource1 = mockResource("resource1", true, true, 0L); doReturn(resource1).when(listener).detectCustomApplication(); - doReturn(Optional.of(new Semver("0.0.9-SNAPSHOT"))).when(listener).readApplicationVersion(resource1); + var applicationArchive = new ApplicationArchive(); + applicationArchive.setVersion("0.0.9-SNAPSHOT"); + doReturn(applicationArchive).when(listener).createApplicationArchive(resource1); doReturn(false).when(listener).isPlatformFirstInitialization(); doReturn("1.0.0").when(applicationVersionService).retrieveApplicationVersion(); //when @@ -255,32 +229,10 @@ void should_install_default_applications_if_no_custom_app_detected() listener.autoDeployDetectedCustomApplication(any()); //then - verify(applicationInstaller, never()).install(any(), eq("1.0.0")); + verify(applicationInstaller, never()).install(any()); verify(defaultLivingApplicationImporter).execute(); } - @Test - void should_read_the_application_version() throws Exception { - ResourcePatternResolver cpResourceResolver = new PathMatchingResourcePatternResolver( - CustomOrDefaultApplicationInstallerTest.class.getClassLoader()); - - // load first zip, version should be 1.0.0 - Resource archive = cpResourceResolver.getResource("/customer-application.zip"); - var version = listener.readApplicationVersion(archive); - - assertThat(version).hasValue(new Semver("1.0.0")); - - // load second zip, version should be 1.0.1 - archive = cpResourceResolver.getResource("/customer-application-v2.zip"); - version = listener.readApplicationVersion(archive); - assertThat(version).hasValue(new Semver("1.0.1")); - - // load empty zip, expect gracious response of underlying method - archive = cpResourceResolver.getResource("/empty-customer-application.zip"); - version = listener.readApplicationVersion(archive); - assertThat(version).isEmpty(); - } - @Test void should_raise_exception_if_more_than_one_configuration_file() throws Exception { //given diff --git a/bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/api/impl/application/installer/detector/ArtifactTypeDetectorTest.java b/bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/api/impl/application/installer/detector/ArtifactTypeDetectorTest.java new file mode 100644 index 00000000000..f12c54c0b00 --- /dev/null +++ b/bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/api/impl/application/installer/detector/ArtifactTypeDetectorTest.java @@ -0,0 +1,56 @@ +/** + * Copyright (C) 2023 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.api.impl.application.installer.detector; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ArtifactTypeDetectorTest { + + @Spy + @InjectMocks + private ArtifactTypeDetector detector; + + @Test + void readApplicationPropertiesVersion(@TempDir Path tempFolder) throws IOException { + var applicationPropertyFile = tempFolder.resolve("application.properties"); + Files.writeString(applicationPropertyFile, "version=1.0.0"); + + var version = detector.readVersion(applicationPropertyFile.toFile()); + + assertThat(version).isEqualTo("1.0.0"); + } + + @Test + void readMissingApplicationPropertiesVersion(@TempDir Path tempFolder) throws IOException { + var applicationPropertyFile = tempFolder.resolve("application.properties"); + Files.writeString(applicationPropertyFile, ""); + + var version = detector.readVersion(applicationPropertyFile.toFile()); + + assertThat(version).isNull(); + } + +} diff --git a/services/bonita-business-data/bonita-business-data-api/src/main/java/org/bonitasoft/engine/business/data/BusinessDataModelRepository.java b/services/bonita-business-data/bonita-business-data-api/src/main/java/org/bonitasoft/engine/business/data/BusinessDataModelRepository.java index d1e6d71c9b6..df454e84edf 100644 --- a/services/bonita-business-data/bonita-business-data-api/src/main/java/org/bonitasoft/engine/business/data/BusinessDataModelRepository.java +++ b/services/bonita-business-data/bonita-business-data-api/src/main/java/org/bonitasoft/engine/business/data/BusinessDataModelRepository.java @@ -25,8 +25,6 @@ public interface BusinessDataModelRepository { * * @param bdmArchive * the Business Data Model, as a jar containing the Business Object classes to deploy. - * @param tenantId - * the ID of the tenant to deploy the Business Data Model to. * @param userId the ID of the user installing the BDM, typically tenant admin (id=-1) * @return the version of the BDM just deployed. * @throws SBusinessDataRepositoryDeploymentException @@ -75,4 +73,16 @@ String install(byte[] bdmArchive, long userId) * if the BDM cannot be retrieved. */ BusinessObjectModel getBusinessObjectModel() throws SBusinessDataRepositoryException; + + /** + * Determine if the given BDM archive is equivalent to the currently deployed BDM. + * + * @param bdmArchive + * the Business Data Model, as a zip containing the Business Object Model. + * @return true If the given BDM archive is the same as the one already deployed. False otherwise. + * @throws InvalidBusinessDataModelException if the given BDM archive is invalid + * @throws SBusinessDataRepositoryDeploymentException if the server jar generation of the given BDM fails + */ + boolean isDeployed(byte[] bdmArchive) + throws InvalidBusinessDataModelException, SBusinessDataRepositoryDeploymentException; } diff --git a/services/bonita-business-data/bonita-business-data-generator/src/main/java/org/bonitasoft/engine/business/data/generator/AbstractBDMCodeGenerator.java b/services/bonita-business-data/bonita-business-data-generator/src/main/java/org/bonitasoft/engine/business/data/generator/AbstractBDMCodeGenerator.java index 6ad85f9563e..7786a849db7 100644 --- a/services/bonita-business-data/bonita-business-data-generator/src/main/java/org/bonitasoft/engine/business/data/generator/AbstractBDMCodeGenerator.java +++ b/services/bonita-business-data/bonita-business-data-generator/src/main/java/org/bonitasoft/engine/business/data/generator/AbstractBDMCodeGenerator.java @@ -52,10 +52,16 @@ public abstract class AbstractBDMCodeGenerator extends CodeGenerator { private static final String NEW_INSTANCE_METHOD_NAME = "newInstance"; - public AbstractBDMCodeGenerator() { + protected AbstractBDMCodeGenerator() { super(); } + @Override + public AbstractBDMCodeGenerator disableRuntimeClassesValidation() { + super.disableRuntimeClassesValidation(); + return this; + } + public void generateBom(final BusinessObjectModel bom, final File destDir) throws IOException, JClassAlreadyExistsException, BusinessObjectModelValidationException, ClassNotFoundException { diff --git a/services/bonita-business-data/bonita-business-data-generator/src/main/java/org/bonitasoft/engine/business/data/generator/CodeGenerator.java b/services/bonita-business-data/bonita-business-data-generator/src/main/java/org/bonitasoft/engine/business/data/generator/CodeGenerator.java index b9b2ec08f11..f4fda8bbdf0 100644 --- a/services/bonita-business-data/bonita-business-data-generator/src/main/java/org/bonitasoft/engine/business/data/generator/CodeGenerator.java +++ b/services/bonita-business-data/bonita-business-data-generator/src/main/java/org/bonitasoft/engine/business/data/generator/CodeGenerator.java @@ -20,6 +20,9 @@ import java.lang.annotation.Annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Target; +import java.lang.management.ManagementFactory; +import java.net.URL; +import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -58,36 +61,74 @@ public class CodeGenerator { private final JCodeModel model; + protected boolean shouldValidateRuntimeClasses = true; public CodeGenerator() { model = new JCodeModel(); } + public CodeGenerator disableRuntimeClassesValidation() { + this.shouldValidateRuntimeClasses = false; + return this; + } + public void generate(final File destDir) throws IOException { try (PrintStream stream = new PrintStream(new NullStream())) { model.build(destDir, stream); } } - public JDefinedClass addClass(final String fullyqualifiedName) throws JClassAlreadyExistsException { - if (fullyqualifiedName == null || fullyqualifiedName.isEmpty()) { - throw new IllegalArgumentException("Classname cannot cannot be null or empty"); + public JDefinedClass addClass(final String fullyQualifiedName) throws JClassAlreadyExistsException { + if (fullyQualifiedName == null || fullyQualifiedName.isEmpty()) { + throw new IllegalArgumentException("Classname cannot be null or empty"); + } + if (!SourceVersion.isName(fullyQualifiedName)) { + throw new IllegalArgumentException("Classname " + fullyQualifiedName + " is not a valid qualified name"); + } + if (shouldValidateRuntimeClasses) { + validateClassNotExistsInRuntime(fullyQualifiedName); } - if (!SourceVersion.isName(fullyqualifiedName)) { - throw new IllegalArgumentException("Classname " + fullyqualifiedName + " is not a valid qualified name"); + return model._class(fullyQualifiedName); + } + + private void validateClassNotExistsInRuntime(final String qualifiedName) { + final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + try { + var clazz = contextClassLoader.loadClass(qualifiedName); + // Here the class is found, which is NOT normal! Let's investigate: + final StringBuilder message = new StringBuilder( + "Class " + qualifiedName + " already exists in target runtime environment"); + final ClassLoader classLoader = clazz.getClassLoader(); + if (classLoader != null) { + if (classLoader instanceof URLClassLoader) { + for (URL url : ((URLClassLoader) classLoader).getURLs()) { + message.append("\n").append(url.toString()); + } + } else { + message.append("\nCurrent classloader is NOT an URLClassLoader: ").append(classLoader.toString()); + } + } + message.append("\nCurrent JVM Id where the class is found: ") + .append(ManagementFactory.getRuntimeMXBean().getName()); + message.append( + "\nMake sure you did not manually add the jar files bdm-model.jar / bdm-dao.jar somewhere on the classpath."); + message.append( + "\nThose jar files are handled by Bonita internally and should not be manipulated outside Bonita."); + throw new IllegalArgumentException(message.toString()); + } catch (final ClassNotFoundException ignored) { + // here is the normal behaviour } - return model._class(fullyqualifiedName); } - public JDefinedClass addInterface(final JDefinedClass definedClass, final String fullyqualifiedName) { - return definedClass._implements(model.ref(fullyqualifiedName)); + public JDefinedClass addInterface(final JDefinedClass definedClass, final String fullyQualifiedName) { + return definedClass._implements(model.ref(fullyQualifiedName)); } - public JDefinedClass addInterface(final String fullyqualifiedName) throws JClassAlreadyExistsException { - if (!fullyqualifiedName.contains(".")) { - return model.rootPackage()._class(JMod.PUBLIC, fullyqualifiedName, ClassType.INTERFACE); + public JDefinedClass addInterface(final String fullyQualifiedName) throws JClassAlreadyExistsException { + if (!fullyQualifiedName.contains(".")) { + return model.rootPackage()._class(JMod.PUBLIC, fullyQualifiedName, ClassType.INTERFACE); } - return model._class(fullyqualifiedName, ClassType.INTERFACE); + return model._class(fullyQualifiedName, ClassType.INTERFACE); } public JFieldVar addField(final JDefinedClass definedClass, final String fieldName, final Class type) { diff --git a/services/bonita-business-data/bonita-business-data-generator/src/main/java/org/bonitasoft/engine/business/data/generator/EntityCodeGenerator.java b/services/bonita-business-data/bonita-business-data-generator/src/main/java/org/bonitasoft/engine/business/data/generator/EntityCodeGenerator.java index 1474b954510..96c0eb8ad28 100644 --- a/services/bonita-business-data/bonita-business-data-generator/src/main/java/org/bonitasoft/engine/business/data/generator/EntityCodeGenerator.java +++ b/services/bonita-business-data/bonita-business-data-generator/src/main/java/org/bonitasoft/engine/business/data/generator/EntityCodeGenerator.java @@ -13,9 +13,6 @@ **/ package org.bonitasoft.engine.business.data.generator; -import java.lang.management.ManagementFactory; -import java.net.URL; -import java.net.URLClassLoader; import java.util.List; import javax.persistence.*; @@ -65,7 +62,6 @@ public EntityCodeGenerator(final CodeGenerator codeGenerator, final BusinessObje public JDefinedClass addEntity(final BusinessObject bo) throws JClassAlreadyExistsException { final String qualifiedName = bo.getQualifiedName(); - validateClassNotExistsInRuntime(qualifiedName); JDefinedClass entityClass = codeGenerator.addClass(qualifiedName); entityClass = codeGenerator.addInterface(entityClass, org.bonitasoft.engine.bdm.Entity.class.getName()); @@ -177,35 +173,6 @@ private void addNamedQuery(final JDefinedClass entityClass, final JAnnotationArr nameQueryAnnotation.param("query", content); } - private void validateClassNotExistsInRuntime(final String qualifiedName) { - final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); - try { - Class clazz = contextClassLoader.loadClass(qualifiedName); - // Here the class is found, which is NOT normal! Let's investigate: - final StringBuilder message = new StringBuilder( - "Class " + qualifiedName + " already exists in target runtime environment"); - final ClassLoader classLoader = clazz.getClassLoader(); - if (classLoader != null) { - if (classLoader instanceof URLClassLoader) { - for (URL url : ((URLClassLoader) classLoader).getURLs()) { - message.append("\n").append(url.toString()); - } - } else { - message.append("\nCurrent classloader is NOT an URLClassLoader: ").append(classLoader.toString()); - } - } - message.append("\nCurrent JVM Id where the class is found: ") - .append(ManagementFactory.getRuntimeMXBean().getName()); - message.append( - "\nMake sure you did not manually add the jar files bdm-model.jar / bdm-dao.jar somewhere on the classpath."); - message.append( - "\nThose jar files are handled by Bonita internally and should not be manipulated outside Bonita."); - throw new IllegalArgumentException(message.toString()); - } catch (final ClassNotFoundException ignored) { - // here is the normal behaviour - } - } - public void addPersistenceIdFieldAndAccessors(final JDefinedClass entityClass, String dbVendor) { final JFieldVar idFieldVar = codeGenerator.addField(entityClass, Field.PERSISTENCE_ID, codeGenerator.toJavaClass(FieldType.LONG)); diff --git a/services/bonita-business-data/bonita-business-data-generator/src/main/java/org/bonitasoft/engine/business/data/generator/server/ServerBDMJarBuilder.java b/services/bonita-business-data/bonita-business-data-generator/src/main/java/org/bonitasoft/engine/business/data/generator/server/ServerBDMJarBuilder.java index 4c84fdadf32..11cdcfc3e08 100644 --- a/services/bonita-business-data/bonita-business-data-generator/src/main/java/org/bonitasoft/engine/business/data/generator/server/ServerBDMJarBuilder.java +++ b/services/bonita-business-data/bonita-business-data-generator/src/main/java/org/bonitasoft/engine/business/data/generator/server/ServerBDMJarBuilder.java @@ -14,11 +14,11 @@ package org.bonitasoft.engine.business.data.generator.server; import java.io.File; -import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import org.bonitasoft.engine.bdm.BusinessObjectModelConverter; import org.bonitasoft.engine.bdm.model.BusinessObject; import org.bonitasoft.engine.bdm.model.BusinessObjectModel; import org.bonitasoft.engine.business.data.generator.AbstractBDMJarBuilder; @@ -39,8 +39,8 @@ public ServerBDMJarBuilder(final JDTCompiler compiler) { super(new ServerBDMCodeGenerator(), compiler); } - public ServerBDMJarBuilder() { - super(new ServerBDMCodeGenerator()); + public ServerBDMJarBuilder(ServerBDMCodeGenerator generator) { + super(generator); } @Override @@ -72,9 +72,7 @@ protected void addPersistenceFile(final File directory, final BusinessObjectMode private void addBOMFile(final File directory, final BusinessObjectModel bom) throws CodeGenerationException { Path file = new File(directory, "bom.xml").toPath(); try { - final URL resource = BusinessObjectModel.class.getResource("/bom.xsd"); - final byte[] bomXML = IOUtils.marshallObjectToXML(bom, resource); - Files.write(file, bomXML); + Files.write(file, new BusinessObjectModelConverter().marshall(bom)); } catch (Exception e) { throw new CodeGenerationException("Error when adding business object model metadata to server jar", e); } diff --git a/services/bonita-business-data/bonita-business-data-generator/src/test/java/org/bonitasoft/engine/business/data/generator/ForeignKeyAnnotatorTest.java b/services/bonita-business-data/bonita-business-data-generator/src/test/java/org/bonitasoft/engine/business/data/generator/ForeignKeyAnnotatorTest.java index 79c52c0f2ad..3786f3bdda8 100644 --- a/services/bonita-business-data/bonita-business-data-generator/src/test/java/org/bonitasoft/engine/business/data/generator/ForeignKeyAnnotatorTest.java +++ b/services/bonita-business-data/bonita-business-data-generator/src/test/java/org/bonitasoft/engine/business/data/generator/ForeignKeyAnnotatorTest.java @@ -61,7 +61,7 @@ public class ForeignKeyAnnotatorTest { @Before public void before() throws Exception { - codeGenerator = new ClientBDMCodeGenerator(); + codeGenerator = new ClientBDMCodeGenerator().disableRuntimeClassesValidation(); foreignKeyAnnotator = new ForeignKeyAnnotator(); jDefinedClass = codeGenerator.addClass(EntityPojo.class.getName()); jFieldVar = codeGenerator.addField(jDefinedClass, "fieldName", EntityPojo.class); diff --git a/services/bonita-business-data/bonita-business-data-generator/src/test/java/org/bonitasoft/engine/business/data/generator/server/ServerBDMJarBuilderITest.java b/services/bonita-business-data/bonita-business-data-generator/src/test/java/org/bonitasoft/engine/business/data/generator/server/ServerBDMJarBuilderITest.java index 7215ecc4d6d..5456c6d4714 100644 --- a/services/bonita-business-data/bonita-business-data-generator/src/test/java/org/bonitasoft/engine/business/data/generator/server/ServerBDMJarBuilderITest.java +++ b/services/bonita-business-data/bonita-business-data-generator/src/test/java/org/bonitasoft/engine/business/data/generator/server/ServerBDMJarBuilderITest.java @@ -14,35 +14,38 @@ package org.bonitasoft.engine.business.data.generator.server; import org.apache.commons.io.filefilter.TrueFileFilter; -import org.bonitasoft.engine.business.data.generator.AbstractBDMJarBuilder; import org.bonitasoft.engine.business.data.generator.BOMBuilder; +import org.junit.Before; import org.junit.Test; public class ServerBDMJarBuilderITest { + private ServerBDMJarBuilder bdmJarBuilder; + + @Before + public void setup() { + bdmJarBuilder = new ServerBDMJarBuilder(new ServerBDMCodeGenerator()); + } + /* Just to test we have no errors in full chain. Must be improved */ @Test public void jar_builder_should_go_well_without_errors() throws Exception { - final AbstractBDMJarBuilder bdmJarBuilder = new ServerBDMJarBuilder(); bdmJarBuilder.build(BOMBuilder.aBOM().build(), TrueFileFilter.TRUE); } @Test public void jar_builder_should_go_well_without_errors_with_queries() throws Exception { - final AbstractBDMJarBuilder bdmJarBuilder = new ServerBDMJarBuilder(); final BOMBuilder builder = new BOMBuilder(); bdmJarBuilder.build(builder.buildComplex(), TrueFileFilter.TRUE); } @Test public void jar_builder_should_go_well_without_errors_with_queries2() throws Exception { - final AbstractBDMJarBuilder bdmJarBuilder = new ServerBDMJarBuilder(); bdmJarBuilder.build(BOMBuilder.aBOM().buildPerson(), TrueFileFilter.TRUE); } @Test public void jar_builder_should_go_well_with_multipleBoolean() throws Exception { - final AbstractBDMJarBuilder bdmJarBuilder = new ServerBDMJarBuilder(); final BOMBuilder builder = new BOMBuilder(); bdmJarBuilder.build(builder.buildModelWithMultipleBoolean(), TrueFileFilter.TRUE); } diff --git a/services/bonita-business-data/bonita-business-data-generator/src/test/java/org/bonitasoft/engine/business/data/generator/server/ServerBDMJarBuilderTest.java b/services/bonita-business-data/bonita-business-data-generator/src/test/java/org/bonitasoft/engine/business/data/generator/server/ServerBDMJarBuilderTest.java index f70f3c18e1d..f676656f36a 100644 --- a/services/bonita-business-data/bonita-business-data-generator/src/test/java/org/bonitasoft/engine/business/data/generator/server/ServerBDMJarBuilderTest.java +++ b/services/bonita-business-data/bonita-business-data-generator/src/test/java/org/bonitasoft/engine/business/data/generator/server/ServerBDMJarBuilderTest.java @@ -46,7 +46,7 @@ public void setUp() throws Exception { @Test public void should_addPersistenceUnittestGetPersistenceFileContentFor() throws Exception { - final ServerBDMJarBuilder builder = spy(new ServerBDMJarBuilder()); + final ServerBDMJarBuilder builder = spy(new ServerBDMJarBuilder(new ServerBDMCodeGenerator())); builder.addPersistenceFile(directory, bom); diff --git a/services/bonita-business-data/bonita-business-data-impl/build.gradle b/services/bonita-business-data/bonita-business-data-impl/build.gradle index 28096445439..5f4e0ac229e 100644 --- a/services/bonita-business-data/bonita-business-data-impl/build.gradle +++ b/services/bonita-business-data/bonita-business-data-impl/build.gradle @@ -14,6 +14,9 @@ dependencies { api "org.apache.commons:commons-lang3:${Deps.commonsLangVersion}" api project(':services:bonita-classloader') runtimeOnly "javax.xml.bind:jaxb-api:${Deps.jaxbVersion}" + annotationProcessor "org.projectlombok:lombok:${Deps.lombokVersion}" + compileOnly "org.projectlombok:lombok:${Deps.lombokVersion}" + testImplementation "junit:junit:${Deps.junit4Version}" testImplementation "org.assertj:assertj-core:${Deps.assertjVersion}" testImplementation "net.javacrumbs.json-unit:json-unit-fluent:${Deps.jsonUnitVersion}" diff --git a/services/bonita-business-data/bonita-business-data-impl/src/main/java/org/bonitasoft/engine/business/data/impl/BusinessDataModelRepositoryImpl.java b/services/bonita-business-data/bonita-business-data-impl/src/main/java/org/bonitasoft/engine/business/data/impl/BusinessDataModelRepositoryImpl.java index c8a8d8f9a94..d4315701589 100644 --- a/services/bonita-business-data/bonita-business-data-impl/src/main/java/org/bonitasoft/engine/business/data/impl/BusinessDataModelRepositoryImpl.java +++ b/services/bonita-business-data/bonita-business-data-impl/src/main/java/org/bonitasoft/engine/business/data/impl/BusinessDataModelRepositoryImpl.java @@ -19,32 +19,27 @@ import java.io.IOException; import java.io.InputStream; import java.net.URL; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; +import java.security.MessageDigest; +import java.util.*; import java.util.stream.Collectors; import javax.xml.bind.JAXBException; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.filefilter.IOFileFilter; import org.apache.commons.io.filefilter.SuffixFileFilter; import org.apache.commons.lang3.exception.ExceptionUtils; import org.bonitasoft.engine.bdm.BusinessObjectModelConverter; import org.bonitasoft.engine.bdm.model.BusinessObjectModel; -import org.bonitasoft.engine.business.data.BusinessDataModelRepository; -import org.bonitasoft.engine.business.data.InvalidBusinessDataModelException; -import org.bonitasoft.engine.business.data.SBusinessDataRepositoryDeploymentException; -import org.bonitasoft.engine.business.data.SBusinessDataRepositoryException; -import org.bonitasoft.engine.business.data.SchemaManager; +import org.bonitasoft.engine.business.data.*; import org.bonitasoft.engine.business.data.generator.AbstractBDMJarBuilder; import org.bonitasoft.engine.business.data.generator.BDMJarGenerationException; import org.bonitasoft.engine.business.data.generator.client.ClientBDMJarBuilder; import org.bonitasoft.engine.business.data.generator.client.ResourcesLoader; import org.bonitasoft.engine.business.data.generator.filter.OnlyDAOImplementationFileFilter; import org.bonitasoft.engine.business.data.generator.filter.WithoutDAOImplementationFileFilter; +import org.bonitasoft.engine.business.data.generator.server.ServerBDMCodeGenerator; import org.bonitasoft.engine.business.data.generator.server.ServerBDMJarBuilder; import org.bonitasoft.engine.classloader.ClassLoaderIdentifier; import org.bonitasoft.engine.classloader.ClassLoaderService; @@ -57,16 +52,21 @@ import org.bonitasoft.engine.dependency.model.ScopeType; import org.bonitasoft.engine.io.IOUtils; import org.bonitasoft.engine.persistence.SBonitaReadException; +import org.bonitasoft.engine.platform.PlatformService; import org.bonitasoft.engine.recorder.SRecorderException; import org.bonitasoft.engine.resources.STenantResource; import org.bonitasoft.engine.resources.STenantResourceLight; import org.bonitasoft.engine.resources.TenantResourceType; import org.bonitasoft.engine.resources.TenantResourcesService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; import org.xml.sax.SAXException; /** * @author Colin PUY */ +@Slf4j +@Service("businessDataModelRepository") public class BusinessDataModelRepositoryImpl implements BusinessDataModelRepository { private static final String BDR_DEPENDENCY_NAME = "BDR"; @@ -83,12 +83,15 @@ public class BusinessDataModelRepositoryImpl implements BusinessDataModelReposit private final ClassLoaderService classLoaderService; private final SchemaManager schemaManager; - private TenantResourcesService tenantResourcesService; - private long tenantId; + private final TenantResourcesService tenantResourcesService; + private final long tenantId; + private final PlatformService platformService; - public BusinessDataModelRepositoryImpl(final DependencyService dependencyService, + public BusinessDataModelRepositoryImpl(final PlatformService platformService, + final DependencyService dependencyService, ClassLoaderService classLoaderService, final SchemaManager schemaManager, - TenantResourcesService tenantResourcesService, long tenantId) { + TenantResourcesService tenantResourcesService, @Value("${tenantId}") long tenantId) { + this.platformService = platformService; this.dependencyService = dependencyService; this.classLoaderService = classLoaderService; this.schemaManager = schemaManager; @@ -239,9 +242,23 @@ protected BusinessObjectModel getBusinessObjectModel(final byte[] bdmZip) protected byte[] generateServerBDMJar(final BusinessObjectModel model) throws SBusinessDataRepositoryDeploymentException { - final AbstractBDMJarBuilder builder = new ServerBDMJarBuilder(); + return generateServerBDMJar(model, true); + } + + protected byte[] generateServerBDMJar(final BusinessObjectModel model, boolean validateRuntimeClasses) + throws SBusinessDataRepositoryDeploymentException { + var generator = new ServerBDMCodeGenerator(); + if (!validateRuntimeClasses) { + generator.disableRuntimeClassesValidation(); + } + final AbstractBDMJarBuilder builder = new ServerBDMJarBuilder(generator); final IOFileFilter classFileAndXmlFileFilter = new SuffixFileFilter(Arrays.asList(".class", ".xml")); try { + // Force productVersion to current platform version. + // The bom.xml file stored in the generated jar will have the proper product version. + // Thus, when comparing these files, a BDM update will be forced if the platform version has changed + // between the existing server jar and the new one. + model.setProductVersion(platformService.getSPlatformProperties().getPlatformVersion()); return builder.build(model, classFileAndXmlFileFilter); } catch (BDMJarGenerationException e) { throw new SBusinessDataRepositoryDeploymentException(e); @@ -334,4 +351,42 @@ public void dropAndUninstall(final long tenantId) throws SBusinessDataRepository } } + @Override + public boolean isDeployed(byte[] bdmArchive) + throws InvalidBusinessDataModelException, SBusinessDataRepositoryDeploymentException { + try { + var bdmDependencyId = dependencyService + .getIdOfDependencyOfArtifact(tenantId, ScopeType.TENANT, BDR_DEPENDENCY_FILENAME); + if (bdmDependencyId.isEmpty()) { + log.debug("No BDM currently deployed."); + return false; + } + var existingSha3 = sha256ToHex(dependencyService.getDependency(bdmDependencyId.get()).getValue()); + var newServerJar = generateServerBDMJar(getBusinessObjectModel(bdmArchive), false); + var newSha3 = sha256ToHex(newServerJar); + log.debug("BDM binary sha3256 comparison: current={}, new={}", existingSha3, newSha3); + return Objects.equals(existingSha3, newSha3); + } catch (SDependencyNotFoundException e) { + log.error("Failed to retrieve existing bdm dependency", e); + return false; + } catch (SBonitaReadException e) { + log.error("Failed to retrieve {} dependency", BDR_DEPENDENCY_FILENAME, e); + return false; + } + } + + @SneakyThrows + private static String sha256ToHex(byte[] source) { + final MessageDigest digest = MessageDigest.getInstance("SHA3-256"); + return bytesToHex(digest.digest(source)); + } + + private static String bytesToHex(byte[] hash) { + StringBuilder hexString = new StringBuilder(2 * hash.length); + for (byte b : hash) { + hexString.append(String.format("%02x", b)); + } + return hexString.toString(); + } + } diff --git a/services/bonita-business-data/bonita-business-data-impl/src/test/java/org/bonitasoft/engine/business/data/impl/BusinessDataModelRepositoryImplTest.java b/services/bonita-business-data/bonita-business-data-impl/src/test/java/org/bonitasoft/engine/business/data/impl/BusinessDataModelRepositoryImplTest.java index dee2f50148a..c5806606aa5 100644 --- a/services/bonita-business-data/bonita-business-data-impl/src/test/java/org/bonitasoft/engine/business/data/impl/BusinessDataModelRepositoryImplTest.java +++ b/services/bonita-business-data/bonita-business-data-impl/src/test/java/org/bonitasoft/engine/business/data/impl/BusinessDataModelRepositoryImplTest.java @@ -19,6 +19,7 @@ import static org.bonitasoft.engine.commons.io.IOUtil.zip; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; +import static org.mockito.Mockito.doReturn; import java.io.InputStream; import java.sql.SQLException; @@ -42,6 +43,8 @@ import org.bonitasoft.engine.dependency.model.SDependency; import org.bonitasoft.engine.dependency.model.ScopeType; import org.bonitasoft.engine.io.IOUtil; +import org.bonitasoft.engine.platform.PlatformService; +import org.bonitasoft.engine.platform.model.SPlatformProperties; import org.bonitasoft.engine.resources.TenantResourceType; import org.bonitasoft.engine.resources.TenantResourcesService; import org.hibernate.tool.schema.spi.CommandAcceptanceException; @@ -73,12 +76,19 @@ public class BusinessDataModelRepositoryImplTest { @Mock private SchemaManagerUpdate schemaManager; + @Mock + private PlatformService platformService; + + @Mock + private SPlatformProperties platformProperties; + private BusinessDataModelRepositoryImpl businessDataModelRepository; @Before public void setUp() { - schemaManager = mock(SchemaManagerUpdate.class); - businessDataModelRepository = spy(new BusinessDataModelRepositoryImpl(dependencyService, + doReturn(platformProperties).when(platformService).getSPlatformProperties(); + doReturn("1.0").when(platformProperties).getPlatformVersion(); + businessDataModelRepository = spy(new BusinessDataModelRepositoryImpl(platformService, dependencyService, classLoaderService, schemaManager, tenantResourcesService, TENANT_ID)); } @@ -276,4 +286,62 @@ public void convertExceptions_should_filter_out_empty_message_lines() { assertThat(Arrays.asList(message.split("\n"))).doesNotContain(""); } + @Test + public void isDeployedComparesBdmWithSameContent() throws Exception { + // given: + var bdmArchive = "fake archive content".getBytes(); + var generatedJarContent = "fake jar content".getBytes(); + when(dependencyService.getIdOfDependencyOfArtifact(TENANT_ID, ScopeType.TENANT, + BusinessDataModelRepositoryImpl.BDR_DEPENDENCY_FILENAME)) + .thenReturn(Optional.of(1L)); + var deployedBdm = new SDependency(); + deployedBdm.setValue_(generatedJarContent); + when(dependencyService.getDependency(1L)).thenReturn(deployedBdm); + doReturn(null).when(businessDataModelRepository).getBusinessObjectModel(bdmArchive); + doReturn(generatedJarContent).when(businessDataModelRepository).generateServerBDMJar(any(), eq(false)); + + // when: + var isDeployed = businessDataModelRepository.isDeployed(bdmArchive); + + // then: + assertThat(isDeployed).isTrue(); + } + + @Test + public void isDeployedComparesBdmWithDifferentContent() throws Exception { + // given: + var bdmArchive = "fake archive content".getBytes(); + var existingJarContent = "fake existing jar content".getBytes(); + var generatedJarContent = "fake jar content".getBytes(); + when(dependencyService.getIdOfDependencyOfArtifact(TENANT_ID, ScopeType.TENANT, + BusinessDataModelRepositoryImpl.BDR_DEPENDENCY_FILENAME)) + .thenReturn(Optional.of(1L)); + var deployedBdm = new SDependency(); + deployedBdm.setValue_(existingJarContent); + when(dependencyService.getDependency(1L)).thenReturn(deployedBdm); + doReturn(null).when(businessDataModelRepository).getBusinessObjectModel(bdmArchive); + doReturn(generatedJarContent).when(businessDataModelRepository).generateServerBDMJar(any(), eq(false)); + + // when: + var isDeployed = businessDataModelRepository.isDeployed(bdmArchive); + + // then: + assertThat(isDeployed).isFalse(); + } + + @Test + public void isDeployedWithoutBdmDeployed() throws Exception { + // given: + var bdmArchive = "fake archive content".getBytes(); + when(dependencyService.getIdOfDependencyOfArtifact(TENANT_ID, ScopeType.TENANT, + BusinessDataModelRepositoryImpl.BDR_DEPENDENCY_FILENAME)) + .thenReturn(Optional.empty()); + + // when: + var isDeployed = businessDataModelRepository.isDeployed(bdmArchive); + + // then: + assertThat(isDeployed).isFalse(); + } + } diff --git a/services/bonita-business-data/bonita-business-data-impl/src/test/java/org/bonitasoft/engine/business/data/impl/ConcurrencyTest.java b/services/bonita-business-data/bonita-business-data-impl/src/test/java/org/bonitasoft/engine/business/data/impl/ConcurrencyTest.java index a6c436d2a4b..d407e1a42d4 100644 --- a/services/bonita-business-data/bonita-business-data-impl/src/test/java/org/bonitasoft/engine/business/data/impl/ConcurrencyTest.java +++ b/services/bonita-business-data/bonita-business-data-impl/src/test/java/org/bonitasoft/engine/business/data/impl/ConcurrencyTest.java @@ -31,6 +31,7 @@ import org.bonitasoft.engine.business.data.JpaTestConfiguration; import org.bonitasoft.engine.classloader.ClassLoaderService; import org.bonitasoft.engine.dependency.DependencyService; +import org.bonitasoft.engine.platform.PlatformService; import org.bonitasoft.engine.resources.TenantResourcesService; import org.bonitasoft.engine.transaction.UserTransactionService; import org.junit.After; @@ -74,9 +75,10 @@ public void setUp() throws Exception { jdbcTemplate = new JdbcTemplate(datasource); } final SchemaManagerUpdate schemaManager = new SchemaManagerUpdate(configuration.getJpaModelConfiguration()); - final BusinessDataModelRepositoryImpl businessDataModelRepositoryImpl = spy(new BusinessDataModelRepositoryImpl( - mock(DependencyService.class), - classLoaderService, schemaManager, mock(TenantResourcesService.class), TENANT_ID)); + final BusinessDataModelRepositoryImpl businessDataModelRepositoryImpl = spy( + new BusinessDataModelRepositoryImpl(mock(PlatformService.class), + mock(DependencyService.class), + classLoaderService, schemaManager, mock(TenantResourcesService.class), TENANT_ID)); final UserTransactionService transactionService = mock(UserTransactionService.class); businessDataRepository = spy( new JPABusinessDataRepositoryImpl(transactionService, businessDataModelRepositoryImpl, diff --git a/services/bonita-business-data/bonita-business-data-impl/src/test/java/org/bonitasoft/engine/business/data/impl/JPABusinessDataRepositoryImplITest.java b/services/bonita-business-data/bonita-business-data-impl/src/test/java/org/bonitasoft/engine/business/data/impl/JPABusinessDataRepositoryImplITest.java index 7dd9598f9a1..319b5aa5d3f 100644 --- a/services/bonita-business-data/bonita-business-data-impl/src/test/java/org/bonitasoft/engine/business/data/impl/JPABusinessDataRepositoryImplITest.java +++ b/services/bonita-business-data/bonita-business-data-impl/src/test/java/org/bonitasoft/engine/business/data/impl/JPABusinessDataRepositoryImplITest.java @@ -39,6 +39,7 @@ import org.bonitasoft.engine.business.data.SBusinessDataNotFoundException; import org.bonitasoft.engine.classloader.ClassLoaderService; import org.bonitasoft.engine.dependency.DependencyService; +import org.bonitasoft.engine.platform.PlatformService; import org.bonitasoft.engine.resources.TenantResourcesService; import org.bonitasoft.engine.transaction.UserTransactionService; import org.junit.After; @@ -90,7 +91,7 @@ public void setUp() throws Exception { final SchemaManagerUpdate schemaManager = new SchemaManagerUpdate(configuration.getJpaModelConfiguration()); final BusinessDataModelRepositoryImpl businessDataModelRepositoryImpl = spy( - new BusinessDataModelRepositoryImpl(mock(DependencyService.class), + new BusinessDataModelRepositoryImpl(mock(PlatformService.class), mock(DependencyService.class), classLoaderService, schemaManager, mock(TenantResourcesService.class), TENANT_ID)); businessDataRepository = spy( new JPABusinessDataRepositoryImpl(transactionService, businessDataModelRepositoryImpl, diff --git a/services/bonita-classloader/src/main/java/org/bonitasoft/engine/dependency/model/AbstractSDependency.java b/services/bonita-classloader/src/main/java/org/bonitasoft/engine/dependency/model/AbstractSDependency.java index f615f28ebcb..f54ab01085b 100644 --- a/services/bonita-classloader/src/main/java/org/bonitasoft/engine/dependency/model/AbstractSDependency.java +++ b/services/bonita-classloader/src/main/java/org/bonitasoft/engine/dependency/model/AbstractSDependency.java @@ -16,9 +16,11 @@ import javax.persistence.Id; import javax.persistence.MappedSuperclass; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.Getter; import lombok.NoArgsConstructor; import lombok.ToString; import lombok.experimental.SuperBuilder; @@ -44,9 +46,10 @@ public abstract class AbstractSDependency implements PersistentObject { private String description; @ToString.Exclude @EqualsAndHashCode.Exclude + @Getter(AccessLevel.NONE) private byte[] value_; - public AbstractSDependency(final String name, final String fileName, final byte[] value) { + protected AbstractSDependency(final String name, final String fileName, final byte[] value) { super(); this.name = name; this.fileName = fileName; diff --git a/services/bonita-commons/src/main/java/org/bonitasoft/engine/commons/io/IOUtil.java b/services/bonita-commons/src/main/java/org/bonitasoft/engine/commons/io/IOUtil.java index d41f5806dd5..1adfd5362c1 100644 --- a/services/bonita-commons/src/main/java/org/bonitasoft/engine/commons/io/IOUtil.java +++ b/services/bonita-commons/src/main/java/org/bonitasoft/engine/commons/io/IOUtil.java @@ -32,8 +32,12 @@ import java.math.BigInteger; import java.net.URI; import java.net.URL; +import java.nio.file.attribute.FileTime; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -156,38 +160,34 @@ public static Map getResources(final Class... classes) throws } public static byte[] generateJar(final Map resources) throws IOException { - if (resources == null || resources.size() == 0) { - final String message = "No resources available"; - throw new IOException(message); + if (resources == null || resources.isEmpty()) { + throw new IOException("No resources available"); } - ByteArrayOutputStream baos = null; - JarOutputStream jarOutStream = null; - try { - baos = new ByteArrayOutputStream(); - jarOutStream = new JarOutputStream(new BufferedOutputStream(baos)); + try (var baos = new ByteArrayOutputStream(); + var jarOutStream = new JarOutputStream(new BufferedOutputStream(baos));) { for (final Map.Entry resource : resources.entrySet()) { - jarOutStream.putNextEntry(new JarEntry(resource.getKey())); + var entry = new JarEntry(resource.getKey()); + // Force entry timestamp to an arbitrary date to ease jar content comparison + entry.setCreationTime(FileTime.from(Instant.ofEpochMilli(0))); + entry.setLastModifiedTime(FileTime.from(Instant.ofEpochMilli(0))); + entry.setLastAccessTime(FileTime.from(Instant.ofEpochMilli(0))); + entry.setTimeLocal(LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC)); + entry.setTime(0); + jarOutStream.putNextEntry(entry); jarOutStream.write(resource.getValue()); } jarOutStream.flush(); baos.flush(); - } finally { - if (jarOutStream != null) { - jarOutStream.close(); - } - if (baos != null) { - baos.close(); - } + baos.close(); + jarOutStream.close(); + return baos.toByteArray(); } - - return baos.toByteArray(); } public static byte[] generateZip(final Map resources) throws IOException { - if (resources == null || resources.size() == 0) { - final String message = "No resources available"; - throw new IOException(message); + if (resources == null || resources.isEmpty()) { + throw new IOException("No resources available"); } ByteArrayOutputStream baos = null;