├── .gitignore ├── .java-version ├── README.md ├── build.gradle ├── check-engine.drawio.png ├── common ├── database │ ├── README.md │ ├── build.gradle │ ├── docker-compose.yml │ └── src │ │ └── main │ │ ├── kotlin │ │ └── io │ │ │ └── reflectoring │ │ │ └── components │ │ │ └── common │ │ │ └── database │ │ │ └── JooqAutoConfiguration.kt │ │ └── resources │ │ ├── META-INF │ │ └── spring │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ └── db │ │ └── migration │ │ └── check-engine │ │ ├── V001__create_table_check.sql │ │ └── V002__add_condition_key.sql └── testcontainers │ ├── build.gradle │ ├── lombok.config │ └── src │ └── main │ └── java │ └── io │ └── reflectoring │ └── components │ └── testcontainers │ ├── PostgreSQLContainerConfiguration.java │ └── PostgreSQLTest.java ├── components └── check-engine │ ├── build.gradle │ └── src │ ├── main │ ├── kotlin │ │ └── io │ │ │ └── reflectoring │ │ │ └── components │ │ │ └── checkengine │ │ │ ├── api │ │ │ ├── Check.kt │ │ │ ├── CheckExecutor.kt │ │ │ ├── CheckId.kt │ │ │ ├── CheckKey.kt │ │ │ ├── CheckQueries.kt │ │ │ ├── CheckRequest.kt │ │ │ ├── CheckResult.kt │ │ │ ├── CheckScheduler.kt │ │ │ ├── ChecksSummary.kt │ │ │ └── ConditionKey.kt │ │ │ └── internal │ │ │ ├── CheckEngineAutoConfiguration.kt │ │ │ ├── checkrunner │ │ │ ├── api │ │ │ │ └── CheckRunner.kt │ │ │ └── internal │ │ │ │ ├── CheckResolver.kt │ │ │ │ ├── CheckRunnerConfiguration.kt │ │ │ │ └── DefaultCheckRunner.kt │ │ │ ├── database │ │ │ ├── api │ │ │ │ └── CheckMutations.kt │ │ │ └── internal │ │ │ │ ├── CheckEngineDatabaseConfiguration.kt │ │ │ │ └── JooqCheckRepository.kt │ │ │ └── queue │ │ │ └── internal │ │ │ ├── CheckRequestListener.kt │ │ │ ├── CheckRequestListenerRegistration.kt │ │ │ ├── CheckRequestListenerSqsConfiguration.kt │ │ │ ├── CheckRequestListenerSqsProperties.kt │ │ │ ├── CheckRequestPublisher.kt │ │ │ └── SqsCheckScheduler.kt │ └── resources │ │ └── META-INF │ │ └── spring │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ └── test │ └── kotlin │ └── io │ └── reflectoring │ └── components │ └── checkengine │ ├── CheckEngineTestApplication.kt │ └── internal │ ├── checkrunner │ └── internal │ │ └── CheckRunnerIntegrationTest.kt │ └── database │ └── internal │ └── JooqCheckRepositoryTest.kt ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── server ├── build.gradle └── src │ ├── main │ └── kotlin │ │ └── io │ │ └── reflectoring │ │ └── components │ │ └── Application.kt │ └── test │ └── kotlin │ └── io │ └── reflectoring │ └── components │ └── InternalPackageTest.kt └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | -------------------------------------------------------------------------------- /.java-version: -------------------------------------------------------------------------------- 1 | 17 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Component-based architecture example 2 | 3 | This repo is an example for a component-based architecture with Kotlin / Java as outlined in these resources: 4 | 5 | - [Clean Architecture Boundaries with Spring Boot and ArchUnit](https://reflectoring.io/java-components-clean-boundaries/) (blog article) 6 | - [Let's build components, not layers](https://www.youtube.com/watch?v=-VmhytwBZVs) (talk at [Spring I/O 2019](https://2019.springio.net/)) 7 | 8 | ## Rules of component architecture 9 | 10 | These are the rules of a component-based architecture as used in this example codebase: 11 | 12 | - A component consists of a set of classes in a dedicated package that defines its namespace. 13 | - A component has a dedicated `api` and `internal` package. 14 | - Any class in an `api` package may be consumed by the outside world. 15 | - Any class in an `internal` package may only be consumed by other classes in that package (i.e. they may not be consumed by the outside world). 16 | - A component may contain sub components in its `internal` package. Each sub component needs to live in its own unique package. 17 | 18 | ## Example component 19 | 20 | This codebase contains an example component called [check-engine](components/check-engine). This check engine is a fully functioning (if simple) component that is responsible for executing arbitrary checks against a given web page. A check may be something like "check if the web page https://foo.bar has valid HTML" or "check if the web page https://abc.def has proper SEO tags". 21 | 22 | The API of the component provides the following functionality: 23 | - Schedule a check for asynchronous execution (see `CheckScheduler`). 24 | - Query for the results of one or more checks (see `CheckQueries`). 25 | 26 | ![check engine](check-engine.drawio.png) 27 | 28 | The check engine component is made up of three sub components: 29 | 30 | - **queue**: This sub component interfaces with a queue to schedule checks for asynchronous implementation. This component implements the `CheckScheduler` API of the parent component. 31 | - **database**: This sub component interfaces with a database to store and retrieve the check results. This component implements the `CheckQueries` API of the parent component. 32 | - **checkrunner**: This sub component is responsible for executing scheduled checks asynchronously and storing the results in the database. It does not implement any of the parent component's API. The checkrunner component looks for `CheckExecutor` implementations in the Spring application context and runs each of those checks on any check that is being scheduled. 33 | 34 | Each component (the main component and the sub components) have their own Spring `@Configuration` class that pulls in all the Spring beans it needs to work. This allows us to create a Spring context on each level of the component tree, should we need it to write integration tests. 35 | 36 | ## Validating the component architecture 37 | 38 | If we follow the component-based approach outlined above, we can very easily validate these dependencies with this rule: 39 | 40 | > No classes that are outside of an "internal" package should access a class inside of that "internal" package. 41 | 42 | We can use [ArchUnit](https://www.archunit.org/) to test our codebase against this rule automatically. See [InternalPackageTest](server/src/test/kotlin/io/reflectoring/components/InternalPackageTest.kt). 43 | 44 | If we follow the conventions of naming internal packages properly, _we can use this one test to validate the component architecture of the whole project_. New components will automatically be included in this test as they are added to the codebase. 45 | 46 | Try introducing an invalid dependency and then run the `InternalPackageTest` and witness it failing :). 47 | 48 | ## Working with the example 49 | 50 | ### Requirements 51 | - JDK17 or above 52 | - Docker 53 | 54 | ### Commands 55 | All commands are to be run from the main folder of this project. 56 | 57 | | Command | Description | 58 | |-------------------------------|--------------------------------------------------------------------------------------------------------| 59 | | `./gradlew build` | builds the example project | 60 | | `./gradlew bootRun` | runs the Spring Boot app (⚠️currently not working, because the datasource is not configured correctly) | 61 | | `./gradlew generateJooq` | generated the JOOQ data access classes from the Flyway scripts | 62 | | `./gradlew composeDownForced` | removes the PostgreSQL Docker image used by JOOQ (run this when you get Flyway errors) | 63 | 64 | ## Discussion of this example codebase 65 | 66 | Some points to note about this example codebase: 67 | 68 | - The `server` Maven module contains a Spring Boot application that pulls in all the components as dependencies. 69 | - The `components` folder contains a Maven module for each component (although, there currently is only one top-level component). 70 | - In this codebase, the `check-engine` component lives in its own Maven module. We could just as well have chosen to include all the code into a single Maven module, as long as the component has its own unique package name. Putting a component into its own Maven module makes the component boundaries more distinct, however. 71 | - This codebase is using Kotlin. It works in pretty much the same way in a Java codebase. 72 | - An important aspect of component boundaries is that components don't share a database. If they share a database, they are coupled via the data layer, which very quickly leads to a tight coupling between components in all the layers. In the example code, the sub component defined through the package `io.reflectoring.components.checkengine.internal.database` is the only component with direct access to the check engine's database tables. 73 | - This example uses Flyway to create and update the required database schema (see the [common/database](common/database) Maven module). This module includes the database scripts for ALL components (even though this example repo only includes a single component named `check-engine`). That means all components would share a single database schema, but would work on separate database tables. There are other options that allow for a higher degree of separation between components' data (schema-per-component, database-per-component, ...). 74 | - This example is using JOOQ for the data access layer, taking advantage of its source generation feature: 75 | - During the build, the [common/database](common/database) module spins up a PostgreSQL Docker container. 76 | - Flyway then generates a database schema on that PostgreSQL instance from the [database scripts](common/database/src/main/resources/db). 77 | - JOOQ then analyzes the schema and generates data classes for each table in the schema. 78 | - The data access layers of our components then use these classes to access the database. This makes sure that the classes are always in sync with the database schema. -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | buildscript { 4 | ext { 5 | javaVersion = '17' 6 | springBootVersion = '3.0.5' 7 | kotlinVersion = '1.7.22' 8 | awsJavaSdkVersion = '1.11.739' 9 | testcontainersVersion = '1.16.2' 10 | } 11 | dependencies { 12 | // opening all Kotlin classes by default so that Spring can do its proxy magic 13 | classpath "org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}" 14 | } 15 | } 16 | 17 | plugins { 18 | // Defining the versions of Gradle plugins to use in the sub-projects. 19 | // "apply false" means that the plugin is no applied by default and has to be activated 20 | // in the subprojects specifically. 21 | id 'org.springframework.boot' version "${springBootVersion}" apply false 22 | id 'io.spring.dependency-management' version '1.1.0' 23 | id 'org.jetbrains.kotlin.jvm' version "${kotlinVersion}" 24 | id 'org.jetbrains.kotlin.plugin.spring' version "${kotlinVersion}" 25 | id 'org.flywaydb.flyway' version '8.0.0-beta3' apply false 26 | id 'nu.studer.jooq' version '8.1' apply false 27 | id 'com.avast.gradle.docker-compose' version '0.14.9' apply false 28 | } 29 | 30 | group = 'io.reflectoring' 31 | version = '0.0.1-SNAPSHOT' 32 | sourceCompatibility = "$javaVersion" 33 | 34 | repositories { 35 | mavenCentral() 36 | } 37 | 38 | subprojects { 39 | 40 | apply plugin: 'java' 41 | apply plugin: 'io.spring.dependency-management' 42 | apply plugin: 'java-library' 43 | apply plugin: 'org.jetbrains.kotlin.jvm' 44 | apply plugin: 'org.jetbrains.kotlin.plugin.spring' 45 | 46 | group = 'io.reflectoring' 47 | version = '0.0.1-SNAPSHOT' 48 | sourceCompatibility = "${javaVersion}" 49 | targetCompatibility = "${javaVersion}" 50 | 51 | repositories { 52 | mavenLocal() 53 | mavenCentral() 54 | } 55 | 56 | dependencyManagement { 57 | imports { 58 | mavenBom("org.springframework.boot:spring-boot-dependencies:${springBootVersion}") 59 | mavenBom("com.amazonaws:aws-java-sdk-bom:${awsJavaSdkVersion}") 60 | mavenBom("org.testcontainers:testcontainers-bom:${testcontainersVersion}") 61 | } 62 | 63 | dependencies { 64 | // SQS 65 | dependency 'io.reflectoring:sqs-starter:0.0.11' 66 | dependency 'io.reflectoring:sqs-starter-test:0.0.11' 67 | 68 | // jOOQ 69 | dependency 'org.jooq:jooq:3.18.2' 70 | dependency 'org.jooq:jooq-meta:3.18.2' 71 | dependency 'org.jooq:jooq-codegen:3.18.2' 72 | 73 | // testing 74 | dependency 'org.reflections:reflections:0.9.10' 75 | dependency 'org.mockito.kotlin:mockito-kotlin:4.0.0' 76 | dependency 'com.tngtech.archunit:archunit-junit5:0.21.0' 77 | 78 | } 79 | } 80 | 81 | // Standard dependencies for all modules 82 | dependencies { 83 | 84 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 85 | testImplementation project(':common:testcontainers') 86 | testImplementation 'org.mockito.kotlin:mockito-kotlin' 87 | } 88 | 89 | tasks.withType(KotlinCompile) { 90 | kotlinOptions { 91 | freeCompilerArgs = ['-Xjsr305=strict'] 92 | jvmTarget = '17' 93 | } 94 | } 95 | 96 | tasks.named('test') { 97 | useJUnitPlatform() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /check-engine.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thombergs/components-example/aa8d11c34e429238435857d7356fb75d5bf29e01/check-engine.drawio.png -------------------------------------------------------------------------------- /common/database/README.md: -------------------------------------------------------------------------------- 1 | # Database 2 | 3 | This module contains the [database migration scripts](src/main/resources/db/migration) that are managed by Flyway and is responsible for generating Java classes from the database to be used with the JOOQ library. 4 | 5 | After changing the SQL scripts in this module, run `./gradlew generateJooq` in the main folder of this project to generate JOOQ classes from an adhoc-database that has been initialized with these scripts via Flyway. 6 | 7 | The `generateJooq` task will start up a local PostgreSQL database, run the Flyway scripts against it to update the schema, and use JOOQ's source code generator to generate Java classes from the database model. This ensures that the database model in the database and in the code stay in sync. 8 | 9 | If you get a Flyway error like "Migration checksum mismatch for migration", you can purge your local PostgreSQL database via the command `./gradlew composeDownForced` from the main folder of this project. -------------------------------------------------------------------------------- /common/database/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.flywaydb.flyway' 3 | id 'nu.studer.jooq' 4 | id 'com.avast.gradle.docker-compose' 5 | } 6 | 7 | configurations { 8 | flywayMigration 9 | } 10 | 11 | dependencies { 12 | flywayMigration 'org.testcontainers:postgresql' 13 | flywayMigration 'org.postgresql:postgresql' 14 | jooqGenerator 'org.postgresql:postgresql' 15 | implementation 'org.postgresql:postgresql' 16 | api 'org.jooq:jooq' 17 | api 'org.jooq:jooq-meta' 18 | api 'org.jooq:jooq-codegen' 19 | implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' 20 | 21 | implementation 'org.flywaydb:flyway-core' 22 | } 23 | 24 | /** 25 | * Start a PostgreSQL Docker container from the docker-compose.yml. 26 | */ 27 | dockerCompose { 28 | stopContainers = false 29 | } 30 | dockerCompose.isRequiredBy(flywayMigrate) 31 | 32 | /** 33 | * Run the Flyway database migrations against the PostgreSQL we started with Docker. 34 | */ 35 | flyway { 36 | configurations = ['flywayMigration'] 37 | url = 'jdbc:postgresql://localhost:5431/components' 38 | user = 'components' 39 | password = 'components' 40 | } 41 | 42 | jooq { 43 | configurations { 44 | main { 45 | generationTool { 46 | logging = org.jooq.meta.jaxb.Logging.WARN 47 | jdbc { 48 | driver = 'org.postgresql.Driver' 49 | url = flyway.url 50 | user = flyway.user 51 | password = flyway.password 52 | } 53 | generator { 54 | name = 'org.jooq.codegen.DefaultGenerator' 55 | database { 56 | inputSchema = 'public' 57 | name = 'org.jooq.meta.postgres.PostgresDatabase' 58 | includes = '' 59 | excludes = '' 60 | } 61 | target { 62 | packageName = 'io.reflectoring.components.common.database' 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } 69 | 70 | /** 71 | * Generates JOOQ classes against the database created by Flyway. 72 | */ 73 | tasks.named('generateJooq').configure { 74 | // ensure database schema has been prepared by Flyway before generating the jOOQ sources 75 | dependsOn tasks.named('flywayMigrate') 76 | 77 | // declare Flyway migration scripts as inputs on the jOOQ task 78 | inputs.files(fileTree('src/main/resources/db/migration')) 79 | .withPropertyName('migrations') 80 | .withPathSensitivity(PathSensitivity.RELATIVE) 81 | 82 | // make jOOQ task participate in incremental builds and build caching 83 | allInputsDeclared = true 84 | outputs.cacheIf { true } 85 | } 86 | -------------------------------------------------------------------------------- /common/database/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | 5 | postgres: 6 | container_name: "components-jooq" 7 | image: postgres:15 8 | command: [ 9 | "postgres", 10 | "-c", "log_min_duration_statement=1000", 11 | "-c", "log_statement=none", 12 | "-c", "log_duration=off", 13 | ] 14 | volumes: 15 | - components-jooq-data:/var/lib/postgresqljooq/data 16 | ports: 17 | - 5431:5432 18 | environment: 19 | - POSTGRES_USER=components 20 | - POSTGRES_PASSWORD=components 21 | - POSTGRES_DB=components 22 | restart: always 23 | 24 | volumes: 25 | components-jooq-data: 26 | driver: local 27 | -------------------------------------------------------------------------------- /common/database/src/main/kotlin/io/reflectoring/components/common/database/JooqAutoConfiguration.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.common.database 2 | 3 | import org.jooq.DSLContext 4 | import org.jooq.SQLDialect 5 | import org.jooq.impl.DSL 6 | import org.springframework.context.annotation.Bean 7 | import org.springframework.context.annotation.Configuration 8 | import javax.sql.DataSource 9 | 10 | @Configuration 11 | class JooqAutoConfiguration { 12 | @Bean 13 | fun jooqDSL(dataSource: DataSource?): DSLContext { 14 | return DSL.using(dataSource, SQLDialect.POSTGRES) 15 | } 16 | } -------------------------------------------------------------------------------- /common/database/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports: -------------------------------------------------------------------------------- 1 | io.reflectoring.components.common.database.JooqAutoConfiguration 2 | 3 | -------------------------------------------------------------------------------- /common/database/src/main/resources/db/migration/check-engine/V001__create_table_check.sql: -------------------------------------------------------------------------------- 1 | create table ce_check ( 2 | id serial unique, 3 | key varchar(50) not null, 4 | tenant_id varchar(36) not null, 5 | page_url varchar(1000) not null, 6 | start_date timestamp not null, 7 | end_date timestamp, 8 | execution_status varchar(50) not null, 9 | result_status varchar(50), 10 | primary key (id) 11 | ); 12 | 13 | create table ce_check_fix ( 14 | id serial unique, 15 | check_id bigint not null, 16 | fix varchar(1000), 17 | primary key (id), 18 | constraint check_fix_fk foreign key (check_id) references ce_check(id) on delete cascade 19 | ); 20 | 21 | create index ce_check_index on ce_check ( 22 | tenant_id, 23 | page_url, 24 | execution_status, 25 | end_date 26 | ); 27 | 28 | -------------------------------------------------------------------------------- /common/database/src/main/resources/db/migration/check-engine/V002__add_condition_key.sql: -------------------------------------------------------------------------------- 1 | alter table ce_check_fix 2 | add column condition_key varchar(50) not null default 'unknown'; -------------------------------------------------------------------------------- /common/testcontainers/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation 'org.springframework.boot:spring-boot-starter-test' 3 | implementation 'org.testcontainers:postgresql' 4 | implementation 'org.testcontainers:junit-jupiter' 5 | api project(':common:database') 6 | } 7 | -------------------------------------------------------------------------------- /common/testcontainers/lombok.config: -------------------------------------------------------------------------------- 1 | lombok.addLombokGeneratedAnnotation = true -------------------------------------------------------------------------------- /common/testcontainers/src/main/java/io/reflectoring/components/testcontainers/PostgreSQLContainerConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.testcontainers; 2 | 3 | import jakarta.annotation.PostConstruct; 4 | import org.jooq.tools.jdbc.SingleConnectionDataSource; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; 8 | import org.springframework.boot.jdbc.DataSourceBuilder; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.testcontainers.containers.PostgreSQLContainer; 12 | 13 | import javax.sql.DataSource; 14 | import java.sql.SQLException; 15 | 16 | /** 17 | * Starts an empty PostgreSQL container and configures a DataSource against it. 18 | */ 19 | @Configuration 20 | class PostgreSQLContainerConfiguration { 21 | 22 | private static final Logger logger = 23 | LoggerFactory.getLogger(PostgreSQLContainerConfiguration.class); 24 | private static final String DATABASE_NAME = "components"; 25 | private static final String USERNAME = "components"; 26 | private static final String PASSWORD = "components"; 27 | 28 | private PostgreSQLContainer container; 29 | 30 | @Bean 31 | PostgreSQLContainer postgreSQLContainer(DataSourceProperties dataSourceProperties) { 32 | container = 33 | new PostgreSQLContainer<>("postgres:15") 34 | .withDatabaseName(DATABASE_NAME) 35 | .withPassword(PASSWORD) 36 | .withUsername(USERNAME); 37 | container.start(); 38 | logger.info( 39 | String.format( 40 | "Started PostgreSQL testcontainer. JDBC URL: %s", container.getJdbcUrl())); 41 | return container; 42 | } 43 | 44 | @PostConstruct 45 | void cleanup(){ 46 | if(container != null) { 47 | container.stop(); 48 | } 49 | } 50 | 51 | @Bean 52 | DataSource postgresqlDataSource(PostgreSQLContainer postgreSQLContainer) throws SQLException { 53 | DataSource postgresDataSource = DataSourceBuilder.create() 54 | .password(PASSWORD) 55 | .username(USERNAME) 56 | .url(postgreSQLContainer.getJdbcUrl()) 57 | .driverClassName(postgreSQLContainer.getDriverClassName()) 58 | .build(); 59 | 60 | // We use a single connection datasource so that JOOQ queries and Spring Data queries 61 | // share the same connection in tests. Otherwise, data added with Spring Data and 62 | // queried with JOOQ (or vice versa) within the same test won't work, because they would 63 | // use different connections. 64 | return new SingleConnectionDataSource(postgresDataSource.getConnection()); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /common/testcontainers/src/main/java/io/reflectoring/components/testcontainers/PostgreSQLTest.java: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.testcontainers; 2 | 3 | import static java.lang.annotation.ElementType.ANNOTATION_TYPE; 4 | import static java.lang.annotation.ElementType.TYPE; 5 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 6 | 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | import org.junit.jupiter.api.Tag; 10 | import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest; 11 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 12 | import org.springframework.context.annotation.Import; 13 | import org.springframework.test.context.ActiveProfiles; 14 | 15 | /** 16 | * Use this annotation on JUnit tests that need a PostgreSQL database. Spins up a testcontainer with 17 | * a PostgreSQL database and adds a {@link javax.sql.DataSource} to the application context. 18 | */ 19 | @Target({TYPE, ANNOTATION_TYPE}) 20 | @Retention(RUNTIME) 21 | @Import(PostgreSQLContainerConfiguration.class) 22 | @Tag("PostgreSQLTest") 23 | @ActiveProfiles("test") 24 | @DataJdbcTest // required to set up data source and Flyway 25 | @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 26 | public @interface PostgreSQLTest {} 27 | -------------------------------------------------------------------------------- /components/check-engine/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation project(':common:database') 3 | 4 | implementation 'org.springframework.boot:spring-boot-starter' 5 | implementation 'org.jetbrains.kotlin:kotlin-reflect' 6 | implementation 'io.reflectoring:sqs-starter' 7 | implementation 'com.amazonaws:aws-java-sdk-sqs' 8 | testImplementation 'io.reflectoring:sqs-starter-test' 9 | 10 | } 11 | -------------------------------------------------------------------------------- /components/check-engine/src/main/kotlin/io/reflectoring/components/checkengine/api/Check.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.api 2 | 3 | import java.time.LocalDateTime 4 | import java.util.UUID 5 | 6 | enum class ExecutionStatus { 7 | IN_PROGRESS, 8 | SUCCESS, 9 | ERROR 10 | } 11 | 12 | class Check( 13 | val id: CheckId, 14 | val key: CheckKey, 15 | val tenantId: UUID, 16 | val pageUrl: String, 17 | val startDate: LocalDateTime, 18 | var endDate: LocalDateTime?, 19 | var executionStatus: ExecutionStatus, 20 | var checkResult: CheckResult? 21 | ) { 22 | 23 | /** 24 | * Marks the check as having failed execution (i.e. an unknown error happened and the check could not finish and 25 | * does not have a result. 26 | */ 27 | fun setExecutionError() { 28 | this.executionStatus = ExecutionStatus.ERROR 29 | this.endDate = LocalDateTime.now() 30 | } 31 | 32 | /** 33 | * Marks the check as having run successfully and saves the given result. 34 | */ 35 | fun setResult(result: CheckResult) { 36 | this.executionStatus = ExecutionStatus.SUCCESS 37 | this.endDate = LocalDateTime.now() 38 | this.checkResult = result 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /components/check-engine/src/main/kotlin/io/reflectoring/components/checkengine/api/CheckExecutor.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.api 2 | 3 | import java.util.UUID 4 | 5 | /** 6 | * Executes a certain check on a given web page and returns a CheckResult. 7 | * All CheckExecutors in the Spring application context will automatically be made available to the check engine. 8 | */ 9 | interface CheckExecutor { 10 | 11 | /** 12 | * Returns the key of the check this executor covers. 13 | */ 14 | fun supportedCheck(): CheckKey 15 | 16 | /** 17 | * Executes the check for the given page. 18 | * Any exception that is thrown by this method will mark the check as unsuccessfully executed. 19 | */ 20 | fun execute(tenantId: UUID, pageUrl: String): CheckResult 21 | } 22 | -------------------------------------------------------------------------------- /components/check-engine/src/main/kotlin/io/reflectoring/components/checkengine/api/CheckId.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.api 2 | 3 | class CheckId( 4 | val id: Long 5 | ) { 6 | 7 | override fun equals(other: Any?): Boolean { 8 | if (this === other) return true 9 | if (javaClass != other?.javaClass) return false 10 | 11 | other as CheckId 12 | 13 | if (id != other.id) return false 14 | 15 | return true 16 | } 17 | 18 | override fun hashCode(): Int { 19 | return id.hashCode() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /components/check-engine/src/main/kotlin/io/reflectoring/components/checkengine/api/CheckKey.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.api 2 | 3 | data class CheckKey( 4 | val key: String 5 | ) 6 | -------------------------------------------------------------------------------- /components/check-engine/src/main/kotlin/io/reflectoring/components/checkengine/api/CheckQueries.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.api 2 | 3 | import java.util.UUID 4 | 5 | interface CheckQueries { 6 | 7 | fun getCheck(checkId: CheckId): Check? 8 | 9 | /** 10 | * Returns the number of checks for a given site and status. Only the latest checks of each type are considered. 11 | */ 12 | fun getLatestChecksCountByStatus(tenantId: UUID, status: ResultStatus): Int 13 | 14 | /** 15 | * Returns a ChecksSummary for each page of a given site. 16 | */ 17 | fun getLatestChecksSummaries(tenantId: UUID): List 18 | 19 | /** 20 | * Returns the latest checks of each type for a given site and page. 21 | */ 22 | fun getLatestChecksByPage(tenantId: UUID, pageUrl: String): List 23 | } 24 | -------------------------------------------------------------------------------- /components/check-engine/src/main/kotlin/io/reflectoring/components/checkengine/api/CheckRequest.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.api 2 | 3 | import java.util.UUID 4 | 5 | /** 6 | * A request to run a specific check on a web page. 7 | */ 8 | data class CheckRequest( 9 | val checkKey: CheckKey, 10 | 11 | /** 12 | * ID used to segregate checks per tenant. 13 | */ 14 | val tenantId: UUID, 15 | val pageUrl: String 16 | ) 17 | -------------------------------------------------------------------------------- /components/check-engine/src/main/kotlin/io/reflectoring/components/checkengine/api/CheckResult.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.api 2 | 3 | enum class ResultStatus { 4 | PASSED, 5 | FAILED 6 | } 7 | 8 | data class Fix( 9 | val failedConditionKey: ConditionKey, 10 | val message: String, 11 | ) 12 | 13 | data class CheckResult( 14 | val status: ResultStatus = ResultStatus.PASSED, 15 | val fixes: List = emptyList() 16 | ) 17 | -------------------------------------------------------------------------------- /components/check-engine/src/main/kotlin/io/reflectoring/components/checkengine/api/CheckScheduler.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.api 2 | 3 | interface CheckScheduler { 4 | 5 | /** 6 | * Schedules the given checks to be executed asynchronously. 7 | */ 8 | fun scheduleChecks(checks: List) 9 | } 10 | -------------------------------------------------------------------------------- /components/check-engine/src/main/kotlin/io/reflectoring/components/checkengine/api/ChecksSummary.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.api 2 | 3 | import java.time.LocalDateTime 4 | 5 | data class ChecksSummary( 6 | val pageUrl: String, 7 | val lastScanned: LocalDateTime, 8 | val passedChecks: Int, 9 | val failedChecks: Int 10 | ) 11 | -------------------------------------------------------------------------------- /components/check-engine/src/main/kotlin/io/reflectoring/components/checkengine/api/ConditionKey.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.api 2 | 3 | /** 4 | * Unique identifier for a condition of a check that may fail. Should be something like "SOCIAL-1-2", where 5 | * "SOCIAL-1" is the check that was executed and "2" is the condition in that check that failed. 6 | */ 7 | data class ConditionKey( 8 | val key: String 9 | ) { 10 | override fun toString(): String { 11 | return this.key 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /components/check-engine/src/main/kotlin/io/reflectoring/components/checkengine/internal/CheckEngineAutoConfiguration.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.internal 2 | 3 | import org.springframework.boot.autoconfigure.AutoConfiguration 4 | import org.springframework.context.annotation.ComponentScan 5 | 6 | @AutoConfiguration 7 | @ComponentScan 8 | class CheckEngineAutoConfiguration 9 | -------------------------------------------------------------------------------- /components/check-engine/src/main/kotlin/io/reflectoring/components/checkengine/internal/checkrunner/api/CheckRunner.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.internal.checkrunner.api 2 | 3 | import io.reflectoring.components.checkengine.api.Check 4 | import io.reflectoring.components.checkengine.api.CheckRequest 5 | 6 | interface CheckRunner { 7 | 8 | fun runCheck(checkRequest: CheckRequest): Check 9 | } 10 | -------------------------------------------------------------------------------- /components/check-engine/src/main/kotlin/io/reflectoring/components/checkengine/internal/checkrunner/internal/CheckResolver.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.internal.checkrunner.internal 2 | 3 | import io.reflectoring.components.checkengine.api.CheckExecutor 4 | import io.reflectoring.components.checkengine.api.CheckKey 5 | import io.reflectoring.components.checkengine.api.CheckRequest 6 | import org.springframework.stereotype.Component 7 | 8 | /** 9 | * Collects all CheckExecutor beans from the application context and provides a method to get the right check 10 | * for a given CheckRequest. 11 | */ 12 | @Component 13 | class CheckResolver(executors: List) { 14 | 15 | private val executors: Map 16 | 17 | init { 18 | this.executors = executors.associateBy { it.supportedCheck() } 19 | } 20 | 21 | /** 22 | * Returns the CheckExecutor for the given CheckRequest. 23 | * Throws an IllegalStateException if there is no CheckExecutor for the given CheckRequest. 24 | */ 25 | fun getExecutorFor(checkRequest: CheckRequest): CheckExecutor { 26 | return executors[checkRequest.checkKey] 27 | ?: throw ExecutorNotFoundException( 28 | checkRequest.checkKey 29 | ) 30 | } 31 | } 32 | 33 | class ExecutorNotFoundException(checkKey: CheckKey) : 34 | java.lang.RuntimeException("could not find CheckExecutor for check with key $checkKey") 35 | -------------------------------------------------------------------------------- /components/check-engine/src/main/kotlin/io/reflectoring/components/checkengine/internal/checkrunner/internal/CheckRunnerConfiguration.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.internal.checkrunner.internal 2 | 3 | import org.springframework.context.annotation.ComponentScan 4 | import org.springframework.context.annotation.Configuration 5 | 6 | @Configuration 7 | @ComponentScan 8 | open class CheckRunnerConfiguration 9 | -------------------------------------------------------------------------------- /components/check-engine/src/main/kotlin/io/reflectoring/components/checkengine/internal/checkrunner/internal/DefaultCheckRunner.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.internal.checkrunner.internal 2 | 3 | import io.reflectoring.components.checkengine.internal.database.api.CheckMutations 4 | import org.slf4j.LoggerFactory 5 | import org.springframework.stereotype.Component 6 | 7 | @Component 8 | class DefaultCheckRunner( 9 | private val checkResolver: CheckResolver, 10 | private val checkMutations: CheckMutations 11 | ) : io.reflectoring.components.checkengine.internal.checkrunner.api.CheckRunner { 12 | 13 | private val logger = LoggerFactory.getLogger(DefaultCheckRunner::class.java) 14 | 15 | override fun runCheck(checkRequest: io.reflectoring.components.checkengine.api.CheckRequest): io.reflectoring.components.checkengine.api.Check { 16 | val check = checkMutations.initializeCheck(checkRequest) 17 | 18 | try { 19 | val executor = checkResolver.getExecutorFor(checkRequest) 20 | val result = executor.execute(checkRequest.tenantId, checkRequest.pageUrl) 21 | check.setResult(result) 22 | checkMutations.updateCheck(check) 23 | } catch (e: Exception) { 24 | logger.error("check with id ${check.id} failed: $checkRequest", e) 25 | check.setExecutionError() 26 | checkMutations.updateCheck(check) 27 | } 28 | return check 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /components/check-engine/src/main/kotlin/io/reflectoring/components/checkengine/internal/database/api/CheckMutations.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.internal.database.api 2 | 3 | import io.reflectoring.components.checkengine.api.Check 4 | import io.reflectoring.components.checkengine.api.CheckRequest 5 | 6 | interface CheckMutations { 7 | 8 | /** 9 | * Inserts a new check in the database. 10 | */ 11 | fun initializeCheck(checkRequest: CheckRequest): Check 12 | 13 | fun updateCheck(check: Check) 14 | } 15 | -------------------------------------------------------------------------------- /components/check-engine/src/main/kotlin/io/reflectoring/components/checkengine/internal/database/internal/CheckEngineDatabaseConfiguration.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.internal.database.internal 2 | 3 | import org.springframework.context.annotation.ComponentScan 4 | import org.springframework.context.annotation.Configuration 5 | import org.springframework.context.annotation.Import 6 | 7 | import io.reflectoring.components.common.database.JooqAutoConfiguration 8 | 9 | @Configuration 10 | @ComponentScan 11 | @Import(JooqAutoConfiguration::class) 12 | class CheckEngineDatabaseConfiguration 13 | -------------------------------------------------------------------------------- /components/check-engine/src/main/kotlin/io/reflectoring/components/checkengine/internal/database/internal/JooqCheckRepository.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.internal.database.internal 2 | 3 | import io.reflectoring.components.checkengine.api.CheckId 4 | import io.reflectoring.components.checkengine.internal.database.api.CheckMutations 5 | import io.reflectoring.components.common.database.Tables.CE_CHECK 6 | import io.reflectoring.components.common.database.Tables.CE_CHECK_FIX 7 | import io.reflectoring.components.common.database.tables.records.CeCheckRecord 8 | import org.jooq.DSLContext 9 | import org.jooq.Record8 10 | import org.jooq.Select 11 | import org.jooq.impl.DSL.case_ 12 | import org.jooq.impl.DSL.field 13 | import org.jooq.impl.DSL.max 14 | import org.jooq.impl.DSL.sum 15 | import org.springframework.stereotype.Component 16 | import java.time.LocalDateTime 17 | import java.util.UUID 18 | 19 | @Component 20 | class JooqCheckRepository( 21 | private val jooq: DSLContext 22 | ) : CheckMutations, io.reflectoring.components.checkengine.api.CheckQueries { 23 | 24 | override fun initializeCheck(checkRequest: io.reflectoring.components.checkengine.api.CheckRequest): io.reflectoring.components.checkengine.api.Check { 25 | val checkRecord = jooq.insertInto(CE_CHECK) 26 | .columns( 27 | CE_CHECK.KEY, 28 | CE_CHECK.TENANT_ID, 29 | CE_CHECK.PAGE_URL, 30 | CE_CHECK.START_DATE, 31 | CE_CHECK.EXECUTION_STATUS 32 | ) 33 | .values( 34 | checkRequest.checkKey.key, 35 | checkRequest.tenantId.toString(), 36 | checkRequest.pageUrl, 37 | LocalDateTime.now(), 38 | io.reflectoring.components.checkengine.api.ExecutionStatus.IN_PROGRESS.name 39 | ) 40 | .returning() 41 | .fetchOne() 42 | ?: throw java.lang.IllegalStateException("could not return row after insert!") 43 | 44 | return toDomainObject(checkRecord) 45 | } 46 | 47 | override fun updateCheck(check: io.reflectoring.components.checkengine.api.Check) { 48 | jooq.update(CE_CHECK) 49 | .set(CE_CHECK.END_DATE, LocalDateTime.now()) 50 | .set(CE_CHECK.EXECUTION_STATUS, check.executionStatus.name) 51 | .set(CE_CHECK.RESULT_STATUS, check.checkResult?.status?.name) 52 | .where(CE_CHECK.ID.eq(check.id.id.toInt())) 53 | .execute() 54 | 55 | check.checkResult?.fixes?.forEach { fix -> 56 | jooq.insertInto(CE_CHECK_FIX, CE_CHECK_FIX.CHECK_ID, CE_CHECK_FIX.FIX, CE_CHECK_FIX.CONDITION_KEY) 57 | .values(check.id.id, fix.message, fix.failedConditionKey.toString()) 58 | .execute() 59 | } 60 | } 61 | 62 | private fun toDomainObject(record: CeCheckRecord): io.reflectoring.components.checkengine.api.Check { 63 | 64 | val fixes = jooq.selectFrom(CE_CHECK_FIX) 65 | .where(CE_CHECK_FIX.CHECK_ID.eq(record.id.toLong())) 66 | .fetchStream() 67 | .map { 68 | io.reflectoring.components.checkengine.api.Fix( 69 | io.reflectoring.components.checkengine.api.ConditionKey(it.conditionKey), 70 | it.fix 71 | ) 72 | } 73 | .toList() 74 | 75 | val result = record.resultStatus?.let { 76 | io.reflectoring.components.checkengine.api.CheckResult( 77 | io.reflectoring.components.checkengine.api.ResultStatus.valueOf(record.resultStatus), 78 | fixes 79 | ) 80 | } 81 | 82 | return io.reflectoring.components.checkengine.api.Check( 83 | CheckId(record.id.toLong()), 84 | io.reflectoring.components.checkengine.api.CheckKey(record.key), 85 | UUID.fromString(record.tenantId), 86 | record.pageUrl, 87 | record.startDate, 88 | record.endDate, 89 | io.reflectoring.components.checkengine.api.ExecutionStatus.valueOf(record.executionStatus), 90 | result 91 | ) 92 | } 93 | 94 | private fun toDomainObject(record: Record8): io.reflectoring.components.checkengine.api.Check { 95 | val typedRecord = CeCheckRecord( 96 | record.value1(), 97 | record.value2(), 98 | record.value3(), 99 | record.value4(), 100 | record.value5(), 101 | record.value6(), 102 | record.value7(), 103 | record.value8() 104 | ) 105 | return toDomainObject(typedRecord) 106 | } 107 | 108 | override fun getCheck(checkId: CheckId): io.reflectoring.components.checkengine.api.Check? { 109 | return jooq.selectFrom(CE_CHECK) 110 | .where(CE_CHECK.ID.eq(checkId.id.toInt())) 111 | .fetchOne() 112 | ?.let { toDomainObject(it) } 113 | } 114 | 115 | override fun getLatestChecksCountByStatus(tenantId: UUID, status: io.reflectoring.components.checkengine.api.ResultStatus): Int { 116 | val latestChecks = latestSuccessfulChecks(tenantId).asTable("latestChecks") 117 | 118 | return jooq.fetchCount( 119 | jooq.selectFrom(latestChecks) 120 | .where(field("result_status").eq(status.name)) 121 | ) 122 | } 123 | 124 | /** 125 | * Subquery that returns the most current check of each type for a given site. 126 | */ 127 | private fun latestSuccessfulChecks(tenantId: UUID): Select> { 128 | return jooq.select( 129 | CE_CHECK.ID, 130 | CE_CHECK.KEY, 131 | CE_CHECK.TENANT_ID, 132 | CE_CHECK.PAGE_URL, 133 | CE_CHECK.START_DATE, 134 | CE_CHECK.END_DATE, 135 | CE_CHECK.EXECUTION_STATUS, 136 | CE_CHECK.RESULT_STATUS 137 | ) 138 | .distinctOn(CE_CHECK.TENANT_ID, CE_CHECK.PAGE_URL, CE_CHECK.KEY) 139 | .from(CE_CHECK) 140 | .where(CE_CHECK.TENANT_ID.eq(tenantId.toString())) 141 | .and(CE_CHECK.EXECUTION_STATUS.eq(io.reflectoring.components.checkengine.api.ExecutionStatus.SUCCESS.name)) 142 | .orderBy(CE_CHECK.TENANT_ID, CE_CHECK.PAGE_URL, CE_CHECK.KEY, CE_CHECK.END_DATE.desc()) 143 | } 144 | 145 | override fun getLatestChecksSummaries(tenantId: UUID): List { 146 | val latestChecks = latestSuccessfulChecks(tenantId).asTable("latestChecks") 147 | 148 | return jooq.select( 149 | field("page_url", String::class.java), 150 | max(field("end_date", LocalDateTime::class.java)), 151 | sum(case_().`when`(field("result_status", String::class.java).eq(io.reflectoring.components.checkengine.api.ResultStatus.PASSED.name), 1).else_(0)).`as`("passedCount"), 152 | sum(case_().`when`(field("result_status", String::class.java).eq(io.reflectoring.components.checkengine.api.ResultStatus.FAILED.name), 1).else_(0)).`as`("failedCount") 153 | ) 154 | .from(latestChecks) 155 | .groupBy(field("page_url", String::class.java)) 156 | .fetch() 157 | .stream() 158 | .map { 159 | io.reflectoring.components.checkengine.api.ChecksSummary( 160 | it.value1(), 161 | it.value2(), 162 | it.value3().toInt(), 163 | it.value4().toInt() 164 | ) 165 | } 166 | .toList() 167 | } 168 | 169 | override fun getLatestChecksByPage(tenantId: UUID, pageUrl: String): List { 170 | 171 | val latestChecks = latestSuccessfulChecks(tenantId).asTable("latestChecks") 172 | 173 | return jooq.select( 174 | field("id", Int::class.java), 175 | field("key", String::class.java), 176 | field("tenant_id", String::class.java), 177 | field("page_url", String::class.java), 178 | field("start_date", LocalDateTime::class.java), 179 | field("end_date", LocalDateTime::class.java), 180 | field("execution_status", String::class.java), 181 | field("result_status", String::class.java) 182 | ).from(latestChecks) 183 | .where(field("page_url").eq(pageUrl)) 184 | .fetch() 185 | .stream() 186 | .map { toDomainObject(it) } 187 | .toList() 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /components/check-engine/src/main/kotlin/io/reflectoring/components/checkengine/internal/queue/internal/CheckRequestListener.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.internal.queue.internal 2 | 3 | import io.reflectoring.components.checkengine.api.CheckRequest 4 | import io.reflectoring.components.checkengine.internal.checkrunner.api.CheckRunner 5 | import io.reflectoring.sqs.api.SqsMessageHandler 6 | import org.springframework.stereotype.Component 7 | 8 | @Component 9 | class CheckRequestListener( 10 | private val checkRunner: CheckRunner 11 | ) : SqsMessageHandler { 12 | 13 | override fun handle(check: CheckRequest) { 14 | checkRunner.runCheck(check) 15 | } 16 | 17 | override fun messageType(): Class { 18 | return CheckRequest::class.java 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /components/check-engine/src/main/kotlin/io/reflectoring/components/checkengine/internal/queue/internal/CheckRequestListenerRegistration.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.internal.queue.internal 2 | 3 | import com.amazonaws.services.sqs.AmazonSQS 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import io.reflectoring.components.checkengine.api.CheckRequest 6 | import io.reflectoring.sqs.api.DefaultExceptionHandler 7 | import io.reflectoring.sqs.api.DefaultSqsMessageHandlerRegistration 8 | import io.reflectoring.sqs.api.SqsMessageHandler 9 | import io.reflectoring.sqs.api.SqsMessagePollerProperties 10 | import org.springframework.beans.factory.annotation.Qualifier 11 | import org.springframework.beans.factory.annotation.Value 12 | import org.springframework.stereotype.Component 13 | import java.time.Duration 14 | 15 | @Component 16 | class CheckRequestListenerRegistration( 17 | private val eventListener: CheckRequestListener, 18 | @Value("\${check-engine.sqs.check-requests.queueUrl}") private val queueUrl: String, 19 | @Value("\${check-engine.sqs.check-requests.pollDelay}") private val pollDelay: Duration, 20 | @Qualifier("checkRequestSqsListenerClient") private val sqsClient: AmazonSQS, 21 | private val objectMapper: ObjectMapper 22 | ) : DefaultSqsMessageHandlerRegistration() { 23 | 24 | override fun messageHandler(): SqsMessageHandler { 25 | return eventListener 26 | } 27 | 28 | override fun name(): String { 29 | return "checkRequests" 30 | } 31 | 32 | override fun messagePollerProperties(): SqsMessagePollerProperties { 33 | return SqsMessagePollerProperties(queueUrl) 34 | .withExceptionHandler(DefaultExceptionHandler()) 35 | .withPollDelay(pollDelay) 36 | } 37 | 38 | override fun sqsClient(): AmazonSQS { 39 | return sqsClient 40 | } 41 | 42 | override fun objectMapper(): ObjectMapper { 43 | return objectMapper 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /components/check-engine/src/main/kotlin/io/reflectoring/components/checkengine/internal/queue/internal/CheckRequestListenerSqsConfiguration.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.internal.queue.internal 2 | 3 | import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration 4 | import com.amazonaws.services.sqs.AmazonSQS 5 | import com.amazonaws.services.sqs.AmazonSQSClientBuilder 6 | import com.fasterxml.jackson.databind.ObjectMapper 7 | import io.reflectoring.sqs.api.SqsQueueInitializer 8 | import org.springframework.beans.factory.annotation.Qualifier 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty 10 | import org.springframework.boot.context.properties.EnableConfigurationProperties 11 | import org.springframework.context.annotation.Bean 12 | import org.springframework.context.annotation.ComponentScan 13 | import org.springframework.context.annotation.Configuration 14 | 15 | @ComponentScan 16 | @Configuration 17 | @EnableConfigurationProperties(CheckRequestListenerSqsProperties::class) 18 | open class CheckRequestListenerSqsConfiguration { 19 | 20 | @Bean("checkRequestSqsListenerClient") 21 | fun checkRequestSqsClient(sqsProperties: CheckRequestListenerSqsProperties): AmazonSQS? { 22 | val builder = AmazonSQSClientBuilder.standard() 23 | if (sqsProperties.endpoint != null) { 24 | builder.withEndpointConfiguration( 25 | EndpointConfiguration( 26 | sqsProperties.endpoint, sqsProperties.region 27 | ) 28 | ) 29 | } else { 30 | builder.withRegion(sqsProperties.region) 31 | } 32 | return builder.build() 33 | } 34 | 35 | @Bean 36 | fun checkRequestPublisher( 37 | sqsProperties: CheckRequestListenerSqsProperties, 38 | @Qualifier("checkRequestSqsListenerClient") sqsClient: AmazonSQS, 39 | objectMapper: ObjectMapper 40 | ): CheckRequestPublisher { 41 | return CheckRequestPublisher(sqsProperties.queueUrl, sqsClient, objectMapper) 42 | } 43 | 44 | @Bean 45 | @ConditionalOnProperty(name = ["check-engine.sqs.check-requests.init"], havingValue = "true") 46 | fun checkRequestsQueueInitializer( 47 | @Qualifier("checkRequestSqsListenerClient") sqsClient: AmazonSQS 48 | ): SqsQueueInitializer? { 49 | return SqsQueueInitializer(sqsClient, "checkRequests") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /components/check-engine/src/main/kotlin/io/reflectoring/components/checkengine/internal/queue/internal/CheckRequestListenerSqsProperties.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.internal.queue.internal 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties 4 | 5 | @ConfigurationProperties("check-engine.sqs.check-requests") 6 | class CheckRequestListenerSqsProperties( 7 | val endpoint: String? = null, 8 | val region: String? = "us-east", 9 | val queueUrl: String 10 | ) 11 | -------------------------------------------------------------------------------- /components/check-engine/src/main/kotlin/io/reflectoring/components/checkengine/internal/queue/internal/CheckRequestPublisher.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.internal.queue.internal 2 | 3 | import com.amazonaws.services.sqs.AmazonSQS 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import io.reflectoring.components.checkengine.api.CheckRequest 6 | import io.reflectoring.sqs.api.SqsMessagePublisher 7 | 8 | class CheckRequestPublisher( 9 | sqsQueueUrl: String?, 10 | sqsClient: AmazonSQS?, 11 | objectMapper: ObjectMapper? 12 | ) : SqsMessagePublisher(sqsQueueUrl, sqsClient, objectMapper) 13 | -------------------------------------------------------------------------------- /components/check-engine/src/main/kotlin/io/reflectoring/components/checkengine/internal/queue/internal/SqsCheckScheduler.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.internal.queue.internal 2 | 3 | import io.reflectoring.components.checkengine.api.CheckRequest 4 | import io.reflectoring.components.checkengine.api.CheckScheduler 5 | import org.springframework.stereotype.Component 6 | 7 | @Component 8 | class SqsCheckScheduler( 9 | private val publisher: CheckRequestPublisher 10 | ) : CheckScheduler { 11 | 12 | override fun scheduleChecks(checks: List) { 13 | checks.forEach { 14 | publisher.publish(it) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /components/check-engine/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports: -------------------------------------------------------------------------------- 1 | io.reflectoring.components.checkengine.internal.CheckEngineAutoConfiguration 2 | -------------------------------------------------------------------------------- /components/check-engine/src/test/kotlin/io/reflectoring/components/checkengine/CheckEngineTestApplication.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication 4 | import org.springframework.boot.runApplication 5 | 6 | @SpringBootApplication 7 | class CheckEngineTestApplication 8 | 9 | fun main(args: Array) { 10 | runApplication(*args) 11 | } 12 | -------------------------------------------------------------------------------- /components/check-engine/src/test/kotlin/io/reflectoring/components/checkengine/internal/checkrunner/internal/CheckRunnerIntegrationTest.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.internal.checkrunner.internal 2 | 3 | import io.reflectoring.components.checkengine.api.CheckExecutor 4 | import io.reflectoring.components.checkengine.api.CheckKey 5 | import io.reflectoring.components.checkengine.api.CheckQueries 6 | import io.reflectoring.components.checkengine.api.CheckRequest 7 | import io.reflectoring.components.checkengine.api.CheckResult 8 | import io.reflectoring.components.checkengine.api.ConditionKey 9 | import io.reflectoring.components.checkengine.api.ExecutionStatus 10 | import io.reflectoring.components.checkengine.api.Fix 11 | import io.reflectoring.components.checkengine.api.ResultStatus 12 | import io.reflectoring.components.checkengine.internal.checkrunner.api.CheckRunner 13 | import io.reflectoring.components.checkengine.internal.database.api.CheckMutations 14 | import io.reflectoring.components.checkengine.internal.database.internal.CheckEngineDatabaseConfiguration 15 | import io.reflectoring.components.testcontainers.PostgreSQLTest 16 | import org.assertj.core.api.Assertions.assertThat 17 | import org.junit.jupiter.api.BeforeEach 18 | import org.junit.jupiter.api.Test 19 | import org.mockito.ArgumentMatchers.anyString 20 | import org.mockito.Mockito 21 | import org.mockito.kotlin.any 22 | import org.mockito.kotlin.given 23 | import org.springframework.beans.factory.annotation.Autowired 24 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase 25 | import org.springframework.context.annotation.Import 26 | import org.springframework.test.context.TestPropertySource 27 | import java.util.UUID 28 | 29 | @PostgreSQLTest 30 | @Import(CheckEngineDatabaseConfiguration::class) 31 | @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 32 | @TestPropertySource(properties = ["debug=true"]) 33 | internal class CheckRunnerIntegrationTest { 34 | 35 | @Autowired 36 | lateinit var checkMutations: CheckMutations 37 | 38 | @Autowired 39 | lateinit var checkQueries: CheckQueries 40 | 41 | lateinit var checkResolver: CheckResolver 42 | 43 | lateinit var checkRunner: CheckRunner 44 | 45 | @BeforeEach 46 | fun init() { 47 | checkResolver = Mockito.mock(CheckResolver::class.java) 48 | checkRunner = DefaultCheckRunner(checkResolver, checkMutations) 49 | } 50 | 51 | @Test 52 | fun testPassedCheck() { 53 | // given 54 | val checkRequest = checkRequest() 55 | val passedResult = passedResult() 56 | val executor = Mockito.mock(CheckExecutor::class.java) 57 | given(checkResolver.getExecutorFor(any())).willReturn(executor) 58 | given(executor.execute(any(), anyString())).willReturn(passedResult) 59 | 60 | // when we run the check 61 | val check = checkRunner.runCheck(checkRequest) 62 | 63 | // then the check is updated in the database 64 | val checkInDatabase = checkQueries.getCheck(check.id) 65 | assertThat(checkInDatabase?.pageUrl).isEqualTo(checkRequest.pageUrl) 66 | assertThat(checkInDatabase?.key).isEqualTo(checkRequest.checkKey) 67 | assertThat(checkInDatabase?.executionStatus).isEqualTo(ExecutionStatus.SUCCESS) 68 | assertThat(checkInDatabase?.checkResult).isEqualTo(passedResult) 69 | assertThat(checkInDatabase?.startDate).isNotNull 70 | assertThat(checkInDatabase?.endDate).isNotNull 71 | } 72 | 73 | @Test 74 | fun testFailedCheck() { 75 | // given 76 | val checkRequest = checkRequest() 77 | val failedResult = failedResult() 78 | val executor = Mockito.mock(CheckExecutor::class.java) 79 | given(checkResolver.getExecutorFor(any())).willReturn(executor) 80 | given(executor.execute(any(), anyString())).willReturn(failedResult) 81 | 82 | // when we run the check 83 | val check = checkRunner.runCheck(checkRequest) 84 | 85 | // then the check is updated in the database 86 | val checkInDatabase = checkQueries.getCheck(check.id) 87 | assertThat(checkInDatabase?.pageUrl).isEqualTo(checkRequest.pageUrl) 88 | assertThat(checkInDatabase?.key).isEqualTo(checkRequest.checkKey) 89 | assertThat(checkInDatabase?.executionStatus).isEqualTo(ExecutionStatus.SUCCESS) 90 | assertThat(checkInDatabase?.checkResult).isEqualTo(failedResult) 91 | assertThat(checkInDatabase?.checkResult?.fixes?.size).isEqualTo(failedResult.fixes.size) 92 | assertThat(checkInDatabase?.startDate).isNotNull 93 | assertThat(checkInDatabase?.endDate).isNotNull 94 | } 95 | 96 | @Test 97 | fun testErroredCheck() { 98 | // given 99 | val checkRequest = checkRequest() 100 | val passedResult = passedResult() 101 | val executor = Mockito.mock(CheckExecutor::class.java) 102 | given(checkResolver.getExecutorFor(any())).willThrow(java.lang.IllegalStateException("BWAAAAH!")) 103 | given(executor.execute(any(), anyString())).willReturn(passedResult) 104 | 105 | // when we run the check 106 | val check = checkRunner.runCheck(checkRequest) 107 | 108 | // then the check is updated in the database 109 | val checkInDatabase = checkQueries.getCheck(check.id) 110 | assertThat(checkInDatabase?.pageUrl).isEqualTo(checkRequest.pageUrl) 111 | assertThat(checkInDatabase?.key).isEqualTo(checkRequest.checkKey) 112 | assertThat(checkInDatabase?.executionStatus).isEqualTo(ExecutionStatus.ERROR) 113 | assertThat(checkInDatabase?.checkResult).isNull() 114 | assertThat(checkInDatabase?.startDate).isNotNull 115 | assertThat(checkInDatabase?.endDate).isNotNull 116 | } 117 | 118 | private fun checkRequest(): CheckRequest { 119 | return CheckRequest(CheckKey("ABC-123"), UUID.randomUUID(), "https://page.url") 120 | } 121 | 122 | private fun passedResult(): CheckResult { 123 | return CheckResult(ResultStatus.PASSED) 124 | } 125 | 126 | private fun failedResult(): CheckResult { 127 | return CheckResult( 128 | ResultStatus.FAILED, 129 | listOf( 130 | Fix(ConditionKey("ABC-123-1"), "fix this"), 131 | Fix(ConditionKey("ABC-123-2"), "fix that") 132 | ) 133 | ) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /components/check-engine/src/test/kotlin/io/reflectoring/components/checkengine/internal/database/internal/JooqCheckRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components.checkengine.internal.database.internal 2 | 3 | import io.reflectoring.components.checkengine.api.CheckKey 4 | import io.reflectoring.components.checkengine.api.CheckRequest 5 | import io.reflectoring.components.checkengine.api.CheckResult 6 | import io.reflectoring.components.checkengine.api.ResultStatus 7 | import io.reflectoring.components.testcontainers.PostgreSQLTest 8 | import org.assertj.core.api.Assertions.assertThat 9 | import org.junit.jupiter.api.Test 10 | import org.springframework.beans.factory.annotation.Autowired 11 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase 12 | import org.springframework.context.annotation.Import 13 | import java.util.UUID 14 | 15 | @PostgreSQLTest 16 | @Import(CheckEngineDatabaseConfiguration::class) 17 | @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 18 | internal class JooqCheckRepositoryTest { 19 | 20 | @Autowired 21 | lateinit var repository: JooqCheckRepository 22 | 23 | @Test 24 | fun getCheckCountByStatus() { 25 | val tenantId = UUID.randomUUID() 26 | val check1 = repository.initializeCheck(CheckRequest(CheckKey("ABC-123"), tenantId, "https://page1.url")) 27 | val check2 = repository.initializeCheck(CheckRequest(CheckKey("ABC-123"), tenantId, "https://page2.url")) 28 | val check3 = repository.initializeCheck(CheckRequest(CheckKey("SEO-42"), tenantId, "https://page1.url")) 29 | val check4 = repository.initializeCheck(CheckRequest(CheckKey("SEO-42"), tenantId, "https://page2.url")) 30 | 31 | check1.setResult(CheckResult(ResultStatus.PASSED)) 32 | check2.setResult(CheckResult(ResultStatus.PASSED)) 33 | check3.setResult(CheckResult(ResultStatus.FAILED)) 34 | check4.setResult(CheckResult(ResultStatus.FAILED)) 35 | 36 | repository.updateCheck(check1) 37 | repository.updateCheck(check2) 38 | repository.updateCheck(check3) 39 | repository.updateCheck(check4) 40 | 41 | assertThat(repository.getLatestChecksCountByStatus(tenantId, ResultStatus.PASSED)).isEqualTo(2) 42 | assertThat(repository.getLatestChecksCountByStatus(tenantId, ResultStatus.FAILED)).isEqualTo(2) 43 | } 44 | 45 | @Test 46 | fun getChecksByPage() { 47 | val tenantId = UUID.randomUUID() 48 | val check1 = repository.initializeCheck(CheckRequest(CheckKey("ABC-123"), tenantId, "https://page1.url")) 49 | val check2 = repository.initializeCheck(CheckRequest(CheckKey("ABC-123"), tenantId, "https://page2.url")) 50 | val check3 = repository.initializeCheck(CheckRequest(CheckKey("SEO-42"), tenantId, "https://page1.url")) 51 | val check4 = repository.initializeCheck(CheckRequest(CheckKey("SEO-42"), tenantId, "https://page2.url")) 52 | val check5 = repository.initializeCheck(CheckRequest(CheckKey("SEO-42"), tenantId, "https://page1.url")) 53 | val check6 = repository.initializeCheck(CheckRequest(CheckKey("SEO-42"), tenantId, "https://page2.url")) 54 | 55 | check1.setResult(CheckResult(ResultStatus.PASSED)) 56 | check2.setResult(CheckResult(ResultStatus.PASSED)) 57 | check3.setResult(CheckResult(ResultStatus.FAILED)) 58 | check4.setResult(CheckResult(ResultStatus.FAILED)) 59 | check5.setExecutionError() 60 | check6.setExecutionError() 61 | 62 | repository.updateCheck(check1) 63 | repository.updateCheck(check2) 64 | repository.updateCheck(check3) 65 | repository.updateCheck(check4) 66 | repository.updateCheck(check5) 67 | repository.updateCheck(check6) 68 | 69 | val pages = repository.getLatestChecksByPage(tenantId, "https://page1.url") 70 | assertThat(pages).hasSize(2) 71 | 72 | val pages2 = repository.getLatestChecksByPage(tenantId, "https://page2.url") 73 | assertThat(pages2).hasSize(2) 74 | } 75 | 76 | @Test 77 | fun getChecksSummaries() { 78 | val tenantId = UUID.randomUUID() 79 | val check1 = repository.initializeCheck(CheckRequest(CheckKey("ABC-123"), tenantId, "https://page1.url")) 80 | val check2 = repository.initializeCheck(CheckRequest(CheckKey("ABC-123"), tenantId, "https://page2.url")) 81 | val check3 = repository.initializeCheck(CheckRequest(CheckKey("SEO-42"), tenantId, "https://page1.url")) 82 | val check4 = repository.initializeCheck(CheckRequest(CheckKey("SEO-42"), tenantId, "https://page2.url")) 83 | val check5 = repository.initializeCheck(CheckRequest(CheckKey("SEO-42"), tenantId, "https://page1.url")) 84 | val check6 = repository.initializeCheck(CheckRequest(CheckKey("SEO-42"), tenantId, "https://page2.url")) 85 | 86 | check1.setResult(CheckResult(ResultStatus.PASSED)) 87 | check2.setResult(CheckResult(ResultStatus.PASSED)) 88 | check3.setResult(CheckResult(ResultStatus.FAILED)) 89 | check4.setResult(CheckResult(ResultStatus.FAILED)) 90 | check5.setExecutionError() 91 | check6.setExecutionError() 92 | 93 | repository.updateCheck(check1) 94 | repository.updateCheck(check2) 95 | repository.updateCheck(check3) 96 | repository.updateCheck(check4) 97 | repository.updateCheck(check5) 98 | repository.updateCheck(check6) 99 | 100 | val summaries = repository.getLatestChecksSummaries(tenantId) 101 | assertThat(summaries).hasSize(2) 102 | assertThat(summaries).filteredOn("pageUrl", "https://page1.url").hasSize(1) 103 | assertThat(summaries).filteredOn("pageUrl", "https://page2.url").hasSize(1) 104 | assertThat(summaries).filteredOn("passedChecks", 1).hasSize(2) 105 | assertThat(summaries).filteredOn("failedChecks", 1).hasSize(2) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thombergs/components-example/aa8d11c34e429238435857d7356fb75d5bf29e01/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Stop when "xargs" is not available. 209 | if ! command -v xargs >/dev/null 2>&1 210 | then 211 | die "xargs is not available" 212 | fi 213 | 214 | # Use "xargs" to parse quoted args. 215 | # 216 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 217 | # 218 | # In Bash we could simply go: 219 | # 220 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 221 | # set -- "${ARGS[@]}" "$@" 222 | # 223 | # but POSIX shell has neither arrays nor command substitution, so instead we 224 | # post-process each arg (as a line of input to sed) to backslash-escape any 225 | # character that might be a shell metacharacter, then use eval to reverse 226 | # that process (while maintaining the separation between arguments), and wrap 227 | # the whole thing up as a single "set" statement. 228 | # 229 | # This will of course break if any of these variables contains a newline or 230 | # an unmatched quote. 231 | # 232 | 233 | eval "set -- $( 234 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 235 | xargs -n1 | 236 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 237 | tr '\n' ' ' 238 | )" '"$@"' 239 | 240 | exec "$JAVACMD" "$@" 241 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /server/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' 3 | id 'java' 4 | } 5 | 6 | dependencies { 7 | // internal modules 8 | implementation project(':components:check-engine') 9 | 10 | // Spring Boot dependencies 11 | implementation 'org.springframework.boot:spring-boot-starter' 12 | implementation 'org.springframework.boot:spring-boot-starter-actuator' 13 | implementation 'org.springframework.session:spring-session-jdbc' 14 | 15 | // reflections 16 | testImplementation 'org.reflections:reflections' 17 | testImplementation 'com.tngtech.archunit:archunit-junit5' 18 | 19 | // Fix for M1 processor 20 | implementation 'io.netty:netty-resolver-dns-native-macos' 21 | } 22 | 23 | bootRun { 24 | jvmArgs = [ 25 | "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005", 26 | "-Dspring.profiles.active=local", 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /server/src/main/kotlin/io/reflectoring/components/Application.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components 2 | 3 | import org.springframework.boot.SpringApplication 4 | import org.springframework.boot.autoconfigure.SpringBootApplication 5 | 6 | @SpringBootApplication 7 | class Application 8 | 9 | fun main(args: Array) { 10 | SpringApplication.run(Application::class.java, *args) 11 | } 12 | -------------------------------------------------------------------------------- /server/src/test/kotlin/io/reflectoring/components/InternalPackageTest.kt: -------------------------------------------------------------------------------- 1 | package io.reflectoring.components 2 | 3 | import com.tngtech.archunit.core.importer.ClassFileImporter 4 | import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses 5 | import org.assertj.core.api.Assertions.assertThat 6 | import org.junit.jupiter.api.Test 7 | import org.reflections.Reflections 8 | import org.reflections.scanners.SubTypesScanner 9 | import java.io.IOException 10 | 11 | 12 | class InternalPackageTest { 13 | 14 | private val BASE_PACKAGE = "io.reflectoring.components" 15 | private val analyzedClasses = ClassFileImporter().importPackages(BASE_PACKAGE) 16 | 17 | @Test 18 | @Throws(IOException::class) 19 | fun internalPackagesAreNotAccessedFromOutside() { 20 | 21 | // so that the test will break when the base package is re-named 22 | assertPackageExists(BASE_PACKAGE) 23 | val internalPackages = internalPackages(BASE_PACKAGE) 24 | for (internalPackage in internalPackages) { 25 | assertPackageExists(internalPackage) 26 | assertPackageIsNotAccessedFromOutside(internalPackage) 27 | } 28 | } 29 | 30 | /** 31 | * Finds all packages named "internal". 32 | */ 33 | private fun internalPackages(basePackage: String): List { 34 | val scanner = SubTypesScanner(false); 35 | val reflections = Reflections(basePackage, scanner) 36 | return reflections.getSubTypesOf(Object::class.java).map { 37 | it.`package`.name 38 | }.filter { 39 | it.endsWith(".internal") 40 | } 41 | } 42 | 43 | private fun assertPackageIsNotAccessedFromOutside(internalPackage: String) { 44 | noClasses() 45 | .that() 46 | .resideOutsideOfPackage(packageMatcher(internalPackage)) 47 | .should() 48 | .dependOnClassesThat() 49 | .resideInAPackage(packageMatcher(internalPackage)) 50 | .check(analyzedClasses) 51 | } 52 | 53 | private fun assertPackageExists(packageName: String?) { 54 | assertThat(analyzedClasses.containPackage(packageName)) 55 | .`as`("package %s exists", packageName) 56 | .isTrue() 57 | } 58 | 59 | private fun packageMatcher(fullyQualifiedPackage: String): String? { 60 | return "$fullyQualifiedPackage.." 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'components' 2 | 3 | include 'components:check-engine' 4 | include 'common:database' 5 | include 'common:testcontainers' 6 | include 'server' --------------------------------------------------------------------------------