diff --git a/.github/workflows/master-build.yml b/.github/workflows/master-build.yml index 79c87fd6a60..a6400af651b 100644 --- a/.github/workflows/master-build.yml +++ b/.github/workflows/master-build.yml @@ -9,22 +9,16 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' - - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 + distribution: 'temurin' + cache: maven - name: Build run: mvn clean verify -DskipTests=true @@ -36,7 +30,7 @@ jobs: timeout-minutes: 90 services: mongo: - image: mongo:4.4 + image: mongo:6 ports: - 27017:27017 @@ -46,7 +40,7 @@ jobs: - 6379:6379 elasticsearch: - image: elasticsearch:7.17.3 + image: elasticsearch:7.17.28 ports: - 9200:9200 - 9300:9300 @@ -67,15 +61,16 @@ jobs: echo $ELASTIC_SEARCH_URL curl -fsSL "$ELASTIC_SEARCH_URL/_cat/health?h=status" - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' + distribution: 'temurin' + cache: maven # - name: Cache SonarCloud packages # uses: actions/cache@v3 @@ -84,13 +79,6 @@ jobs: # key: ${{ runner.os }}-sonar # restore-keys: ${{ runner.os }}-sonar - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - name: Generate certificates run: cd src/main/resources/certificates && openssl genrsa -out keypair.pem 4096 && openssl rsa -in keypair.pem -pubout -out public.crt && openssl pkcs8 -topk8 -inform PEM -outform DER -nocrypt -in keypair.pem -out private.der && cd ../../../.. @@ -113,23 +101,17 @@ jobs: needs: test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: token: ${{ secrets.PUSH_DOCS }} fetch-depth: 0 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' - - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 + distribution: 'temurin' + cache: maven - name: Build run: mvn clean package install -DskipTests=true @@ -139,7 +121,7 @@ jobs: mvn javadoc:javadoc cp -r ./target/apidocs/* ./docs/javadoc/ - - uses: EndBug/add-and-commit@v8 + - uses: EndBug/add-and-commit@v9 with: add: docs pathspec_error_handling: exitImmediately diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 0005231f061..ff3edb08caa 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -8,22 +8,16 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' - - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 + distribution: 'temurin' + cache: maven - name: Build run: mvn clean verify -DskipTests=true @@ -35,7 +29,7 @@ jobs: timeout-minutes: 200 services: mongo: - image: mongo:4.4 + image: mongo:6 ports: - 27017:27017 @@ -45,7 +39,7 @@ jobs: - 6379:6379 elasticsearch: - image: elasticsearch:7.17.3 + image: elasticsearch:7.17.28 ports: - 9200:9200 - 9300:9300 @@ -66,15 +60,16 @@ jobs: echo $ELASTIC_SEARCH_URL curl -fsSL "$ELASTIC_SEARCH_URL/_cat/health?h=status" - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' + distribution: 'temurin' + cache: maven # - name: Cache SonarCloud packages # uses: actions/cache@v3 @@ -83,13 +78,6 @@ jobs: # key: ${{ runner.os }}-sonar # restore-keys: ${{ runner.os }}-sonar - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - name: Generate certificates run: cd src/main/resources/certificates && openssl genrsa -out keypair.pem 4096 && openssl rsa -in keypair.pem -pubout -out public.crt && openssl pkcs8 -topk8 -inform PEM -outform DER -nocrypt -in keypair.pem -out private.der && cd ../../../.. diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index b5fdc3067e1..64f8d110c21 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -9,27 +9,21 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' + distribution: 'temurin' + cache: maven - name: Get Project Version from pom.xml uses: entimaniac/read-pom-version-action@1.0.0 id: getVersion - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - name: Build run: mvn clean verify -DskipTests=true @@ -71,22 +65,16 @@ jobs: echo $ELASTIC_SEARCH_URL curl -fsSL "$ELASTIC_SEARCH_URL/_cat/health?h=status" - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' - - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 + distribution: 'temurin' + cache: maven - name: Generate certificates run: cd src/main/resources/certificates && openssl genrsa -out keypair.pem 4096 && openssl rsa -in keypair.pem -pubout -out public.crt && openssl pkcs8 -topk8 -inform PEM -outform DER -nocrypt -in keypair.pem -out private.der && cd ../../../.. @@ -108,7 +96,7 @@ jobs: contents: read packages: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - id: install-secret-key name: Install gpg secret key @@ -117,10 +105,11 @@ jobs: gpg --list-secret-keys --keyid-format LONG - name: Set up Maven Central Repository - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' + distribution: 'temurin' + cache: maven server-id: central server-username: MAVEN_USERNAME server-password: MAVEN_PASSWORD @@ -141,26 +130,20 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' - - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 + distribution: 'temurin' + cache: maven - name: Build run: mvn -P docker-build clean package install -DskipTests=true - name: Log in to Docker Hub - uses: docker/login-action@v1 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_AUTH_TOKEN }} @@ -170,14 +153,14 @@ jobs: id: getVersion - name: Push Version ${{ steps.getVersion.outputs.version }} - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v7 with: push: true tags: netgrif/application-engine:${{ steps.getVersion.outputs.version }} - name: Push Latest if: ${{ !contains(steps.getVersion.outputs.version, '-SNAPSHOT') }} - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v7 with: push: true tags: netgrif/application-engine:latest @@ -190,19 +173,13 @@ jobs: contents: read packages: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v5 with: java-version: '11' - distribution: 'adopt' - - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 + distribution: 'temurin' + cache: maven - name: Publish artifact on GitHub Packages run: mvn -B -P github-publish clean deploy -DskipTests @@ -219,7 +196,7 @@ jobs: id-token: write security-events: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - name: Build run: mvn clean package install -DskipTests=true diff --git a/pom.xml b/pom.xml index e57436ea5cc..470b48b79fe 100644 --- a/pom.xml +++ b/pom.xml @@ -91,6 +91,27 @@ + + central + https://repo.maven.apache.org/maven2 + + true + + + false + + + + Central Portal Snapshots + central-portal-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + false + + + true + + jitpack.io https://jitpack.io @@ -315,6 +336,11 @@ batik-all 1.17 + + xml-apis + xml-apis-ext + 1.3.04 + commons-io commons-io diff --git a/src/main/groovy/com/netgrif/application/engine/migration/MigrationOrderedCommandLineRunner.groovy b/src/main/groovy/com/netgrif/application/engine/migration/MigrationOrderedCommandLineRunner.groovy index 8fc940ac528..7c0c52d9bc1 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/MigrationOrderedCommandLineRunner.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/MigrationOrderedCommandLineRunner.groovy @@ -1,6 +1,7 @@ package com.netgrif.application.engine.migration - +import com.netgrif.application.engine.configuration.ApplicationShutdownProvider +import com.netgrif.application.engine.configuration.properties.MigrationProperties import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService import com.netgrif.application.engine.startup.AbstractOrderedCommandLineRunner import org.slf4j.Logger @@ -14,26 +15,55 @@ abstract class MigrationOrderedCommandLineRunner extends AbstractOrderedCommandL private static final Logger log = LoggerFactory.getLogger(MigrationOrderedCommandLineRunner) private String title = this.class.simpleName + private boolean shutdownAfterFinish = false @Autowired private MigrationRepository repository + @Autowired + private MigrationProperties migrationProperties + @Autowired private IPetriNetService service + @Autowired + private ApplicationShutdownProvider shutdownProvider + @Override void run(String... strings) throws Exception { if (repository.existsByTitle(title)) { log.info("Migration ${title} was already applied") return } + if (migrationProperties.getSkip().contains(title)) { + log.info("Migration ${title} is skipped according to property nae.migration.skip") + return + } log.info("Applying migration ${title}") migrate() repository.save(new Migration(title)) - service.evictAllCaches() + if (migrationProperties.isEvictCaches()) { + log.info("Evicting all caches after migration") + service.evictAllCaches() + } log.info("Migration ${title} applied") + if (shutdownAfterFinish || migrationProperties.isShutdownAfterMigration()) { + // sleep is for elastic executor and other things to flush their work after migration. + // the number was chosen arbitrary by feeling 😅 + sleep(333) + shutdownProvider.shutdown(this.class) + } + } + + protected void enableShutdownAfterFinish() { + this.shutdownAfterFinish = true; + } + + protected void disableShutdownAfterFinish() { + this.shutdownAfterFinish = false; } abstract void migrate() -} \ No newline at end of file + +} diff --git a/src/main/groovy/com/netgrif/application/engine/startup/RunnerController.groovy b/src/main/groovy/com/netgrif/application/engine/startup/RunnerController.groovy index f02d30e1c8f..d653c4c4d30 100644 --- a/src/main/groovy/com/netgrif/application/engine/startup/RunnerController.groovy +++ b/src/main/groovy/com/netgrif/application/engine/startup/RunnerController.groovy @@ -43,4 +43,4 @@ class RunnerController { } return runnerOrder } -} \ No newline at end of file +} diff --git a/src/main/java/com/netgrif/application/engine/auth/service/AbstractUserService.java b/src/main/java/com/netgrif/application/engine/auth/service/AbstractUserService.java index 2a3be9d5ed7..d9e68491cf2 100644 --- a/src/main/java/com/netgrif/application/engine/auth/service/AbstractUserService.java +++ b/src/main/java/com/netgrif/application/engine/auth/service/AbstractUserService.java @@ -53,6 +53,11 @@ public void addDefaultAuthorities(IUser user) { } } + @Override + public boolean existsById(String id) { + return repository.existsById(id); + } + @Override public IUser assignAuthority(String userId, String authorityId) { IUser user = resolveById(userId, true); diff --git a/src/main/java/com/netgrif/application/engine/auth/service/interfaces/IUserService.java b/src/main/java/com/netgrif/application/engine/auth/service/interfaces/IUserService.java index 01956bc2b8b..989062f7585 100644 --- a/src/main/java/com/netgrif/application/engine/auth/service/interfaces/IUserService.java +++ b/src/main/java/com/netgrif/application/engine/auth/service/interfaces/IUserService.java @@ -49,6 +49,8 @@ public interface IUserService { void addDefaultAuthorities(IUser user); + boolean existsById(String id); + IUser assignAuthority(String userId, String authorityId); IUser getLoggedOrSystem(); diff --git a/src/main/java/com/netgrif/application/engine/configuration/ApplicationShutdownProvider.java b/src/main/java/com/netgrif/application/engine/configuration/ApplicationShutdownProvider.java new file mode 100644 index 00000000000..db24485e5d9 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/configuration/ApplicationShutdownProvider.java @@ -0,0 +1,39 @@ +package com.netgrif.application.engine.configuration; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ApplicationContext; +import org.springframework.core.task.TaskExecutor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ApplicationShutdownProvider { + + private final ApplicationContext applicationContext; + private final TaskExecutor taskExecutor; + + public void shutdown(Class calledBy, int exitCode) { + String className = calledBy == null ? "unknown" : calledBy.getSimpleName(); + log.info("Application was signalled by {} to shutdown; exit code: {}", className, exitCode); + if (taskExecutor != null && taskExecutor instanceof ThreadPoolTaskExecutor) { + log.info("Shutting down thread pool executor"); + ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) taskExecutor; + executor.shutdown(); + } + int ec = SpringApplication.exit(applicationContext, () -> exitCode); + System.exit(ec); + } + + public void shutdown(Class calledBy) { + shutdown(calledBy, 0); + } + + public void shutdown() { + shutdown(null, 0); + } + +} diff --git a/src/main/java/com/netgrif/application/engine/configuration/properties/MigrationProperties.java b/src/main/java/com/netgrif/application/engine/configuration/properties/MigrationProperties.java new file mode 100644 index 00000000000..aeba1845e06 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/configuration/properties/MigrationProperties.java @@ -0,0 +1,39 @@ +package com.netgrif.application.engine.configuration.properties; + + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.LinkedHashSet; +import java.util.Set; + +@Data +@Configuration +@ConfigurationProperties(prefix = "nae.migration") +public class MigrationProperties { + + /** + * A list of migration process identifiers or names that should be skipped when applying migration logic. + * This property allows you to configure specific migrations that should be ignored, + * typically useful for excluding unnecessary or problematic migrations. + */ + private Set skip = new LinkedHashSet<>(); + + /** + * Indicates whether caches should be evicted as part of the migration process. + * This property allows enabling or disabling the cache eviction mechanism, which + * is useful in ensuring consistency and up-to-date data during migration operations. + * Default value is {@code true}. + */ + private boolean evictCaches = true; + + /** + * Specifies whether the application should automatically shut down once the migration process is completed. + * This property can be used to terminate the application after the migration, ensuring a clean exit + * if no further operations are intended post-migration. + * Default value is {@code false}. + */ + private boolean shutdownAfterMigration = false; + +} diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java b/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java index 58c7db6f9d9..910e7590002 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java @@ -772,12 +772,9 @@ public List save(List tasks) { } @Override - public void resolveUserRef(Case useCase) { - useCase.getTasks().forEach(taskPair -> { - Optional taskOptional = taskRepository.findById(taskPair.getTask()); - taskOptional.ifPresent(task -> resolveUserRef(task, useCase)); - }); - + public List resolveUserRef(Case useCase) { + List tasks = taskRepository.findAllBy_idIn(useCase.getTasks().stream().map(TaskPair::getTask).collect(Collectors.toList())); + return tasks.stream().map(task -> resolveUserRef(task, useCase)).collect(Collectors.toList()); } @Override @@ -786,9 +783,9 @@ public Task resolveUserRef(Task task, Case useCase) { task.getNegativeViewUsers().clear(); task.getUserRefs().forEach((id, permission) -> { List userIds = getExistingUsers((UserListFieldValue) useCase.getDataSet().get(id).getValue()); - if (userIds != null && userIds.size() != 0 && permission.containsKey("view") && !permission.get("view")) { + if (userIds != null && !userIds.isEmpty() && permission.containsKey("view") && !permission.get("view")) { task.getNegativeViewUsers().addAll(userIds); - } else if (userIds != null && userIds.size() != 0) { + } else if (userIds != null && !userIds.isEmpty()) { task.addUsers(new HashSet<>(userIds), permission); } }); diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java b/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java index 3d9ac2dd2b3..2cae7ed9203 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java @@ -228,7 +228,7 @@ public Case resolveUserRef(Case useCase) { private void resolveUserRefPermissions(Case useCase, String userListId, Map permission) { List userIds = getExistingUsers((UserListFieldValue) useCase.getDataSet().get(userListId).getValue()); - if (userIds != null && userIds.size() != 0) { + if (userIds != null && !userIds.isEmpty()) { if (permission.containsKey("view") && !permission.get("view")) { useCase.getNegativeViewUsers().addAll(userIds); } else { @@ -241,7 +241,7 @@ private List getExistingUsers(UserListFieldValue userListValue) { if (userListValue == null) return null; return userListValue.getUserValues().stream().map(UserFieldValue::getId) - .filter(id -> userService.resolveById(id, false) != null) + .filter(id -> userService.existsById(id)) .collect(Collectors.toList()); } diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ITaskService.java b/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ITaskService.java index f7b867eb79c..691b2de4a9b 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ITaskService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ITaskService.java @@ -15,7 +15,6 @@ import com.netgrif.application.engine.workflow.web.responsebodies.TaskReference; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Locale; @@ -106,7 +105,7 @@ public interface ITaskService { DelegateTaskEventOutcome delegateTask(LoggedUser loggedUser, String delegatedId, String taskId, Map params) throws TransitionNotExecutableException; - void resolveUserRef(Case useCase); + List resolveUserRef(Case useCase); Task resolveUserRef(Task task, Case useCase); @@ -127,4 +126,4 @@ public interface ITaskService { List save(List tasks); SetDataEventOutcome getMainOutcome(Map outcomes, String taskId); -} \ No newline at end of file +} diff --git a/src/test/java/com/netgrif/application/engine/auth/service/UserServiceTest.java b/src/test/java/com/netgrif/application/engine/auth/service/UserServiceTest.java new file mode 100644 index 00000000000..874e29fd24e --- /dev/null +++ b/src/test/java/com/netgrif/application/engine/auth/service/UserServiceTest.java @@ -0,0 +1,41 @@ +package com.netgrif.application.engine.auth.service; + +import com.netgrif.application.engine.TestHelper; +import com.netgrif.application.engine.auth.domain.IUser; +import com.netgrif.application.engine.auth.service.interfaces.IUserService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest +@ActiveProfiles({"test"}) +@ExtendWith(SpringExtension.class) +public class UserServiceTest { + + @Autowired + private IUserService service; + + @Autowired + private TestHelper testHelper; + + @BeforeEach + void before() { + testHelper.truncateDbs(); + } + + @Test + public void shouldUserExist() { + IUser user = service.findByEmail("super@netgrif.com", true); + assertNotNull(user); + boolean userExists = service.existsById(user.getStringId()); + assertTrue(userExists); + } + +}