diff --git a/backend/build.gradle b/backend/build.gradle index 5a2aadc..ab084a5 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -37,10 +37,14 @@ dependencies { implementation 'org.springframework.experimental.ai:spring-ai-tika-document-reader:0.7.1-SNAPSHOT' implementation 'org.springframework.experimental.ai:spring-ai-pgvector-store:0.7.1-SNAPSHOT' implementation 'org.liquibase:liquibase-core' + implementation 'net.javacrumbs.shedlock:shedlock-spring:5.2.0' + implementation 'net.javacrumbs.shedlock:shedlock-provider-jdbc-template:5.2.0' implementation 'com.pgvector:pgvector:0.1.3' runtimeOnly 'org.postgresql:postgresql' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' + testImplementation 'com.tngtech.archunit:archunit-junit5:1.1.0' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } bootJar { diff --git a/backend/src/main/java/ch/xxx/aidoclibchat/adapter/config/ForwardServletFilter.java b/backend/src/main/java/ch/xxx/aidoclibchat/adapter/config/ForwardServletFilter.java index 1b78aac..5da4407 100644 --- a/backend/src/main/java/ch/xxx/aidoclibchat/adapter/config/ForwardServletFilter.java +++ b/backend/src/main/java/ch/xxx/aidoclibchat/adapter/config/ForwardServletFilter.java @@ -37,7 +37,7 @@ @WebFilter @Component public class ForwardServletFilter implements Filter { - private static final Logger LOG = LoggerFactory.getLogger(ForwardServletFilter.class); + private static final Logger LOGGER = LoggerFactory.getLogger(ForwardServletFilter.class); public static final List SUPPORTED_LOCALES = List.of(Locale.ENGLISH, Locale.GERMAN); public static final List REST_PATHS = List.of("/rest", "/actuator", "/h2-console", "/swagger-ui.html", "/swagger-ui", "/v3"); diff --git a/backend/src/main/java/ch/xxx/aidoclibchat/adapter/config/SecurityConfig.java b/backend/src/main/java/ch/xxx/aidoclibchat/adapter/config/SecurityConfig.java index a576ae6..0d9bce6 100644 --- a/backend/src/main/java/ch/xxx/aidoclibchat/adapter/config/SecurityConfig.java +++ b/backend/src/main/java/ch/xxx/aidoclibchat/adapter/config/SecurityConfig.java @@ -29,6 +29,7 @@ @EnableWebSecurity @Order(SecurityProperties.DEFAULT_FILTER_ORDER) public class SecurityConfig { +// private static final Logger LOGGER = LoggerFactory.getLogger(SecurityConfig.class); /* private final JwtTokenService jwtTokenService; diff --git a/backend/src/main/java/ch/xxx/aidoclibchat/domain/exceptions/DocumentException.java b/backend/src/main/java/ch/xxx/aidoclibchat/domain/exceptions/DocumentException.java new file mode 100644 index 0000000..cf62740 --- /dev/null +++ b/backend/src/main/java/ch/xxx/aidoclibchat/domain/exceptions/DocumentException.java @@ -0,0 +1,9 @@ +package ch.xxx.aidoclibchat.domain.exceptions; + +public class DocumentException extends RuntimeException { + private static final long serialVersionUID = -5601313921637319936L; + + public DocumentException(String message, Throwable th) { + super(message, th); + } +} diff --git a/backend/src/main/java/ch/xxx/aidoclibchat/usecase/mapping/DocumentMapper.java b/backend/src/main/java/ch/xxx/aidoclibchat/usecase/mapping/DocumentMapper.java index 73d199f..185839e 100644 --- a/backend/src/main/java/ch/xxx/aidoclibchat/usecase/mapping/DocumentMapper.java +++ b/backend/src/main/java/ch/xxx/aidoclibchat/usecase/mapping/DocumentMapper.java @@ -21,6 +21,7 @@ import org.springframework.web.multipart.MultipartFile; import ch.xxx.aidoclibchat.domain.common.DocumentType; +import ch.xxx.aidoclibchat.domain.exceptions.DocumentException; import ch.xxx.aidoclibchat.domain.model.dto.AiResult; import ch.xxx.aidoclibchat.domain.model.dto.DocumentDto; import ch.xxx.aidoclibchat.domain.model.dto.DocumentSearchDto; @@ -28,6 +29,7 @@ @Component public class DocumentMapper { + public Document toEntity(MultipartFile multipartFile) { var entity = new Document(); try { @@ -36,7 +38,7 @@ public Document toEntity(MultipartFile multipartFile) { entity.setDocumentType(Optional.ofNullable(multipartFile.getContentType()).stream() .map(this::toDocumentType).findFirst().orElse(DocumentType.UNKNOWN)); } catch (IOException e) { - throw new RuntimeException(e); + throw new DocumentException("IOException", e); } return entity; } diff --git a/backend/src/test/java/ch/xxx/aidocumentlibrarychat/AidocumentlibrarychatApplicationTests.java b/backend/src/test/java/ch/xxx/aidoclibchat/AidocumentlibrarychatApplicationTests.java similarity index 95% rename from backend/src/test/java/ch/xxx/aidocumentlibrarychat/AidocumentlibrarychatApplicationTests.java rename to backend/src/test/java/ch/xxx/aidoclibchat/AidocumentlibrarychatApplicationTests.java index 5fa470c..00c83d4 100644 --- a/backend/src/test/java/ch/xxx/aidocumentlibrarychat/AidocumentlibrarychatApplicationTests.java +++ b/backend/src/test/java/ch/xxx/aidoclibchat/AidocumentlibrarychatApplicationTests.java @@ -10,7 +10,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package ch.xxx.aidocumentlibrarychat; +package ch.xxx.aidoclibchat; import org.springframework.boot.test.context.SpringBootTest; diff --git a/backend/src/test/java/ch/xxx/aidoclibchat/architecture/MyArchitectureTests.java b/backend/src/test/java/ch/xxx/aidoclibchat/architecture/MyArchitectureTests.java new file mode 100644 index 0000000..e6a1800 --- /dev/null +++ b/backend/src/test/java/ch/xxx/aidoclibchat/architecture/MyArchitectureTests.java @@ -0,0 +1,144 @@ +/** + * Copyright 2023 Sven Loesekann + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +package ch.xxx.aidoclibchat.architecture; + +import static com.tngtech.archunit.lang.conditions.ArchConditions.beAnnotatedWith; + +import java.util.List; +import java.util.regex.Pattern; + +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.web.reactive.error.DefaultErrorAttributes; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.web.bind.annotation.RestController; + +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.domain.JavaField; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeTests; +import com.tngtech.archunit.core.importer.Location; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchCondition; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.lang.CompositeArchRule; +import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; +import com.tngtech.archunit.library.Architectures; +import com.tngtech.archunit.library.GeneralCodingRules; +import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition; + +import ch.xxx.aidoclibchat.architecture.MyArchitectureTests.DoNotIncludeGenerated; +import jakarta.annotation.PostConstruct; +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; + +@AnalyzeClasses(packages = "ch.xxx.aaidoclibchat", importOptions = { DoNotIncludeTests.class, + DoNotIncludeGenerated.class }) +public class MyArchitectureTests { + private static final Logger LOGGER = LoggerFactory.getLogger(MyArchitectureTests.class); + private static final ArchRule NO_CLASSES_SHOULD_USE_FIELD_INJECTION = createNoFieldInjectionRule(); + + private JavaClasses importedClasses = new ClassFileImporter() + .withImportOptions(List.of(new DoNotIncludeTests(), new DoNotIncludeGenerated())) + .importPackages("ch.xxx.aidoclibchat"); + + @ArchTest + static final ArchRule clean_architecture_respected = Architectures.onionArchitecture().domainModels("..domain..") + .applicationServices("..usecase..").adapter("rest", "..adapter.controller..") +// .adapter("cron", "..adapter.cron..") + .adapter("repo", "..adapter.repository..") +// .adapter("events", "..adapter.events..") +// .adapter("client", "..adapter.client..") + .adapter("config", "..adapter.config..").withOptionalLayers(true); + + @Test + public void cyclesDomain() { + SlicesRuleDefinition.slices().matching("..domain.(*)..").should().beFreeOfCycles(); + } + + @Test + public void cyclesUseCases() { + SlicesRuleDefinition.slices().matching("..usecase.(*)..").should().beFreeOfCycles(); + } + + @Test + public void cyclesAdapter() { + SlicesRuleDefinition.slices().matching("..adapter.(*)..").should().beFreeOfCycles(); + } + + @Test + public void ruleControllerAnnotations() { + ArchRule beAnnotatedWith = ArchRuleDefinition.classes().that().resideInAPackage("..adapter.controller..") + .should().beAnnotatedWith(RestController.class).orShould().beAnnotatedWith(Configuration.class); + beAnnotatedWith.check(this.importedClasses); + } + + @Test + public void ruleExceptionsType() { + ArchRule exceptionType = ArchRuleDefinition.classes().that().resideInAPackage("..domain.exceptions..").should() + .beAssignableTo(RuntimeException.class).orShould().beAssignableTo(DefaultErrorAttributes.class); + exceptionType.check(this.importedClasses); + } + +// @Test + public void ruleCronJobMethodsAnnotations() { + ArchRule exceptionType = ArchRuleDefinition.methods().that().arePublic().and().areDeclaredInClassesThat() + .resideInAPackage("..adapter.cron..").should().beAnnotatedWith(PostConstruct.class).orShould() + .beAnnotatedWith(Scheduled.class).andShould().beAnnotatedWith(SchedulerLock.class).orShould() + .beAnnotatedWith(Order.class); + exceptionType.check(this.importedClasses); + } + + @Test + public void ruleGeneralCodingRulesLoggers() { + ArchRuleDefinition.fields().that().haveRawType(Logger.class).should().bePrivate().andShould().beStatic() + .andShould().beFinal().because("we agreed on this convention").check(this.importedClasses); + } + + @Test + public void ruleGeneralCodingRules() { + ArchRule archRule = CompositeArchRule.of(GeneralCodingRules.NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS) + .and(NO_CLASSES_SHOULD_USE_FIELD_INJECTION).because("Good practice"); + JavaClasses classesToCheck = this.importedClasses + .that(JavaClass.Predicates.resideOutsideOfPackages("..adapter.clients.test..")); + archRule.check(classesToCheck); + } + + private static ArchRule createNoFieldInjectionRule() { + ArchCondition annotatedWithSpringAutowired = beAnnotatedWith( + "org.springframework.beans.factory.annotation.Autowired"); + ArchCondition annotatedWithGuiceInject = beAnnotatedWith("com.google.inject.Inject"); + ArchCondition annotatedWithJakartaInject = beAnnotatedWith("javax.inject.Inject"); + ArchRule beAnnotatedWithAnInjectionAnnotation = ArchRuleDefinition.noFields() + .should(annotatedWithSpringAutowired.or(annotatedWithGuiceInject).or(annotatedWithJakartaInject) + .as("be annotated with an injection annotation")); + return beAnnotatedWithAnInjectionAnnotation; + } + + static final class DoNotIncludeGenerated implements ImportOption { + private static final Pattern GENERATED_CLASSES_PATTERN = Pattern.compile(".+\\$\\d+\\.class$"); + private static final Pattern AOT_GENERATED_PATTERN = Pattern + .compile(".*(__BeanDefinitions|SpringCGLIB\\$\\$0)\\.class$"); + private static final Pattern AOT_TEST_GENERATED_PATTERN = Pattern.compile(".*__TestContext.*\\.class$"); + + @Override + public boolean includes(Location location) { + return !(location.matches(AOT_GENERATED_PATTERN) || location.matches(AOT_TEST_GENERATED_PATTERN) || location.matches(GENERATED_CLASSES_PATTERN)); + } + } +}