diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f8f4245 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +gradle.properties +gradlew +gradlew.bat +gradle/ + +**/.gradle +**/build \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..7b17158 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,21 @@ +include: + - project: 'gitlab/gitlab' + ref: 'main' + file: 'ci-templates/gradle_java.yml' + + +deploy: + stage: deploy + tags: + - dind + script: + - echo "Building with gradle version ${BUILDVERSION}" + - gradle -Pversion=${BUILDVERSION} publish + - echo "BUILDVERSION=$BUILDVERSION" >> version.env + artifacts: + reports: + dotenv: version.env + rules: + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + - if: $CI_COMMIT_BRANCH =~ /^release/ + - if: $CI_COMMIT_TAG \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..eedabae --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,116 @@ +plugins { + `java-library` + `maven-publish` + `kotlin-dsl` + pmd + checkstyle + jacoco + id("io.freefair.lombok") version "8.4" + id("org.sonarqube") version "4.4.1.3373" +} + +val springBootVersion = "3.1.4" +val springCloudVersion = "4.0.4" +val lombokVersion = "1.18.30" + +configurations { + all { + exclude(group = "org.springframework.boot", module = "spring-boot-starter-logging") + exclude(group = "ch.qos.logback", module = "logback-classic") + exclude(group = "org.apache.logging.log4j", module = "log4j-to-slf4j") + } +} + + + +dependencies { + api("org.springframework.boot:spring-boot-starter-amqp:${springBootVersion}") + api("org.springframework.boot:spring-boot-starter-web:${springBootVersion}") + api("org.springframework.cloud:spring-cloud-starter-openfeign:${springCloudVersion}") + api("org.springframework.boot:spring-boot-configuration-processor:${springBootVersion}") + api("org.projectlombok:lombok:${lombokVersion}") + runtimeOnly("org.springframework.boot:spring-boot-devtools:${springBootVersion}") + testImplementation("org.springframework.boot:spring-boot-starter-test:${springBootVersion}") +} + +group = "com.knecon.fforesight" +description = "lifecycle-commons" +java.sourceCompatibility = JavaVersion.VERSION_17 +java.targetCompatibility = JavaVersion.VERSION_17 + +repositories { + mavenLocal() + maven { + url = uri("https://nexus.knecon.com/repository/gindev/"); + credentials { + username = providers.gradleProperty("mavenUser").getOrNull(); + password = providers.gradleProperty("mavenPassword").getOrNull(); + } + } + mavenCentral() +} + +publishing { + publications { + create("mavenJava") { + from(components["java"]) + } + } + repositories { + maven { + url = uri("https://nexus.knecon.com/repository/red-platform-releases/") + credentials { + username = providers.gradleProperty("mavenUser").getOrNull(); + password = providers.gradleProperty("mavenPassword").getOrNull(); + } + } + } +} + +tasks.withType { + onlyIf { publication.name == "mavenJava" } +} + + +pmd { + isConsoleOutput = true +} + +tasks.pmdMain { + pmd.ruleSetFiles = files("${rootDir}/config/pmd/pmd.xml") +} + +tasks.pmdTest { + pmd.ruleSetFiles = files("${rootDir}/config/pmd/test_pmd.xml") +} + +tasks.named("test") { + useJUnitPlatform() + reports { + junitXml.outputLocation.set(layout.buildDirectory.dir("reports/junit")) + } +} + +sonarqube { + properties { + property("sonar.login", providers.gradleProperty("sonarToken").getOrNull()) + property("sonar.host.url", "https://sonarqube.knecon.com") + } +} + +tasks.test { + finalizedBy(tasks.jacocoTestReport) +} + +tasks.jacocoTestReport { + dependsOn(tasks.test) + reports { + xml.required.set(true) + csv.required.set(false) + html.outputLocation.set(layout.buildDirectory.dir("jacocoHtml")) + } +} + +java { + withJavadocJar() +} \ No newline at end of file diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..8faaf13 --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/pmd/pmd.xml b/config/pmd/pmd.xml new file mode 100644 index 0000000..7537e1c --- /dev/null +++ b/config/pmd/pmd.xml @@ -0,0 +1,20 @@ + + + + + Knecon ruleset checks the code for bad stuff + + + + + + + + + + + + diff --git a/config/pmd/test_pmd.xml b/config/pmd/test_pmd.xml new file mode 100644 index 0000000..be4aa92 --- /dev/null +++ b/config/pmd/test_pmd.xml @@ -0,0 +1,22 @@ + + + + + Knecon test ruleset checks the code for bad stuff + + + + + + + + + + + + + + diff --git a/gradle.properties.kts b/gradle.properties.kts new file mode 100644 index 0000000..ae8371b --- /dev/null +++ b/gradle.properties.kts @@ -0,0 +1 @@ +version = 0.1-SNAPSHOT \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..58c22a1 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,5 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +rootProject.name = "tenant-commons" diff --git a/src/main/java/com/knecon/fforesight/lifecyclecommons/LifecycleAspect.java b/src/main/java/com/knecon/fforesight/lifecyclecommons/LifecycleAspect.java new file mode 100644 index 0000000..ce99212 --- /dev/null +++ b/src/main/java/com/knecon/fforesight/lifecyclecommons/LifecycleAspect.java @@ -0,0 +1,59 @@ +package com.knecon.fforesight.lifecyclecommons; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.amqp.AmqpRejectAndDontRequeueException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Aspect +@Component +@Slf4j +@RequiredArgsConstructor +public class LifecycleAspect { + + private final LifecycleManager lifecycleManager; + + @Value("${aspect.base-package}") + private String basePackage; + + + @Around("@annotation(org.springframework.amqp.rabbit.annotation.RabbitListener) || " + + "@annotation(org.springframework.web.bind.annotation.GetMapping) || " + + "@annotation(org.springframework.web.bind.annotation.PostMapping) || " + + "@annotation(org.springframework.web.bind.annotation.PutMapping) || " + + "@annotation(org.springframework.web.bind.annotation.DeleteMapping) || " + + "@annotation(org.springframework.web.bind.annotation.RequestMapping))") + public Object checkLifecycle(ProceedingJoinPoint joinPoint) throws Throwable { + + String targetClassName = joinPoint.getTarget().getClass().getPackageName(); + if (!targetClassName.startsWith(basePackage)) { + return joinPoint.proceed(); + } + + synchronized (lifecycleManager) { + if (!lifecycleManager.isRunning()) { + log.info("Application is shutting down, rejecting new messages."); + throw new AmqpRejectAndDontRequeueException("Application is shutting down, rejecting new messages."); + } + lifecycleManager.incrementAndGet(); + } + + try { + return joinPoint.proceed(); + } finally { + int remainingTasks = lifecycleManager.decrementAndGet(); + synchronized (lifecycleManager) { + if (remainingTasks == 0 && !lifecycleManager.isRunning()) { + lifecycleManager.countDown(); + log.info("All tasks are done, ready for shutdown."); + } + } + } + } + +} diff --git a/src/main/java/com/knecon/fforesight/lifecyclecommons/LifecycleManager.java b/src/main/java/com/knecon/fforesight/lifecyclecommons/LifecycleManager.java new file mode 100644 index 0000000..fcb322f --- /dev/null +++ b/src/main/java/com/knecon/fforesight/lifecyclecommons/LifecycleManager.java @@ -0,0 +1,106 @@ +package com.knecon.fforesight.lifecyclecommons; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.context.ApplicationListener; +import org.springframework.context.SmartLifecycle; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +@RequiredArgsConstructor +public class LifecycleManager implements SmartLifecycle, ApplicationListener { + + private volatile boolean running = false; + private volatile boolean shutdownInitiated = false; + private final Object shutdownMonitor = new Object(); + private final AtomicInteger activeTasks = new AtomicInteger(0); + private final CountDownLatch latch = new CountDownLatch(1); + + + public void countDown() { + + latch.countDown(); + } + + + public int incrementAndGet() { + + return activeTasks.incrementAndGet(); + } + + + public int decrementAndGet() { + + return activeTasks.decrementAndGet(); + } + + + @Override + public void start() { + + synchronized (shutdownMonitor) { + running = true; + } + } + + + @Override + public void stop() { + + synchronized (shutdownMonitor) { + running = false; + if (activeTasks.get() == 0) { + latch.countDown(); // No active tasks, release the latch immediately + } + } + } + + + @Override + public boolean isRunning() { + + return running; + } + + + @Override + public boolean isAutoStartup() { + + return true; + } + + + @Override + public int getPhase() { + + return Integer.MAX_VALUE; // Start this component last and stop it first + } + + + @Override + public void onApplicationEvent(ContextClosedEvent event) { + + synchronized (shutdownMonitor) { + if (shutdownInitiated) { + return; // Avoid multiple shutdown initiations + } + shutdownInitiated = true; + } + + stop(); + log.info("Context is closing, waiting for ongoing tasks to complete."); + try { + latch.await(); // Wait for the latch to count down to zero + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("Shutdown was interrupted", e); + } + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..f67e16a --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,2 @@ +aspect: + base-package: com.knecon \ No newline at end of file diff --git a/src/test/java/com/knecon/fforesight/lifecyclecommons/config/LifecycleAspectTest.java b/src/test/java/com/knecon/fforesight/lifecyclecommons/config/LifecycleAspectTest.java new file mode 100644 index 0000000..7291d43 --- /dev/null +++ b/src/test/java/com/knecon/fforesight/lifecyclecommons/config/LifecycleAspectTest.java @@ -0,0 +1,73 @@ +package com.knecon.fforesight.lifecyclecommons.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.amqp.AmqpRejectAndDontRequeueException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import com.knecon.fforesight.lifecyclecommons.LifecycleManager; +import com.knecon.fforesight.lifecyclecommons.config.utils.AspectTestConfig; +import com.knecon.fforesight.lifecyclecommons.config.utils.TestController; + +import lombok.SneakyThrows; + +@ExtendWith({SpringExtension.class, MockitoExtension.class}) +@SpringBootTest(classes = {AspectTestConfig.class, TestController.class}) +@Import(AspectTestConfig.class) +public class LifecycleAspectTest { + + @Autowired + private TestController testController; + + @Autowired + private LifecycleManager lifecycleManager; + + + @BeforeEach + public void setup() { + + lifecycleManager.start(); + } + + + @Test + @SneakyThrows + public void testHttpGetAspectWhenRunning() { + + String response = testController.testGet(); + + assertEquals("Test Get", response); + } + + + @Test + @SneakyThrows + public void testStopLifecycleWhenRunning() { + + String response = testController.testGet(); + + lifecycleManager.stop(); + assertEquals("Test Get", response); + } + + + @Test + public void testHttpGetAspectWhenNotRunning() { + + lifecycleManager.stop(); + + Exception exception = assertThrows(AmqpRejectAndDontRequeueException.class, () -> { + testController.testGet(); + }); + assertEquals("Application is shutting down, rejecting new messages.", exception.getMessage()); + } + +} diff --git a/src/test/java/com/knecon/fforesight/lifecyclecommons/config/utils/AspectTestConfig.java b/src/test/java/com/knecon/fforesight/lifecyclecommons/config/utils/AspectTestConfig.java new file mode 100644 index 0000000..1c2e197 --- /dev/null +++ b/src/test/java/com/knecon/fforesight/lifecyclecommons/config/utils/AspectTestConfig.java @@ -0,0 +1,28 @@ +package com.knecon.fforesight.lifecyclecommons.config.utils; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +import com.knecon.fforesight.lifecyclecommons.LifecycleAspect; +import com.knecon.fforesight.lifecyclecommons.LifecycleManager; + +@TestConfiguration +@EnableAspectJAutoProxy +public class AspectTestConfig { + + @Bean + public LifecycleManager lifecycleManager() { + + return new LifecycleManager(); + } + + + @Bean + public LifecycleAspect lifecycleAspect(LifecycleManager lifecycleManager) { + + return new LifecycleAspect(lifecycleManager); + } + +} + diff --git a/src/test/java/com/knecon/fforesight/lifecyclecommons/config/utils/TestController.java b/src/test/java/com/knecon/fforesight/lifecyclecommons/config/utils/TestController.java new file mode 100644 index 0000000..33bdba7 --- /dev/null +++ b/src/test/java/com/knecon/fforesight/lifecyclecommons/config/utils/TestController.java @@ -0,0 +1,17 @@ +package com.knecon.fforesight.lifecyclecommons.config.utils; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TestController { + + @GetMapping("/test-get") + public String testGet() throws InterruptedException { + + Thread.sleep(5000); + return "Test Get"; + } + +} + diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 0000000..f67e16a --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,2 @@ +aspect: + base-package: com.knecon \ No newline at end of file