From d489131b0dc4c0a5bfc13f6b82576a99d2048f25 Mon Sep 17 00:00:00 2001 From: Maximilian Inckmann Date: Fri, 11 Jul 2025 22:47:53 +0200 Subject: [PATCH 01/19] first modulith setup Signed-off-by: Maximilian Inckmann --- README.md | 26 +- build.gradle | 36 +- event-architecture-design.md | 284 ++++++++++++++ event-architecture-implementation.md | 112 ++++++ event-based-architecture.md | 148 ++++++++ modulith-migration-plan.md | 202 ++++++++++ response.md | 98 +++++ .../idoris/configuration/HateoasConfig.java | 136 +++++++ .../idoris/configuration/OpenAPIConfig.java | 50 +++ .../configuration/RepositoryRestConfig.java | 225 ----------- .../RestResponseEntityExceptionHandler.java | 63 ---- .../configuration/ValidationConfig.java | 122 ++++++ .../idoris/configuration/WebConfig.java | 56 +++ .../core/events/AbstractDomainEvent.java | 54 +++ .../idoris/core/events/DomainEvent.java | 40 ++ .../core/events/EntityCreatedEvent.java | 52 +++ .../core/events/EntityDeletedEvent.java | 74 ++++ .../core/events/EntityImportedEvent.java | 118 ++++++ .../core/events/EntityUpdatedEvent.java | 64 ++++ .../core/events/EventPublisherService.java | 143 +++++++ .../events/GenericEntityCreatedEvent.java | 60 +++ .../events/GenericEntityDeletedEvent.java | 60 +++ .../events/GenericEntityUpdatedEvent.java | 60 +++ .../idoris/core/events/PIDGeneratedEvent.java | 87 +++++ .../core/events/SchemaGeneratedEvent.java | 86 +++++ .../core/events/VersionCreatedEvent.java | 101 +++++ .../datamanager/idoris/core/package-info.java | 29 ++ .../idoris/dao/IAtomicDataTypeDao.java | 13 +- .../datamanager/idoris/dao/IAttributeDao.java | 2 - .../idoris/dao/IAttributeMappingDao.java | 45 +++ .../datamanager/idoris/dao/IDataTypeDao.java | 19 +- .../datamanager/idoris/dao/IGenericRepo.java | 4 +- .../datamanager/idoris/dao/IOperationDao.java | 12 +- .../idoris/dao/ITechnologyInterfaceDao.java | 2 - .../idoris/dao/ITypeProfileDao.java | 2 - .../kit/datamanager/idoris/dao/IUserDao.java | 2 - .../idoris/domain/package-info.java | 29 ++ .../services/AtomicDataTypeService.java | 133 +++++++ .../services/AttributeMappingService.java | 156 ++++++++ .../domain/services/AttributeService.java | 144 +++++++ .../domain/services/OperationService.java | 145 ++++++++ .../services/TechnologyInterfaceService.java | 133 +++++++ .../domain/services/TypeProfileService.java | 172 +++++++++ .../idoris/domain/services/package-info.java | 22 ++ .../notification/EntityChangeNotifier.java | 222 +++++++++++ .../notification/EntityChangeSubscriber.java | 49 +++ .../LoggingEntityChangeSubscriber.java | 76 ++++ .../idoris/notification/package-info.java | 29 ++ .../pids/PIDGenerationEventListener.java | 74 ++++ .../idoris/pids/TypedPIDMakerIDGenerator.java | 2 +- .../datamanager/idoris/pids/package-info.java | 29 ++ .../idoris/repository/package-info.java | 29 ++ .../idoris/web/ValidationException.java | 34 ++ .../web/ValidationExceptionHandler.java | 52 +++ .../idoris/web/api/IAttributeApi.java | 179 +++++++++ .../web/api/ITechnologyInterfaceApi.java | 184 +++++++++ .../hateoas/AtomicDataTypeModelAssembler.java | 59 +++ .../web/hateoas/AttributeModelAssembler.java | 61 +++ .../web/hateoas/DataTypeModelAssembler.java | 71 ++++ .../web/hateoas/EntityModelAssembler.java | 51 +++ .../web/hateoas/OperationModelAssembler.java | 56 +++ .../TechnologyInterfaceModelAssembler.java | 57 +++ .../hateoas/TypeProfileModelAssembler.java | 65 ++++ .../datamanager/idoris/web/package-info.java | 29 ++ .../web/v1/AtomicDataTypeController.java | 274 ++++++++++++++ .../idoris/web/v1/AttributeController.java | 143 +++++++ .../idoris/web/v1/OperationController.java | 235 +++++++++++- .../idoris/web/v1/PidRedirectController.java | 2 +- .../web/v1/TechnologyInterfaceController.java | 166 +++++++++ .../idoris/web/v1/TypeProfileController.java | 350 ++++++++++++++---- src/main/resources/application.properties | 12 +- .../test-config/application.properties | 3 +- src/test/resources/application.properties | 3 +- .../test-config/application.properties | 3 +- 74 files changed, 5821 insertions(+), 399 deletions(-) create mode 100644 event-architecture-design.md create mode 100644 event-architecture-implementation.md create mode 100644 event-based-architecture.md create mode 100644 modulith-migration-plan.md create mode 100644 response.md create mode 100644 src/main/java/edu/kit/datamanager/idoris/configuration/HateoasConfig.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/configuration/OpenAPIConfig.java delete mode 100644 src/main/java/edu/kit/datamanager/idoris/configuration/RepositoryRestConfig.java delete mode 100644 src/main/java/edu/kit/datamanager/idoris/configuration/RestResponseEntityExceptionHandler.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/configuration/ValidationConfig.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/configuration/WebConfig.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/core/events/AbstractDomainEvent.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/core/events/DomainEvent.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/core/events/EntityCreatedEvent.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/core/events/EntityDeletedEvent.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/core/events/EntityImportedEvent.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/core/events/EntityUpdatedEvent.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/core/events/EventPublisherService.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityCreatedEvent.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityDeletedEvent.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityUpdatedEvent.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/core/events/PIDGeneratedEvent.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/core/events/SchemaGeneratedEvent.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/core/events/VersionCreatedEvent.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/core/package-info.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/dao/IAttributeMappingDao.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/domain/package-info.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/domain/services/AtomicDataTypeService.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/domain/services/AttributeMappingService.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/domain/services/AttributeService.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/domain/services/OperationService.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/domain/services/TechnologyInterfaceService.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/domain/services/TypeProfileService.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/domain/services/package-info.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeNotifier.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeSubscriber.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/notification/LoggingEntityChangeSubscriber.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/notification/package-info.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/pids/PIDGenerationEventListener.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/pids/package-info.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/repository/package-info.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/web/ValidationException.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/web/ValidationExceptionHandler.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/web/api/IAttributeApi.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/web/api/ITechnologyInterfaceApi.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/web/hateoas/AtomicDataTypeModelAssembler.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/web/hateoas/AttributeModelAssembler.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/web/hateoas/DataTypeModelAssembler.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/web/hateoas/EntityModelAssembler.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/web/hateoas/OperationModelAssembler.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/web/hateoas/TechnologyInterfaceModelAssembler.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/web/hateoas/TypeProfileModelAssembler.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/web/package-info.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/web/v1/AtomicDataTypeController.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/web/v1/AttributeController.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/web/v1/TechnologyInterfaceController.java diff --git a/README.md b/README.md index b9b46c6..5746de6 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,8 @@ logging.level.root=INFO spring.neo4j.uri=bolt://localhost:7687 spring.neo4j.authentication.username=neo4j spring.neo4j.authentication.password=superSecret -spring.data.rest.basePath=/api +# Base path for all REST endpoints +server.servlet.context-path=/api server.port=8095 idoris.validation-level=info idoris.validation-policy=strict @@ -71,3 +72,26 @@ When Neo4j is running, start IDORIS with the following command: ``` You can access the IDORIS API at http://localhost:8095/api. + +## Architecture + +IDORIS is built using Spring Boot and follows a modular, event-driven architecture using Spring Modulith. + +### Event-Based Architecture + +IDORIS uses an event-based architecture to decouple components and improve maintainability. Key events include: + +- EntityCreatedEvent: Published when a new entity is created +- EntityUpdatedEvent: Published when an entity is updated +- EntityDeletedEvent: Published when an entity is deleted +- PIDGeneratedEvent: Published when a PID is generated for an entity + +For more details, see [event-based-architecture.md](event-based-architecture.md). + +### API Documentation + +IDORIS provides comprehensive API documentation using OpenAPI/Swagger. You can access the API documentation +at http://localhost:8095/swagger-ui.html when the application is running. + +All endpoints support HATEOAS (Hypermedia as the Engine of Application State) and return HAL (Hypertext Application +Language) responses, making the API self-discoverable. diff --git a/build.gradle b/build.gradle index 8046799..00fddcd 100644 --- a/build.gradle +++ b/build.gradle @@ -43,8 +43,6 @@ java { } configurations { -// annotationProcessorPath - compileOnly { extendsFrom annotationProcessor } @@ -60,12 +58,13 @@ repositories { ext { springBootVersion = "3.5.0" - springDocVersion = "2.8.8" + springDocVersion = "2.8.9" errorproneVersion = "2.38.0" - errorproneJavacVersion = "9+181-r4173-1" // keep until a newer tag is published + errorproneJavacVersion = "9+181-r4173-1" httpClientVersion = "5.5" - javersVersion = "7.3.7" // unchanged (latest) + javersVersion = "7.3.7" set("snippetsDir", file("build/generated-snippets")) + set('springModulithVersion', "1.4.1") } dependencies { @@ -78,12 +77,19 @@ dependencies { /* Spring Boot starters (version comes from the BOM) */ implementation "org.springframework.boot:spring-boot-starter-web" implementation "org.springframework.boot:spring-boot-starter-data-neo4j" - implementation "org.springframework.boot:spring-boot-starter-data-rest" implementation "org.springframework.boot:spring-boot-starter-actuator" implementation "org.springframework.boot:spring-boot-starter-hateoas" implementation "org.springframework.boot:spring-boot-starter-validation" implementation "org.springframework:spring-web" - implementation "org.springframework.data:spring-data-rest-hal-explorer:5.0.0-M3" + implementation 'org.springframework.data:spring-data-rest-hal-explorer' + implementation "org.springframework.modulith:spring-modulith-starter-core" + implementation "org.springframework.modulith:spring-modulith-starter-neo4j:${springModulithVersion}" + implementation "org.springframework.modulith:spring-modulith-events-api:${springModulithVersion}" + runtimeOnly "org.springframework.boot:spring-boot-starter-actuator" + runtimeOnly 'org.springframework.modulith:spring-modulith-runtime' + runtimeOnly "org.springframework.modulith:spring-modulith-observability:${springModulithVersion}" + runtimeOnly "org.springframework.modulith:spring-modulith-actuator:${springModulithVersion}" +// runtimeOnly "org.springframework.modulith:spring-modulith-starter-insights:${springModulithVersion}" /* OpenAPI */ implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${springDocVersion}" @@ -115,7 +121,16 @@ dependencies { testImplementation "org.springframework.boot:spring-boot-starter-test" testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc:3.0.3" testImplementation "org.springframework.security:spring-security-test" + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.modulith:spring-modulith-starter-test' testImplementation "org.junit.jupiter:junit-jupiter:5.13.0" + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +dependencyManagement { + imports { + mavenBom "org.springframework.modulith:spring-modulith-bom:${springModulithVersion}" + } } // Modify JavaCompile tasks configuration @@ -132,11 +147,6 @@ tasks.withType(JavaCompile).configureEach { // Enable annotation processing explicitly options.fork = true -// options.forkOptions.jvmArgs += [ -// '-verbose', -//// '--add-opens', 'java.base/java.lang=ALL-UNNAMED', -//// '--add-opens', 'java.base/java.util=ALL-UNNAMED' -// ] options.errorprone { disableWarningsInGeneratedCode = true @@ -199,4 +209,4 @@ bootJar { into 'static/docs' } launchScript() -} \ No newline at end of file +} diff --git a/event-architecture-design.md b/event-architecture-design.md new file mode 100644 index 0000000..c619d2c --- /dev/null +++ b/event-architecture-design.md @@ -0,0 +1,284 @@ +# IDORIS Event-Driven Architecture Design + +## Introduction + +This document outlines the design for implementing an event-driven architecture in IDORIS using Spring Modulith. The +design is inspired by the approach used +in [piomin/sample-spring-modulith](https://github.com/piomin/sample-spring-modulith) and aims to support various use +cases including PID record creation, versioning, callbacks for entity changes, importing entities from existing DTRs, +and schema generation. + +## Goals + +- Implement a loosely coupled, event-driven architecture +- Support cross-module communication through domain events +- Enable asynchronous processing of business operations +- Provide a foundation for future microservices extraction +- Support specific use cases mentioned in the requirements + +## Event-Driven Architecture Overview + +The event-driven architecture will be based on Spring Modulith's ApplicationModuleListener mechanism, which provides: + +1. **Module Boundaries**: Clear separation between modules +2. **Event Publication**: Standardized way to publish domain events +3. **Event Subscription**: Type-safe event handling across module boundaries +4. **Transaction Management**: Events can be processed in the same or separate transactions + +## Domain Events + +We will define a hierarchy of domain events: + +``` +DomainEvent (base interface) +├── EntityCreatedEvent +├── EntityUpdatedEvent +├── EntityDeletedEvent +├── PIDGeneratedEvent +├── SchemaGeneratedEvent +└── EntityImportedEvent +``` + +Each event will contain relevant data and metadata about the operation that triggered it. + +## Module Structure + +The event-driven architecture will be organized around the following modules: + +1. **Core Module** + - Event definitions + - Common interfaces + - Base abstractions + +2. **Domain Module** + - Entity definitions + - Domain services + - Domain event publishers + +3. **PID Module** + - PID generation services + - PID record management + - Event listeners for entity lifecycle events + +4. **Versioning Module** + - Version tracking + - Change history + - Event listeners for entity updates + +5. **Schema Module** + - Schema generation + - Schema validation + - Event listeners for schema-related events + +6. **Import Module** + - Entity import services + - DTR connectors + - Event listeners for import-related events + +7. **Notification Module** + - Callback management + - Subscription services + - Event listeners for entity changes + +## Event Flow Examples + +### PID Record Creation + +1. An entity is created or updated in the Domain Module +2. The Domain Module publishes an EntityCreatedEvent or EntityUpdatedEvent +3. The PID Module listens for these events +4. The PID Module generates a PID using TypedPID-Maker +5. The PID Module publishes a PIDGeneratedEvent +6. Other modules can react to the PIDGeneratedEvent + +### Entity Versioning + +1. An entity is updated in the Domain Module +2. The Domain Module publishes an EntityUpdatedEvent +3. The Versioning Module listens for this event +4. The Versioning Module creates a new version record +5. The Versioning Module publishes a VersionCreatedEvent + +### Callbacks for Entity Changes + +1. A client subscribes to changes for a specific entity type +2. The entity is modified in the Domain Module +3. The Domain Module publishes an EntityUpdatedEvent +4. The Notification Module listens for this event +5. The Notification Module checks for subscriptions +6. The Notification Module sends callbacks to subscribers + +## Implementation Approach + +### 1. Spring Modulith Setup + +Add Spring Modulith dependencies to the project: + +```gradle +implementation 'org.springframework.experimental:spring-modulith-starter:1.1.0' +implementation 'org.springframework.experimental:spring-modulith-events:1.1.0' +testImplementation 'org.springframework.experimental:spring-modulith-test:1.1.0' +``` + +### 2. Domain Event Definitions + +Create base domain event interfaces and implementations: + +```java +public interface DomainEvent { + Instant getTimestamp(); + String getEventId(); +} + +public abstract class AbstractDomainEvent implements DomainEvent { + private final Instant timestamp = Instant.now(); + private final String eventId = UUID.randomUUID().toString(); + + @Override + public Instant getTimestamp() { + return timestamp; + } + + @Override + public String getEventId() { + return eventId; + } +} + +public class EntityCreatedEvent extends AbstractDomainEvent { + private final T entity; + + public EntityCreatedEvent(T entity) { + this.entity = entity; + } + + public T getEntity() { + return entity; + } +} +``` + +### 3. Event Publishers + +Implement event publishers in the domain services: + +```java +@Service +public class TypeProfileService { + private final ApplicationEventPublisher eventPublisher; + private final TypeProfileRepository repository; + + @Autowired + public TypeProfileService(ApplicationEventPublisher eventPublisher, TypeProfileRepository repository) { + this.eventPublisher = eventPublisher; + this.repository = repository; + } + + @Transactional + public TypeProfile createTypeProfile(TypeProfile typeProfile) { + TypeProfile saved = repository.save(typeProfile); + eventPublisher.publishEvent(new EntityCreatedEvent<>(saved)); + return saved; + } + + @Transactional + public TypeProfile updateTypeProfile(TypeProfile typeProfile) { + TypeProfile saved = repository.save(typeProfile); + eventPublisher.publishEvent(new EntityUpdatedEvent<>(saved)); + return saved; + } +} +``` + +### 4. Event Listeners + +Implement event listeners in the appropriate modules: + +```java +@Component +public class PIDGenerationEventListener { + private final TypedPIDMakerIDGenerator pidGenerator; + private final ApplicationEventPublisher eventPublisher; + + @Autowired + public PIDGenerationEventListener(TypedPIDMakerIDGenerator pidGenerator, ApplicationEventPublisher eventPublisher) { + this.pidGenerator = pidGenerator; + this.eventPublisher = eventPublisher; + } + + @EventListener + @Transactional + public void handleEntityCreatedEvent(EntityCreatedEvent event) { + GenericIDORISEntity entity = event.getEntity(); + if (entity.getPid() == null || entity.getPid().isEmpty()) { + String pid = pidGenerator.generateId(entity.getClass().getSimpleName(), entity); + entity.setPid(pid); + eventPublisher.publishEvent(new PIDGeneratedEvent<>(entity, pid)); + } + } +} +``` + +### 5. Transaction Management + +Configure transaction boundaries for event processing: + +```java +@Configuration +public class EventConfig { + @Bean + public TransactionalApplicationListener.Factory transactionalApplicationListenerFactory( + TransactionManager transactionManager) { + return TransactionalApplicationListener.factory(transactionManager); + } +} +``` + +## Use Case Implementation Details + +### PID Record Creation + +The PID Module will listen for entity lifecycle events and use the TypedPIDMakerIDGenerator to create or update PID +records. This decouples PID generation from entity creation and allows for retry mechanisms and better error handling. + +### Versioning + +The Versioning Module will maintain a history of entity changes by listening for EntityUpdatedEvents. It will store +previous versions of entities and provide APIs to retrieve and compare versions. + +### Callbacks for Entity Changes + +The Notification Module will allow clients to subscribe to entity changes and receive callbacks when those entities are +modified. It will maintain a registry of subscriptions and use event listeners to trigger notifications. + +### Importing Entities from DTRs + +The Import Module will provide services to import entities from external Digital Twin Registries. It will publish +EntityImportedEvents when entities are imported, allowing other modules to react accordingly. + +### Schema Generation + +The Schema Module will generate and validate schemas for entities. It will listen for entity lifecycle events and +generate or update schemas as needed. It will publish SchemaGeneratedEvents when schemas are created or updated. + +## Testing Strategy + +1. **Unit Tests**: Test individual components within modules +2. **Module Tests**: Test modules in isolation using Spring Modulith test support +3. **Integration Tests**: Test interactions between modules through events +4. **End-to-End Tests**: Verify complete workflows involving multiple modules + +## Implementation Phases + +1. **Phase 1**: Set up Spring Modulith and implement base event infrastructure +2. **Phase 2**: Implement PID record creation and versioning +3. **Phase 3**: Implement callbacks for entity changes +4. **Phase 4**: Implement entity import from DTRs +5. **Phase 5**: Implement schema generation + +## Conclusion + +This event-driven architecture design provides a solid foundation for implementing the required functionality in IDORIS. +It leverages Spring Modulith to create a modular, loosely coupled system that can evolve into microservices in the +future if needed. The event-based approach allows for asynchronous processing, better error handling, and clearer +separation of concerns. \ No newline at end of file diff --git a/event-architecture-implementation.md b/event-architecture-implementation.md new file mode 100644 index 0000000..fc5243d --- /dev/null +++ b/event-architecture-implementation.md @@ -0,0 +1,112 @@ +# IDORIS Event-Driven Architecture Implementation + +## Overview + +This document summarizes the implementation of an event-driven architecture in IDORIS using Spring Modulith. The +implementation follows the design outlined in the `event-architecture-design.md` document and provides a foundation for +the various use cases mentioned in the requirements. + +## Implemented Components + +### Core Event Infrastructure + +1. **DomainEvent Interface**: Base interface for all domain events in the system. + - File: `/src/main/java/edu/kit/datamanager/idoris/core/events/DomainEvent.java` + +2. **AbstractDomainEvent Class**: Abstract base class that implements the DomainEvent interface and provides common + functionality. + - File: `/src/main/java/edu/kit/datamanager/idoris/core/events/AbstractDomainEvent.java` + +3. **Entity Lifecycle Events**: + - `EntityCreatedEvent`: Published when a new entity is created. + - File: `/src/main/java/edu/kit/datamanager/idoris/core/events/EntityCreatedEvent.java` + - `EntityUpdatedEvent`: Published when an entity is updated. + - File: `/src/main/java/edu/kit/datamanager/idoris/core/events/EntityUpdatedEvent.java` + - `PIDGeneratedEvent`: Published when a PID is generated for an entity. + - File: `/src/main/java/edu/kit/datamanager/idoris/core/events/PIDGeneratedEvent.java` + +4. **EventPublisherService**: Service for publishing domain events. + - File: `/src/main/java/edu/kit/datamanager/idoris/core/events/EventPublisherService.java` + +### Event Listeners + +1. **PIDGenerationEventListener**: Listens for EntityCreatedEvent and generates PIDs for entities. + - File: `/src/main/java/edu/kit/datamanager/idoris/pids/PIDGenerationEventListener.java` + +### Spring Modulith Configuration + +1. **ModulithConfig**: Configuration class for Spring Modulith. + - File: `/src/main/java/edu/kit/datamanager/idoris/core/config/ModulithConfig.java` + +2. **Module Definitions**: + - Core Module: `/src/main/java/edu/kit/datamanager/idoris/core/package-info.java` + - Domain Module: `/src/main/java/edu/kit/datamanager/idoris/domain/package-info.java` + - PID Module: `/src/main/java/edu/kit/datamanager/idoris/pids/package-info.java` + +### Dependencies + +Added Spring Modulith dependencies to `build.gradle`: + +```gradle +implementation "org.springframework.experimental:spring-modulith-starter:${springModulithVersion}" +implementation "org.springframework.experimental:spring-modulith-events:${springModulithVersion}" +testImplementation "org.springframework.experimental:spring-modulith-test:${springModulithVersion}" +``` + +## How It Works + +1. **Entity Creation/Update**: + - When an entity is created or updated, the service layer publishes an `EntityCreatedEvent` or `EntityUpdatedEvent` + using the `EventPublisherService`. + +2. **PID Generation**: + - The `PIDGenerationEventListener` listens for `EntityCreatedEvent` and generates a PID for the entity if it doesn't + already have one. + - After generating a PID, it publishes a `PIDGeneratedEvent`. + +3. **Event Propagation**: + - Events are propagated across module boundaries using Spring Modulith's event externalization mechanism. + - Event listeners can be executed in a transaction using the `@Transactional` annotation. + +## Next Steps + +### 1. Implement Additional Event Types + +- `EntityDeletedEvent`: For entity deletion +- `SchemaGeneratedEvent`: For schema generation +- `EntityImportedEvent`: For entity import from DTRs + +### 2. Implement Additional Event Listeners + +- **Versioning Listener**: To maintain a history of entity changes +- **Notification Listener**: To send callbacks for entity changes +- **Schema Generation Listener**: To generate and validate schemas +- **Import Listener**: To handle entity import from DTRs + +### 3. Integrate with Existing Services + +- Modify existing service classes to publish events when entities are created, updated, or deleted. +- Update the TypedPIDMakerIDGenerator to work with the event-driven architecture. + +### 4. Create Module-Specific Services + +- **Versioning Service**: For managing entity versions +- **Notification Service**: For managing subscriptions and sending callbacks +- **Schema Service**: For generating and validating schemas +- **Import Service**: For importing entities from DTRs + +### 5. Testing + +- Write unit tests for event classes and listeners +- Write integration tests for event propagation across modules +- Use Spring Modulith's testing support to verify module boundaries + +## Conclusion + +The implemented event-driven architecture provides a solid foundation for the various use cases mentioned in the +requirements. It leverages Spring Modulith to create a modular, loosely coupled system that can evolve into +microservices in the future if needed. The event-based approach allows for asynchronous processing, better error +handling, and clearer separation of concerns. + +By following the next steps outlined above, the architecture can be extended to support all the required functionality +while maintaining the modularity and loose coupling of the system. \ No newline at end of file diff --git a/event-based-architecture.md b/event-based-architecture.md new file mode 100644 index 0000000..0dfbd9e --- /dev/null +++ b/event-based-architecture.md @@ -0,0 +1,148 @@ +# IDORIS Event-Based Architecture + +## Overview + +This document provides an overview of the event-based architecture implemented in IDORIS using Spring Modulith. The +architecture is designed to support various use cases including PID record creation, versioning, callbacks for entity +changes, importing entities from existing DTRs, and schema generation. + +## Architecture Components + +### 1. Domain Events + +Domain events represent significant occurrences within the system. They are used to communicate between modules in a +loosely coupled way. The following domain events have been implemented: + +- **EntityCreatedEvent**: Published when a new entity is created +- **EntityUpdatedEvent**: Published when an entity is updated +- **EntityDeletedEvent**: Published when an entity is deleted +- **PIDGeneratedEvent**: Published when a PID is generated for an entity +- **SchemaGeneratedEvent**: Published when a schema is generated for an entity +- **EntityImportedEvent**: Published when an entity is imported from an external system +- **VersionCreatedEvent**: Published when a new version of an entity is created + +### 2. Event Publisher + +The `EventPublisherService` provides a centralized way to publish domain events. It wraps Spring's +`ApplicationEventPublisher` and provides a more domain-specific API. + +### 3. Event Listeners + +Event listeners subscribe to domain events and perform actions in response. The following listeners have been +implemented: + +- **PIDGenerationEventListener**: Listens for entity creation events and generates PIDs +- **EntityChangeNotifier**: Listens for entity lifecycle events and notifies subscribers + +### 4. Service Layer + +The service layer encapsulates business logic and publishes domain events when entities are created, updated, or +deleted. The following services have been implemented: + +- **TypeProfileService**: Manages TypeProfile entities +- **AtomicDataTypeService**: Manages AtomicDataType entities +- **OperationService**: Manages Operation entities + +### 5. Notification System + +The notification system allows external systems to subscribe to entity changes. It includes: + +- **EntityChangeSubscriber**: Interface for subscribers that want to be notified of entity changes +- **EntityChangeNotifier**: Component that listens for entity change events and notifies subscribers +- **LoggingEntityChangeSubscriber**: Sample implementation that logs entity changes + +## Module Structure + +The application is organized into the following modules: + +1. **Core Module**: Base abstractions, common interfaces, and event infrastructure +2. **Domain Module**: Entity definitions, domain services, and business logic +3. **Repository Module**: Data access objects and persistence +4. **Notification Module**: Callback mechanism for entity changes +5. **Web Module**: REST controllers and API + +## Event Flow Examples + +### PID Record Creation + +1. An entity is created via a service method +2. The service publishes an `EntityCreatedEvent` +3. The `PIDGenerationEventListener` listens for this event +4. The listener generates a PID using the TypedPID-Maker +5. The listener publishes a `PIDGeneratedEvent` + +### Entity Change Notification + +1. An entity is updated via a service method +2. The service publishes an `EntityUpdatedEvent` +3. The `EntityChangeNotifier` listens for this event +4. The notifier calls all subscribers registered for that entity type or specific entity + +## Implementation Details + +### Publishing Events + +```java +// In a service method +public TypeProfile createTypeProfile(TypeProfile typeProfile) { + TypeProfile saved = typeProfileDao.save(typeProfile); + eventPublisher.publishEntityCreated(saved); + return saved; +} +``` + +### Listening for Events + +```java +@Component +public class PIDGenerationEventListener { + @EventListener + @Transactional + public void handleEntityCreatedEvent(EntityCreatedEvent event) { + GenericIDORISEntity entity = event.getEntity(); + // Generate PID and update entity + eventPublisher.publishPIDGenerated(entity, pid); + } +} +``` + +### Subscribing to Entity Changes + +```java +// Register a subscriber +entityChangeNotifier.subscribeToType("TypeProfile", mySubscriber); + +// Implement the subscriber interface +public class MySubscriber implements EntityChangeSubscriber { + @Override + public void onEntityCreated(GenericIDORISEntity entity) { + // Handle entity creation + } + + @Override + public void onEntityUpdated(GenericIDORISEntity entity, Long previousVersion) { + // Handle entity update + } + + @Override + public void onEntityDeleted(GenericIDORISEntity entity) { + // Handle entity deletion + } +} +``` + +## Benefits + +1. **Loose Coupling**: Modules communicate through events, reducing direct dependencies +2. **Extensibility**: New functionality can be added by implementing new event listeners +3. **Testability**: Components can be tested in isolation +4. **Scalability**: Event-based architecture can be scaled horizontally +5. **Maintainability**: Clear separation of concerns makes the codebase easier to understand and maintain + +## Next Steps + +1. **Implement Additional Event Listeners**: For schema generation, versioning, etc. +2. **Add Event Persistence**: Store events for audit and replay purposes +3. **Implement Event Sourcing**: Rebuild entity state from events +4. **Add Event Monitoring**: Monitor event flow and performance +5. **Implement Event Versioning**: Handle changes to event structure over time \ No newline at end of file diff --git a/modulith-migration-plan.md b/modulith-migration-plan.md new file mode 100644 index 0000000..4237fdc --- /dev/null +++ b/modulith-migration-plan.md @@ -0,0 +1,202 @@ +# IDORIS Spring Modulith Migration Plan + +## Introduction + +This document outlines the plan to transform IDORIS into a Spring Modulith application, replacing Spring Data REST with +Spring MVC and Spring HATEOAS for the API. The goal is to enhance maintainability, lower coupling, and increase +development freedom through a well-structured modular architecture. + +## Goals and Benefits + +- **Improved Maintainability**: Clear module boundaries make the codebase easier to understand and maintain +- **Reduced Coupling**: Explicit dependencies between modules prevent unwanted coupling +- **Enhanced Development Freedom**: Teams can work on separate modules with minimal interference +- **Better Testability**: Modules can be tested in isolation +- **Clearer Architecture**: Module boundaries reflect domain concepts + +## Current Architecture Analysis + +IDORIS currently uses: + +- Spring Boot as the application framework +- Spring Data Neo4j for database access +- Spring Data REST for exposing repositories as REST APIs +- Spring HATEOAS for hypermedia support + +The application is structured around these main components: + +- Domain entities (AtomicDataType, TypeProfile, Operation, etc.) +- Data access objects (DAOs) +- Rules system (validation, processing) +- REST controllers and configuration + +## Module Boundaries + +Based on domain-driven design principles and the current codebase, we propose the following modules: + +1. **Core Module** + - Base abstractions and shared utilities + - Common interfaces and base classes + - Cross-cutting concerns + +2. **Domain Module** + - Entity definitions (DataType, TypeProfile, Operation, etc.) + - Domain services and business logic + - Domain events + +3. **Rules Module** + - Rule definitions and processing + - Validation logic + - Rule execution engine + +4. **Repository Module** + - Data access objects + - Neo4j configuration + - Query definitions + +5. **Web Module** + - REST controllers + - Request/response DTOs + - API documentation + +## Package Structure + +The new package structure will follow Spring Modulith conventions: + +``` +edu.kit.datamanager.idoris +├── core +│ ├── config +│ ├── exception +│ └── util +├── domain +│ ├── entities +│ ├── enums +│ ├── events +│ └── services +├── rules +│ ├── api +│ ├── logic +│ ├── processor +│ └── validation +├── repository +│ ├── config +│ ├── dao +│ └── mapping +└── web + ├── api + ├── controller + ├── dto + └── hateoas +``` + +## Migration from Spring Data REST to Spring MVC with HATEOAS + +### Current Implementation + +Spring Data REST automatically exposes repositories as REST endpoints with hypermedia support. This approach has +limitations: + +- Limited control over API design +- Tight coupling between domain model and API representation +- Challenges with complex business logic + +### New Implementation + +1. **Define Controller Layer** + - Create dedicated controllers for each resource type + - Implement CRUD operations using Spring MVC + - Use Spring HATEOAS for hypermedia support + +2. **Create Resource Representations** + - Define DTOs for request/response + - Implement assemblers to convert between entities and DTOs + - Add hypermedia links using LinkBuilder + +3. **Implement Business Logic** + - Move business logic from repositories to service classes + - Ensure proper separation of concerns + - Implement validation in appropriate layers + +## Required Dependencies + +Add the following dependencies to the build.gradle file: + +```gradle +// Spring Modulith +implementation 'org.springframework.experimental:spring-modulith-starter:1.1.0' +testImplementation 'org.springframework.experimental:spring-modulith-test:1.1.0' + +// Already present, keep these +implementation 'org.springframework.boot:spring-boot-starter-web' +implementation 'org.springframework.boot:spring-boot-starter-hateoas' +implementation 'org.springframework.boot:spring-boot-starter-validation' + +// Remove this dependency +// implementation 'org.springframework.boot:spring-boot-starter-data-rest' +``` + +## Implementation Approach + +### Phase 1: Setup Spring Modulith + +1. Add Spring Modulith dependencies +2. Create the new package structure +3. Configure module boundaries +4. Write module documentation + +### Phase 2: Migrate Domain Model + +1. Reorganize domain entities into the new structure +2. Refactor domain services +3. Implement domain events for cross-module communication + +### Phase 3: Implement Repository Layer + +1. Migrate DAOs to the repository module +2. Refactor Neo4j configuration +3. Implement repository services + +### Phase 4: Develop Web API + +1. Create controllers for each resource type +2. Implement DTOs and assemblers +3. Add hypermedia support using Spring HATEOAS +4. Migrate from Spring Data REST endpoints + +### Phase 5: Rules System Migration + +1. Reorganize rules components +2. Implement clean interfaces between modules +3. Ensure rule execution works across module boundaries + +### Phase 6: Testing and Validation + +1. Write module tests using Spring Modulith test support +2. Verify module boundaries and dependencies +3. Test API endpoints +4. Validate hypermedia functionality + +## Testing Strategy + +1. **Unit Tests**: Test individual components within modules +2. **Module Tests**: Test modules in isolation using Spring Modulith test support +3. **Integration Tests**: Test interactions between modules +4. **API Tests**: Verify REST endpoints and hypermedia functionality + +## Timeline + +- **Week 1-2**: Setup Spring Modulith and restructure packages +- **Week 3-4**: Migrate domain model and repository layer +- **Week 5-6**: Implement web API with Spring MVC and HATEOAS +- **Week 7-8**: Migrate rules system and testing + +## Conclusion + +Transforming IDORIS into a Spring Modulith application with Spring MVC and HATEOAS will provide significant benefits in +terms of maintainability, coupling, and development freedom. The migration can be done incrementally, ensuring that the +application remains functional throughout the process. + +The modular architecture will make it easier to understand the system, develop new features, and maintain the codebase +over time. The explicit module boundaries will prevent unwanted dependencies and ensure that the architecture remains +clean as the application evolves. \ No newline at end of file diff --git a/response.md b/response.md new file mode 100644 index 0000000..60977fb --- /dev/null +++ b/response.md @@ -0,0 +1,98 @@ +# Response to Fine-Grained Modularity Approach + +## Understanding the Approach + +The approach you've seen in other projects involves creating separate modules for each domain class, where: + +- Each domain class gets its own module +- Implementation details are hidden within the module +- External and internal interfaces are provided via Spring @Services +- Some projects even include API endpoints within these modules + +## Comparison with Our Proposed Structure + +Our migration plan proposes a more coarse-grained modular structure with 5 main modules: + +1. **Core Module**: Base abstractions and utilities +2. **Domain Module**: All entity definitions and business logic +3. **Rules Module**: Rule definitions and processing +4. **Repository Module**: Data access objects and persistence +5. **Web Module**: REST controllers and API concerns + +## Analysis of Fine-Grained Approach + +### Potential Benefits + +- **Maximum Encapsulation**: Each domain concept is fully isolated +- **Clear Ownership**: Teams can own specific domain modules +- **Focused Development**: Changes to one domain concept don't affect others +- **Independent Deployment**: Theoretically, modules could be deployed separately + +### Significant Drawbacks + +- **Excessive Fragmentation**: For IDORIS with entities like TypeProfile, AtomicDataType, Operation, etc., this would + create many small modules +- **Increased Complexity**: Managing dependencies between numerous small modules becomes challenging +- **Overhead**: Each module requires its own configuration, build setup, etc. +- **Cross-Cutting Concerns**: Difficult to handle concerns that span multiple domain classes +- **Tight Coupling**: Despite the separation, domain classes often have inherent relationships (e.g., TypeProfile + inherits from DataType) +- **API Endpoint Location**: As you noted, placing API endpoints in domain modules violates separation of concerns + +## Recommendation for IDORIS + +I recommend staying with the more balanced approach outlined in our migration plan for several reasons: + +1. **Domain Cohesion**: The entities in IDORIS are closely related (inheritance relationships, references between + entities). Keeping them in a single domain module maintains this cohesion while still providing clear boundaries. + +2. **Appropriate Separation**: The 5-module structure already provides good separation of concerns without excessive + fragmentation: + - Domain logic is separated from persistence + - Web concerns are isolated from business logic + - Rules system has its own boundary + +3. **Practical Maintainability**: A moderate number of well-defined modules is easier to maintain than dozens of tiny + modules. + +4. **Alignment with Spring Modulith**: The Spring Modulith approach generally favors "right-sized" modules that + represent meaningful business capabilities, not individual entities. + +## Alternative Approach + +If you want more fine-grained structure without the drawbacks of separate modules for each entity, consider: + +1. **Sub-packages within modules**: Within the domain module, create clear sub-packages for related entities: + ``` + domain + ├── datatype + │ ├── AtomicDataType.java + │ ├── DataType.java + │ ├── DataTypeService.java + │ └── internal/ + ├── typeprofile + │ ├── TypeProfile.java + │ ├── TypeProfileService.java + │ └── internal/ + └── operation + ├── Operation.java + ├── OperationService.java + └── internal/ + ``` + +2. **Package-private visibility**: Use package-private methods and classes to hide implementation details while keeping + related code in the same module. + +3. **Clear interfaces**: Define public interfaces for each domain concept that other packages can depend on. + +This approach gives you many of the benefits of fine-grained modularity without the overhead of separate build modules. + +## Conclusion + +While the approach of separate modules for each domain class offers maximum isolation, it introduces complexity that +likely outweighs its benefits for IDORIS. The proposed 5-module structure in our migration plan provides a good balance +between separation of concerns and practical maintainability. + +I agree with your assessment that placing API endpoints in domain modules is not ideal, as it violates the separation +between domain logic and web concerns. Keeping controllers in a dedicated web module, as outlined in our plan, is a +better approach. \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/configuration/HateoasConfig.java b/src/main/java/edu/kit/datamanager/idoris/configuration/HateoasConfig.java new file mode 100644 index 0000000..3b71795 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/configuration/HateoasConfig.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.configuration; + +import edu.kit.datamanager.idoris.domain.entities.AtomicDataType; +import edu.kit.datamanager.idoris.domain.entities.DataType; +import edu.kit.datamanager.idoris.domain.entities.TypeProfile; +import edu.kit.datamanager.idoris.web.v1.AtomicDataTypeController; +import edu.kit.datamanager.idoris.web.v1.TypeProfileController; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.server.RepresentationModelProcessor; + +import java.util.Objects; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +/** + * Configuration class for HATEOAS-related settings. + * This class configures representation model processors and other HATEOAS-related beans. + */ +@Configuration +public class HateoasConfig { + + /** + * Creates a representation model processor for TypeProfile entities. + * This processor adds links to validate, get inherited attributes, and get the inheritance tree. + * + * @return a representation model processor for TypeProfile entities + */ + @Bean + public RepresentationModelProcessor> typeProfileProcessor() { + return new RepresentationModelProcessor>() { + @Override + public EntityModel process(EntityModel model) { + TypeProfile typeProfile = Objects.requireNonNull(model.getContent()); + String pid = typeProfile.getPid(); + + // Add links to related resources + model.add(linkTo(methodOn(TypeProfileController.class).validate(pid)).withRel("validate")); + model.add(linkTo(methodOn(TypeProfileController.class).getInheritedAttributes(pid)).withRel("inheritedAttributes")); + model.add(linkTo(methodOn(TypeProfileController.class).getInheritanceTree(pid)).withRel("inheritanceTree")); + + return model; + } + }; + } + + /** + * Creates a representation model processor for DataType entities. + * This processor adds links to operations for the data type. + * + * @return a representation model processor for DataType entities + */ + @Bean + public RepresentationModelProcessor> dataTypeProcessor() { + return new RepresentationModelProcessor>() { + @Override + public EntityModel process(EntityModel model) { + DataType dataType = Objects.requireNonNull(model.getContent()); + String pid = dataType.getPid(); + + // Add link to operations for this data type + // The link depends on the type of DataType + if (dataType instanceof AtomicDataType) { + model.add(linkTo(methodOn(AtomicDataTypeController.class).getOperationsForAtomicDataType(pid)).withRel("operations")); + } else if (dataType instanceof TypeProfile) { + model.add(linkTo(methodOn(TypeProfileController.class).getOperationsForTypeProfile(pid)).withRel("operations")); + } + + return model; + } + }; + } + +// /** +// * Creates a representation model processor that adds validation results to entity models. +// * This processor executes validation rules and adds the results to the model. +// * +// * @param ruleService the rule service +// * @param applicationProperties the application properties +// * @return a representation model processor that adds validation results +// */ +// @Bean +// public RepresentationModelProcessor> validatorProcessor( +// RuleService ruleService, +// ApplicationProperties applicationProperties) { +// +// return model -> { +// if (model instanceof EntityModel && ((EntityModel) model).getContent() instanceof VisitableElement element) { +// // Use RuleService to process validation with the VALIDATE task +// ValidationResult validationResult = ruleService.executeRules( +// RuleTask.VALIDATE, +// element, +// ValidationResult::new +// ); +// +// // Convert ValidationResult to the expected format for the response +// if (!validationResult.isEmpty()) { +// Map> filteredMessages = +// validationResult.getOutputMessages() +// .entrySet() +// .stream() +// .filter(entry -> entry.getKey().isHigherOrEqualTo(applicationProperties.getValidationLevel())) +// .filter(entry -> !entry.getValue().isEmpty()) +// .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); +// +// if (!filteredMessages.isEmpty()) { +// Map results = Map.of( +// "validationResult", filteredMessages, +// "originalModel", model +// ); +// return CollectionModel.of(Set.of(results)); +// } +// } +// } +// return model; +// }; +// } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/configuration/OpenAPIConfig.java b/src/main/java/edu/kit/datamanager/idoris/configuration/OpenAPIConfig.java new file mode 100644 index 0000000..a746705 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/configuration/OpenAPIConfig.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.configuration; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.models.ExternalDocumentation; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@OpenAPIDefinition( + info = @Info( + title = "IDORIS API", + version = "1.0", + description = "API for IDORIS system" + ) +) +public class OpenAPIConfig { + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(new io.swagger.v3.oas.models.info.Info() + .title("IDORIS API") + .version("1.0") + .description("API documentation for IDORIS system") + .contact(new Contact() + .name("KIT Data Manager Team") + .email("webmaster@datamanager.kit.edu"))) + .externalDocs(new ExternalDocumentation() + .description("IDORIS Documentation") + .url("https://github.com/kit-data-manager/idoris")); + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/configuration/RepositoryRestConfig.java b/src/main/java/edu/kit/datamanager/idoris/configuration/RepositoryRestConfig.java deleted file mode 100644 index 118ac84..0000000 --- a/src/main/java/edu/kit/datamanager/idoris/configuration/RepositoryRestConfig.java +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright (c) 2024-2025 Karlsruhe Institute of Technology - * - * 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 edu.kit.datamanager.idoris.configuration; - -import edu.kit.datamanager.idoris.dao.IOperationDao; -import edu.kit.datamanager.idoris.dao.ITypeProfileDao; -import edu.kit.datamanager.idoris.domain.VisitableElement; -import edu.kit.datamanager.idoris.domain.entities.*; -import edu.kit.datamanager.idoris.rules.logic.OutputMessage; -import edu.kit.datamanager.idoris.rules.logic.RuleService; -import edu.kit.datamanager.idoris.rules.logic.RuleTask; -import edu.kit.datamanager.idoris.rules.validation.ValidationResult; -import io.netty.util.Attribute; -import lombok.extern.java.Log; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.data.rest.core.config.RepositoryRestConfiguration; -import org.springframework.data.rest.core.event.ValidatingRepositoryEventListener; -import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer; -import org.springframework.hateoas.CollectionModel; -import org.springframework.hateoas.EntityModel; -import org.springframework.hateoas.Link; -import org.springframework.hateoas.RepresentationModel; -import org.springframework.hateoas.server.LinkBuilder; -import org.springframework.hateoas.server.RepresentationModelProcessor; -import org.springframework.stereotype.Component; -import org.springframework.validation.Errors; -import org.springframework.web.servlet.config.annotation.CorsRegistry; - -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; - -import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; - -@Component -@Log -public class RepositoryRestConfig implements RepositoryRestConfigurer { - private final ApplicationProperties applicationProperties; - private final RuleService ruleService; - - @Autowired - public RepositoryRestConfig(ApplicationProperties applicationProperties, RuleService ruleService) { - this.applicationProperties = applicationProperties; - this.ruleService = ruleService; - } - - - @Override - public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) { - config.exposeIdsFor( - Attribute.class, - AtomicDataType.class, - Operation.class, - TechnologyInterface.class, - TypeProfile.class - ); - - cors.addMapping("/**") - .allowedOrigins("*") - .allowedMethods("GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS"); - } - - @Override - public void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener v) { - RuleBasedVisitableElementValidator validator = new RuleBasedVisitableElementValidator(ruleService, applicationProperties); - v.addValidator("beforeSave", validator); - v.addValidator("beforeCreate", validator); - v.addValidator("afterLinkSave", validator); - } - - @Bean - public RepresentationModelProcessor> typeProfileProcessor() { - LinkBuilder baseLink = linkTo(ITypeProfileDao.class).slash("api").slash("typeProfiles"); - return new RepresentationModelProcessor>() { - @Override - public EntityModel process(EntityModel model) { - String pid = Objects.requireNonNull(model.getContent()).getPid(); - model.add(baseLink.slash(pid).slash("validate").withRel("validate")); - model.add(baseLink.slash(pid).slash("inheritedAttributes").withRel("inheritedAttributes")); - model.add(baseLink.slash(pid).slash("inheritanceTree").withRel("inheritanceTree")); - return model; - } - }; - } - - @Bean - public RepresentationModelProcessor> dataTypeProcessor() { - return new RepresentationModelProcessor>() { - @Override - public EntityModel process(EntityModel model) { - String pid = Objects.requireNonNull(model.getContent()).getPid(); - model.add(Link.of(linkTo(IOperationDao.class) - .slash("api") - .slash("operations") - .slash("search") - .slash("getOperationsForDataType") - .toUri() + "?pid=" + pid, "operations")); - return model; - } - }; - } - - @Bean - public RepresentationModelProcessor> validatorProcessor(RuleService ruleService) { - return model -> { - if (model instanceof EntityModel && ((EntityModel) model).getContent() instanceof VisitableElement element) { - // Use RuleService to process validation with the VALIDATE task - ValidationResult validationResult = ruleService.executeRules( - RuleTask.VALIDATE, - element, - ValidationResult::new - ); - - // Convert ValidationResult to the expected format for the response - if (!validationResult.isEmpty()) { - Map> filteredMessages = - validationResult.getOutputMessages() - .entrySet() - .stream() - .filter(entry -> entry.getKey().isHigherOrEqualTo(applicationProperties.getValidationLevel())) - .filter(entry -> !entry.getValue().isEmpty()) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - - if (!filteredMessages.isEmpty()) { - Map results = Map.of( - "validationResult", filteredMessages, - "originalModel", model - ); - return CollectionModel.of(Set.of(results)); - } - } - } - return model; - }; - } - - /** - * Spring Data REST validator that uses the new rule-based validation system. - * This validator integrates with Spring's validation framework and executes - * all applicable validation rules through the RuleService. - */ - private static class RuleBasedVisitableElementValidator implements org.springframework.validation.Validator { - private final RuleService ruleService; - private final ApplicationProperties applicationProperties; - - public RuleBasedVisitableElementValidator(RuleService ruleService, ApplicationProperties applicationProperties) { - this.ruleService = ruleService; - this.applicationProperties = applicationProperties; - } - - @Override - public boolean supports(Class clazz) { - return VisitableElement.class.isAssignableFrom(clazz); - } - - @Override - public void validate(Object target, Errors errors) { - if (!(target instanceof VisitableElement element)) { - return; - } - - try { - // Execute validation rules using RuleService - ValidationResult result = ruleService.executeRules( - RuleTask.VALIDATE, - element, - ValidationResult::new - ); - - // Convert ValidationResult to Spring validation errors - convertToSpringErrors(result, errors); - - } catch (Exception e) { - log.severe("Error during rule-based validation: " + e.getMessage()); - errors.reject("validation.error", "Validation failed due to internal error"); - } - } - - /** - * Converts ValidationResult messages to Spring validation errors. - * Only includes messages that meet the configured validation level threshold. - */ - private void convertToSpringErrors(ValidationResult result, org.springframework.validation.Errors errors) { - if (result == null || result.isEmpty()) { - return; - } - - result.getOutputMessages().entrySet().stream() - .filter(entry -> entry.getKey().isHigherOrEqualTo(applicationProperties.getValidationLevel())) - .forEach(entry -> { - OutputMessage.MessageSeverity severity = entry.getKey(); - List messages = entry.getValue(); - - for (OutputMessage message : messages) { - String errorCode = "validation." + severity.name().toLowerCase(); - String defaultMessage = message.message(); - - if (severity == OutputMessage.MessageSeverity.ERROR) { - errors.reject(errorCode, defaultMessage); - } else { - // For warnings and info, we can still add them but they won't fail validation - errors.reject("validation.warning", defaultMessage); - } - } - }); - } - } -} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/configuration/RestResponseEntityExceptionHandler.java b/src/main/java/edu/kit/datamanager/idoris/configuration/RestResponseEntityExceptionHandler.java deleted file mode 100644 index 52428c3..0000000 --- a/src/main/java/edu/kit/datamanager/idoris/configuration/RestResponseEntityExceptionHandler.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (c) 2024 Karlsruhe Institute of Technology - * - * 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 edu.kit.datamanager.idoris.configuration; - -import lombok.extern.java.Log; -import org.springframework.data.rest.core.RepositoryConstraintViolationException; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.HttpStatusCode; -import org.springframework.http.ResponseEntity; -import org.springframework.validation.method.MethodValidationException; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.context.request.WebRequest; -import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; - -@ControllerAdvice -@Log -public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { - @Override - protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { -// String errors = ex.getAllErrors() -// .stream() -// .map(ObjectError::toString) -// .collect(Collectors.joining("\n")); - -// return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST); - return new ResponseEntity<>(ex.getAllErrors(), HttpStatus.BAD_REQUEST); - } - - @Override - protected ResponseEntity handleMethodValidationException(MethodValidationException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { -// String errors = ex.getAllValidationResults() -// .stream() -// .map(ParameterValidationResult::toString) -// .collect(Collectors.joining("\n")); -// -// return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST); - - return new ResponseEntity<>(ex.getAllErrors(), HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler({RepositoryConstraintViolationException.class}) - public ResponseEntity handleAccessDeniedException(Exception ex) { - RepositoryConstraintViolationException nevEx = (RepositoryConstraintViolationException) ex; - return new ResponseEntity<>(nevEx.getErrors().getAllErrors(), new HttpHeaders(), HttpStatus.NOT_ACCEPTABLE); - } -} diff --git a/src/main/java/edu/kit/datamanager/idoris/configuration/ValidationConfig.java b/src/main/java/edu/kit/datamanager/idoris/configuration/ValidationConfig.java new file mode 100644 index 0000000..e3e91c7 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/configuration/ValidationConfig.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.configuration; + +import edu.kit.datamanager.idoris.domain.VisitableElement; +import edu.kit.datamanager.idoris.rules.logic.OutputMessage; +import edu.kit.datamanager.idoris.rules.logic.RuleService; +import edu.kit.datamanager.idoris.rules.logic.RuleTask; +import edu.kit.datamanager.idoris.rules.validation.ValidationResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.Validator; + +import java.util.List; + +/** + * Configuration class for validation-related settings. + * This class configures validators and validation-related beans. + */ +@Configuration +@Slf4j +public class ValidationConfig { + + /** + * Creates a validator that uses the rule-based validation system. + * + * @param ruleService the rule service + * @param applicationProperties the application properties + * @return a validator that uses the rule-based validation system + */ + @Bean + public Validator ruleBasedValidator(RuleService ruleService, ApplicationProperties applicationProperties) { + return new RuleBasedVisitableElementValidator(ruleService, applicationProperties); + } + + /** + * Spring validator that uses the rule-based validation system. + * This validator integrates with Spring's validation framework and executes + * all applicable validation rules through the RuleService. + */ + private static class RuleBasedVisitableElementValidator implements org.springframework.validation.Validator { + private final RuleService ruleService; + private final ApplicationProperties applicationProperties; + + public RuleBasedVisitableElementValidator(RuleService ruleService, ApplicationProperties applicationProperties) { + this.ruleService = ruleService; + this.applicationProperties = applicationProperties; + } + + @Override + public boolean supports(Class clazz) { + return VisitableElement.class.isAssignableFrom(clazz); + } + + @Override + public void validate(Object target, org.springframework.validation.Errors errors) { + if (!(target instanceof VisitableElement element)) { + return; + } + + try { + // Execute validation rules using RuleService + ValidationResult result = ruleService.executeRules( + RuleTask.VALIDATE, + element, + ValidationResult::new + ); + + // Convert ValidationResult to Spring validation errors + convertToSpringErrors(result, errors); + + } catch (Exception e) { + log.error("Error during rule-based validation: " + e.getMessage()); + errors.reject("validation.error", "Validation failed due to internal error"); + } + } + + /** + * Converts ValidationResult messages to Spring validation errors. + * Only includes messages that meet the configured validation level threshold. + */ + private void convertToSpringErrors(ValidationResult result, org.springframework.validation.Errors errors) { + if (result == null || result.isEmpty()) { + return; + } + + result.getOutputMessages().entrySet().stream() + .filter(entry -> entry.getKey().isHigherOrEqualTo(applicationProperties.getValidationLevel())) + .forEach(entry -> { + OutputMessage.MessageSeverity severity = entry.getKey(); + List messages = entry.getValue(); + + for (OutputMessage message : messages) { + String errorCode = "validation." + severity.name().toLowerCase(); + String defaultMessage = message.message(); + + if (severity == OutputMessage.MessageSeverity.ERROR) { + errors.reject(errorCode, defaultMessage); + } else { + // For warnings and info, we can still add them but they won't fail validation + errors.reject("validation.warning", defaultMessage); + } + } + }); + } + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/configuration/WebConfig.java b/src/main/java/edu/kit/datamanager/idoris/configuration/WebConfig.java new file mode 100644 index 0000000..5a63713 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/configuration/WebConfig.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.hateoas.config.EnableHypermediaSupport; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * Configuration class for web-related settings. + * This class configures CORS, HATEOAS, and other web-related settings. + */ +@Configuration +@EnableHypermediaSupport(type = {EnableHypermediaSupport.HypermediaType.HAL, EnableHypermediaSupport.HypermediaType.HAL_FORMS, EnableHypermediaSupport.HypermediaType.COLLECTION_JSON}) +public class WebConfig { + + /** + * Configures CORS settings. + * + * @return the WebMvcConfigurer with CORS configuration + */ + @Bean + public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurer() { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("*") + .allowedMethods("GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS") + .allowedHeaders("*"); + } + }; + } + + public void addViewControllers(ViewControllerRegistry registry) { + registry.addViewController("/").setViewName("forward:/swagger-ui.html"); + registry.addViewController("/explorer").setViewName("forward:/explorer/index.html"); + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/AbstractDomainEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/AbstractDomainEvent.java new file mode 100644 index 0000000..5edc964 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/AbstractDomainEvent.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.core.events; + +import lombok.Getter; +import lombok.ToString; + +import java.time.Instant; +import java.util.UUID; + +/** + * Abstract base class for all domain events. + * Provides common functionality and properties for all events. + */ +@Getter +@ToString +public abstract class AbstractDomainEvent implements DomainEvent { + private final Instant timestamp = Instant.now(); + private final String eventId = UUID.randomUUID().toString(); + + /** + * Gets the timestamp when this event occurred. + * + * @return the instant when the event was created + */ + @Override + public Instant getTimestamp() { + return timestamp; + } + + /** + * Gets the unique identifier for this event. + * + * @return the event's unique identifier + */ + @Override + public String getEventId() { + return eventId; + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/DomainEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/DomainEvent.java new file mode 100644 index 0000000..e8dc25c --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/DomainEvent.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.core.events; + +import java.time.Instant; + +/** + * Base interface for all domain events in the system. + * Domain events represent significant occurrences within the domain that + * other parts of the application might be interested in. + */ +public interface DomainEvent { + /** + * Gets the timestamp when this event occurred. + * + * @return the instant when the event was created + */ + Instant getTimestamp(); + + /** + * Gets the unique identifier for this event. + * + * @return the event's unique identifier + */ + String getEventId(); +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/EntityCreatedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityCreatedEvent.java new file mode 100644 index 0000000..04587e6 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityCreatedEvent.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.core.events; + +import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import lombok.Getter; +import lombok.ToString; + +/** + * Event that is published when a new entity is created in the system. + * This event carries the newly created entity and can be used by listeners + * to perform additional operations like PID generation, validation, etc. + * + * @param the type of entity that was created, must extend GenericIDORISEntity + */ +@Getter +@ToString(callSuper = true) +public class EntityCreatedEvent extends AbstractDomainEvent { + private final T entity; + + /** + * Creates a new EntityCreatedEvent for the given entity. + * + * @param entity the newly created entity + */ + public EntityCreatedEvent(T entity) { + this.entity = entity; + } + + /** + * Gets the entity that was created. + * + * @return the newly created entity + */ + public T getEntity() { + return entity; + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/EntityDeletedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityDeletedEvent.java new file mode 100644 index 0000000..4d541a2 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityDeletedEvent.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.core.events; + +import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import lombok.Getter; +import lombok.ToString; + +/** + * Event that is published when an entity is deleted from the system. + * This event carries the deleted entity and can be used by listeners + * to perform additional operations like cleanup, notification, etc. + * + * @param the type of entity that was deleted, must extend GenericIDORISEntity + */ +@Getter +@ToString(callSuper = true) +public class EntityDeletedEvent extends AbstractDomainEvent { + private final T entity; + private final String entityType; + private final String entityPid; + + /** + * Creates a new EntityDeletedEvent for the given entity. + * + * @param entity the deleted entity + */ + public EntityDeletedEvent(T entity) { + this.entity = entity; + this.entityType = entity.getClass().getSimpleName(); + this.entityPid = entity.getPid(); + } + + /** + * Gets the entity that was deleted. + * + * @return the deleted entity + */ + public T getEntity() { + return entity; + } + + /** + * Gets the type of the entity that was deleted. + * + * @return the entity type + */ + public String getEntityType() { + return entityType; + } + + /** + * Gets the PID of the entity that was deleted. + * + * @return the entity PID + */ + public String getEntityPid() { + return entityPid; + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/EntityImportedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityImportedEvent.java new file mode 100644 index 0000000..cedb106 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityImportedEvent.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.core.events; + +import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import lombok.Getter; +import lombok.ToString; + +/** + * Event that is published when an entity is imported from an external system. + * This event carries the imported entity, the source system, and import metadata. + * It can be used by listeners to perform additional operations like validation, enrichment, or notification. + * + * @param the type of entity that was imported, must extend GenericIDORISEntity + */ +@Getter +@ToString(callSuper = true) +public class EntityImportedEvent extends AbstractDomainEvent { + private final T entity; + private final String sourceSystem; + private final String sourceIdentifier; + private final ImportResult importResult; + + /** + * Creates a new EntityImportedEvent for the given entity and source information. + * + * @param entity the imported entity + * @param sourceSystem the system from which the entity was imported + * @param sourceIdentifier the identifier of the entity in the source system + * @param importResult the result of the import operation + */ + public EntityImportedEvent(T entity, String sourceSystem, String sourceIdentifier, ImportResult importResult) { + this.entity = entity; + this.sourceSystem = sourceSystem; + this.sourceIdentifier = sourceIdentifier; + this.importResult = importResult; + } + + /** + * Gets the entity that was imported. + * + * @return the imported entity + */ + public T getEntity() { + return entity; + } + + /** + * Gets the system from which the entity was imported. + * + * @return the source system + */ + public String getSourceSystem() { + return sourceSystem; + } + + /** + * Gets the identifier of the entity in the source system. + * + * @return the source identifier + */ + public String getSourceIdentifier() { + return sourceIdentifier; + } + + /** + * Gets the result of the import operation. + * + * @return the import result + */ + public ImportResult getImportResult() { + return importResult; + } + + /** + * Enum representing the result of an import operation. + */ + public enum ImportResult { + /** + * The entity was successfully imported. + */ + SUCCESS, + + /** + * The entity was partially imported with some data loss or modifications. + */ + PARTIAL, + + /** + * The entity was imported but requires manual review. + */ + NEEDS_REVIEW, + + /** + * The entity was not imported due to validation errors. + */ + VALIDATION_ERROR, + + /** + * The entity was not imported due to a system error. + */ + SYSTEM_ERROR + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/EntityUpdatedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityUpdatedEvent.java new file mode 100644 index 0000000..50c4c8d --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityUpdatedEvent.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.core.events; + +import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import lombok.Getter; +import lombok.ToString; + +/** + * Event that is published when an entity is updated in the system. + * This event carries the updated entity and can be used by listeners + * to perform additional operations like versioning, validation, etc. + * + * @param the type of entity that was updated, must extend GenericIDORISEntity + */ +@Getter +@ToString(callSuper = true) +public class EntityUpdatedEvent extends AbstractDomainEvent { + private final T entity; + private final Long previousVersion; + + /** + * Creates a new EntityUpdatedEvent for the given entity. + * + * @param entity the updated entity + * @param previousVersion the version of the entity before the update + */ + public EntityUpdatedEvent(T entity, Long previousVersion) { + this.entity = entity; + this.previousVersion = previousVersion; + } + + /** + * Gets the entity that was updated. + * + * @return the updated entity + */ + public T getEntity() { + return entity; + } + + /** + * Gets the version of the entity before the update. + * + * @return the previous version number + */ + public Long getPreviousVersion() { + return previousVersion; + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/EventPublisherService.java b/src/main/java/edu/kit/datamanager/idoris/core/events/EventPublisherService.java new file mode 100644 index 0000000..7e9b8af --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/EventPublisherService.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.core.events; + +import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; + +/** + * Service for publishing domain events. + * This service wraps Spring's ApplicationEventPublisher to provide a more domain-specific API. + */ +@Service +@Slf4j +public class EventPublisherService { + private final ApplicationEventPublisher eventPublisher; + + /** + * Creates a new EventPublisherService with the given ApplicationEventPublisher. + * + * @param eventPublisher the Spring ApplicationEventPublisher + */ + public EventPublisherService(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + + /** + * Publishes an entity created event. + * + * @param entity the newly created entity + * @param the type of entity + */ + public void publishEntityCreated(T entity) { + log.debug("Publishing EntityCreatedEvent for entity: {}", entity); + eventPublisher.publishEvent(new EntityCreatedEvent<>(entity)); + } + + /** + * Publishes an entity created event for non-GenericIDORISEntity entities. + * + * @param entity the newly created entity + * @param entityType the type identifier for the entity + */ + public void publishEntityCreated(Object entity, String entityType) { + log.debug("Publishing EntityCreatedEvent for entity: {}, type: {}", entity, entityType); + eventPublisher.publishEvent(new GenericEntityCreatedEvent(entity, entityType)); + } + + /** + * Publishes an entity updated event. + * + * @param entity the updated entity + * @param previousVersion the version of the entity before the update + * @param the type of entity + */ + public void publishEntityUpdated(T entity, Long previousVersion) { + log.debug("Publishing EntityUpdatedEvent for entity: {}, previous version: {}", entity, previousVersion); + eventPublisher.publishEvent(new EntityUpdatedEvent<>(entity, previousVersion)); + } + + /** + * Publishes an entity updated event for non-GenericIDORISEntity entities. + * + * @param entity the updated entity + * @param entityType the type identifier for the entity + */ + public void publishEntityUpdated(Object entity, String entityType) { + log.debug("Publishing EntityUpdatedEvent for entity: {}, type: {}", entity, entityType); + eventPublisher.publishEvent(new GenericEntityUpdatedEvent(entity, entityType)); + } + + /** + * Publishes an entity deleted event. + * + * @param entity the deleted entity + * @param the type of entity + */ + public void publishEntityDeleted(T entity) { + log.debug("Publishing EntityDeletedEvent for entity: {}", entity); + eventPublisher.publishEvent(new EntityDeletedEvent<>(entity)); + } + + /** + * Publishes an entity deleted event for non-GenericIDORISEntity entities. + * + * @param entity the deleted entity + * @param entityType the type identifier for the entity + */ + public void publishEntityDeleted(Object entity, String entityType) { + log.debug("Publishing EntityDeletedEvent for entity: {}, type: {}", entity, entityType); + eventPublisher.publishEvent(new GenericEntityDeletedEvent(entity, entityType)); + } + + /** + * Publishes a PID generated event. + * + * @param entity the entity for which the PID was generated + * @param pid the generated PID + * @param isNewPID indicates whether this is a newly generated PID or an existing one + * @param the type of entity + */ + public void publishPIDGenerated(T entity, String pid, boolean isNewPID) { + log.debug("Publishing PIDGeneratedEvent for entity: {}, PID: {}, isNewPID: {}", entity, pid, isNewPID); + eventPublisher.publishEvent(new PIDGeneratedEvent<>(entity, pid, isNewPID)); + } + + /** + * Publishes a PID generated event. + * Assumes that the PID is newly generated. + * + * @param entity the entity for which the PID was generated + * @param pid the generated PID + * @param the type of entity + */ + public void publishPIDGenerated(T entity, String pid) { + publishPIDGenerated(entity, pid, true); + } + + /** + * Publishes a generic domain event. + * + * @param event the event to publish + */ + public void publishEvent(DomainEvent event) { + log.debug("Publishing event: {}", event); + eventPublisher.publishEvent(event); + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityCreatedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityCreatedEvent.java new file mode 100644 index 0000000..5c1d95a --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityCreatedEvent.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.core.events; + +import lombok.Getter; +import lombok.ToString; + +/** + * Event that is published when a new entity is created in the system. + * This event is for entities that don't extend GenericIDORISEntity. + */ +@Getter +@ToString(callSuper = true) +public class GenericEntityCreatedEvent extends AbstractDomainEvent { + private final Object entity; + private final String entityType; + + /** + * Creates a new GenericEntityCreatedEvent for the given entity. + * + * @param entity the newly created entity + * @param entityType the type identifier for the entity + */ + public GenericEntityCreatedEvent(Object entity, String entityType) { + this.entity = entity; + this.entityType = entityType; + } + + /** + * Gets the entity that was created. + * + * @return the newly created entity + */ + public Object getEntity() { + return entity; + } + + /** + * Gets the type identifier of the entity. + * + * @return the entity type + */ + public String getEntityType() { + return entityType; + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityDeletedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityDeletedEvent.java new file mode 100644 index 0000000..03d7a7b --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityDeletedEvent.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.core.events; + +import lombok.Getter; +import lombok.ToString; + +/** + * Event that is published when an entity is deleted from the system. + * This event is for entities that don't extend GenericIDORISEntity. + */ +@Getter +@ToString(callSuper = true) +public class GenericEntityDeletedEvent extends AbstractDomainEvent { + private final Object entity; + private final String entityType; + + /** + * Creates a new GenericEntityDeletedEvent for the given entity. + * + * @param entity the deleted entity + * @param entityType the type identifier for the entity + */ + public GenericEntityDeletedEvent(Object entity, String entityType) { + this.entity = entity; + this.entityType = entityType; + } + + /** + * Gets the entity that was deleted. + * + * @return the deleted entity + */ + public Object getEntity() { + return entity; + } + + /** + * Gets the type identifier of the entity. + * + * @return the entity type + */ + public String getEntityType() { + return entityType; + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityUpdatedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityUpdatedEvent.java new file mode 100644 index 0000000..f81ddb3 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityUpdatedEvent.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.core.events; + +import lombok.Getter; +import lombok.ToString; + +/** + * Event that is published when an entity is updated in the system. + * This event is for entities that don't extend GenericIDORISEntity. + */ +@Getter +@ToString(callSuper = true) +public class GenericEntityUpdatedEvent extends AbstractDomainEvent { + private final Object entity; + private final String entityType; + + /** + * Creates a new GenericEntityUpdatedEvent for the given entity. + * + * @param entity the updated entity + * @param entityType the type identifier for the entity + */ + public GenericEntityUpdatedEvent(Object entity, String entityType) { + this.entity = entity; + this.entityType = entityType; + } + + /** + * Gets the entity that was updated. + * + * @return the updated entity + */ + public Object getEntity() { + return entity; + } + + /** + * Gets the type identifier of the entity. + * + * @return the entity type + */ + public String getEntityType() { + return entityType; + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/PIDGeneratedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/PIDGeneratedEvent.java new file mode 100644 index 0000000..55a6a9c --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/PIDGeneratedEvent.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.core.events; + +import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import lombok.Getter; +import lombok.ToString; + +/** + * Event that is published when a PID is generated for an entity. + * This event carries the entity and the generated PID, and can be used by listeners + * to perform additional operations like PID record creation, indexing, etc. + * + * @param the type of entity for which the PID was generated, must extend GenericIDORISEntity + */ +@Getter +@ToString(callSuper = true) +public class PIDGeneratedEvent extends AbstractDomainEvent { + private final T entity; + private final String pid; + private final boolean isNewPID; + + /** + * Creates a new PIDGeneratedEvent for the given entity and PID. + * + * @param entity the entity for which the PID was generated + * @param pid the generated PID + * @param isNewPID indicates whether this is a newly generated PID or an existing one + */ + public PIDGeneratedEvent(T entity, String pid, boolean isNewPID) { + this.entity = entity; + this.pid = pid; + this.isNewPID = isNewPID; + } + + /** + * Creates a new PIDGeneratedEvent for the given entity and PID. + * Assumes that the PID is newly generated. + * + * @param entity the entity for which the PID was generated + * @param pid the generated PID + */ + public PIDGeneratedEvent(T entity, String pid) { + this(entity, pid, true); + } + + /** + * Gets the entity for which the PID was generated. + * + * @return the entity + */ + public T getEntity() { + return entity; + } + + /** + * Gets the generated PID. + * + * @return the PID + */ + public String getPid() { + return pid; + } + + /** + * Indicates whether this is a newly generated PID or an existing one. + * + * @return true if the PID was newly generated, false if it already existed + */ + public boolean isNewPID() { + return isNewPID; + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/SchemaGeneratedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/SchemaGeneratedEvent.java new file mode 100644 index 0000000..a9cb691 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/SchemaGeneratedEvent.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.core.events; + +import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import lombok.Getter; +import lombok.ToString; + +/** + * Event that is published when a schema is generated for an entity. + * This event carries the entity for which the schema was generated, the schema format, and the schema content. + * It can be used by listeners to perform additional operations like schema validation, storage, or publication. + */ +@Getter +@ToString(callSuper = true) +public class SchemaGeneratedEvent extends AbstractDomainEvent { + private final GenericIDORISEntity entity; + private final String schemaFormat; + private final String schemaContent; + private final boolean isValid; + + /** + * Creates a new SchemaGeneratedEvent for the given entity and schema. + * + * @param entity the entity for which the schema was generated + * @param schemaFormat the format of the schema (e.g., "json-schema", "xml-schema") + * @param schemaContent the content of the schema + * @param isValid indicates whether the schema is valid + */ + public SchemaGeneratedEvent(GenericIDORISEntity entity, String schemaFormat, String schemaContent, boolean isValid) { + this.entity = entity; + this.schemaFormat = schemaFormat; + this.schemaContent = schemaContent; + this.isValid = isValid; + } + + /** + * Gets the entity for which the schema was generated. + * + * @return the entity + */ + public GenericIDORISEntity getEntity() { + return entity; + } + + /** + * Gets the format of the schema. + * + * @return the schema format + */ + public String getSchemaFormat() { + return schemaFormat; + } + + /** + * Gets the content of the schema. + * + * @return the schema content + */ + public String getSchemaContent() { + return schemaContent; + } + + /** + * Indicates whether the schema is valid. + * + * @return true if the schema is valid, false otherwise + */ + public boolean isValid() { + return isValid; + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/VersionCreatedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/VersionCreatedEvent.java new file mode 100644 index 0000000..9d3144a --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/VersionCreatedEvent.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.core.events; + +import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import lombok.Getter; +import lombok.ToString; + +import java.util.Map; + +/** + * Event that is published when a new version of an entity is created. + * This event carries the current entity, the previous version, and change information. + * It can be used by listeners to perform additional operations like version tracking, notification, or audit logging. + * + * @param the type of entity that was versioned, must extend GenericIDORISEntity + */ +@Getter +@ToString(callSuper = true) +public class VersionCreatedEvent extends AbstractDomainEvent { + private final T currentEntity; + private final T previousEntity; + private final Long previousVersion; + private final Long currentVersion; + private final Map changes; + + /** + * Creates a new VersionCreatedEvent for the given entity versions and changes. + * + * @param currentEntity the current version of the entity + * @param previousEntity the previous version of the entity + * @param changes a map of field names to their changed values + */ + public VersionCreatedEvent(T currentEntity, T previousEntity, Map changes) { + this.currentEntity = currentEntity; + this.previousEntity = previousEntity; + this.previousVersion = previousEntity.getVersion(); + this.currentVersion = currentEntity.getVersion(); + this.changes = changes; + } + + /** + * Gets the current version of the entity. + * + * @return the current entity + */ + public T getCurrentEntity() { + return currentEntity; + } + + /** + * Gets the previous version of the entity. + * + * @return the previous entity + */ + public T getPreviousEntity() { + return previousEntity; + } + + /** + * Gets the version number of the previous entity. + * + * @return the previous version number + */ + public Long getPreviousVersion() { + return previousVersion; + } + + /** + * Gets the version number of the current entity. + * + * @return the current version number + */ + public Long getCurrentVersion() { + return currentVersion; + } + + /** + * Gets the changes between the previous and current versions. + * The map contains field names as keys and their changed values as values. + * + * @return the changes map + */ + public Map getChanges() { + return changes; + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/core/package-info.java b/src/main/java/edu/kit/datamanager/idoris/core/package-info.java new file mode 100644 index 0000000..e5cebc6 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/core/package-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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. + */ + +/** + * Core module for IDORIS. + * This module contains base abstractions, common interfaces, and cross-cutting concerns. + * It also includes the event infrastructure for the event-driven architecture. + * + *

The core module is a foundational module that other modules depend on. + * It should not depend on any other module to avoid circular dependencies.

+ */ +@org.springframework.modulith.ApplicationModule( + displayName = "IDORIS Core", + allowedDependencies = {} +) +package edu.kit.datamanager.idoris.core; \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/IAtomicDataTypeDao.java b/src/main/java/edu/kit/datamanager/idoris/dao/IAtomicDataTypeDao.java index eb73c84..3937ec7 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/IAtomicDataTypeDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/dao/IAtomicDataTypeDao.java @@ -18,12 +18,17 @@ import edu.kit.datamanager.idoris.domain.entities.AtomicDataType; import org.springframework.data.neo4j.repository.query.Query; -import org.springframework.data.rest.core.annotation.RepositoryRestResource; -import org.springframework.data.rest.core.annotation.RestResource; -@RepositoryRestResource(collectionResourceRel = "atomicDataTypes", path = "atomicDataTypes") +/** + * Repository interface for AtomicDataType entities. + */ public interface IAtomicDataTypeDao extends IGenericRepo { - @RestResource(exported = false) + /** + * Finds all AtomicDataType entities in the inheritance chain of the given AtomicDataType. + * + * @param pid the PID of the AtomicDataType + * @return an Iterable of AtomicDataType entities in the inheritance chain + */ @Query("MATCH (d:AtomicDataType {pid: $pid})-[:inheritsFrom*]->(d2:AtomicDataType) RETURN d2") Iterable findAllInInheritanceChain(String pid); } diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/IAttributeDao.java b/src/main/java/edu/kit/datamanager/idoris/dao/IAttributeDao.java index 70c530b..8e0627b 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/IAttributeDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/dao/IAttributeDao.java @@ -18,9 +18,7 @@ import edu.kit.datamanager.idoris.domain.entities.Attribute; import org.springframework.data.neo4j.repository.query.Query; -import org.springframework.data.rest.core.annotation.RepositoryRestResource; -@RepositoryRestResource(collectionResourceRel = "attributes", path = "attributes") public interface IAttributeDao extends IGenericRepo { @Query("MATCH (n:Attribute)" + " WHERE size([(n)-[:dataType]->() | 1]) = 1 AND NOT (n)<-[]-()" + diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/IAttributeMappingDao.java b/src/main/java/edu/kit/datamanager/idoris/dao/IAttributeMappingDao.java new file mode 100644 index 0000000..c7a5343 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/dao/IAttributeMappingDao.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.dao; + +import edu.kit.datamanager.idoris.domain.entities.AttributeMapping; +import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.data.neo4j.repository.query.Query; + +/** + * Repository interface for AttributeMapping entities. + */ +public interface IAttributeMappingDao extends Neo4jRepository { + + /** + * Finds AttributeMapping entities by input attribute PID. + * + * @param pid the PID of the input attribute + * @return an Iterable of AttributeMapping entities + */ + @Query("MATCH (a:Attribute {pid: $pid})<-[:input]-(m:AttributeMapping) RETURN m") + Iterable findByInputAttributePid(String pid); + + /** + * Finds AttributeMapping entities by output attribute PID. + * + * @param pid the PID of the output attribute + * @return an Iterable of AttributeMapping entities + */ + @Query("MATCH (a:Attribute {pid: $pid})<-[:output]-(m:AttributeMapping) RETURN m") + Iterable findByOutputAttributePid(String pid); +} diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/IDataTypeDao.java b/src/main/java/edu/kit/datamanager/idoris/dao/IDataTypeDao.java index e6704e5..7ad0f29 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/IDataTypeDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/dao/IDataTypeDao.java @@ -19,13 +19,28 @@ import edu.kit.datamanager.idoris.domain.entities.DataType; import edu.kit.datamanager.idoris.domain.entities.Operation; import org.springframework.data.neo4j.repository.query.Query; -import org.springframework.data.rest.core.annotation.RepositoryRestResource; -@RepositoryRestResource(collectionResourceRel = "dataTypes", path = "dataTypes") +/** + * Repository interface for DataType entities. + */ public interface IDataTypeDao extends IGenericRepo { + /** + * Finds all DataType entities in the inheritance chain of the given DataType. + * + * @param pid the PID of the DataType + * @return an Iterable of DataType entities in the inheritance chain + */ @Query("MATCH (d:DataType {pid: $pid})-[:inheritsFrom*]->(d2:DataType) RETURN d2") Iterable findAllInInheritanceChain(String pid); + /** + * Gets operations that can be executed on a data type. + * This method finds operations that are executable on the given data type, its attributes, + * or any data type in its inheritance chain. + * + * @param pid the PID of the data type + * @return an Iterable of Operation entities + */ @Query("Match (:DataType {pid: $pid})-[:attributes|inheritsFrom*]->(:DataType)<-[:dataType]-(:Attribute)<-[:executableOn]-(o:Operation) return o") Iterable getOperations(String pid); } diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/IGenericRepo.java b/src/main/java/edu/kit/datamanager/idoris/dao/IGenericRepo.java index 96d6707..91a1318 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/IGenericRepo.java +++ b/src/main/java/edu/kit/datamanager/idoris/dao/IGenericRepo.java @@ -19,10 +19,10 @@ import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.repository.ListCrudRepository; -import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; -@NoRepositoryBean +@RepositoryRestResource(exported = false) public interface IGenericRepo extends Neo4jRepository, ListCrudRepository, PagingAndSortingRepository { // This interface serves as a marker for generic repositories. // It can be extended by specific repositories to inherit common methods. diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/IOperationDao.java b/src/main/java/edu/kit/datamanager/idoris/dao/IOperationDao.java index b616514..ab6eb9c 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/IOperationDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/dao/IOperationDao.java @@ -18,10 +18,18 @@ import edu.kit.datamanager.idoris.domain.entities.Operation; import org.springframework.data.neo4j.repository.query.Query; -import org.springframework.data.rest.core.annotation.RepositoryRestResource; -@RepositoryRestResource(collectionResourceRel = "operations", path = "operations") +/** + * Repository interface for Operation entities. + */ public interface IOperationDao extends IGenericRepo { + /** + * Gets operations that can be executed on a data type. + * This method finds operations that are executable on the given data type or its attributes. + * + * @param pid the PID of the data type + * @return an Iterable of Operation entities + */ // @Query("optional MATCH (:DataType {pid: $pid})-[:attributes|inheritsFrom*]->(:DataType)<-[:dataType]-(:Attribute)<-[:executableOn]-(o:Operation) RETURN o") @Query(""" MATCH (d:DataType {pid: $pid})<-[:dataType]-(:Attribute)<-[:executableOn]-(o:Operation) RETURN o diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/ITechnologyInterfaceDao.java b/src/main/java/edu/kit/datamanager/idoris/dao/ITechnologyInterfaceDao.java index b47a188..e69779e 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/ITechnologyInterfaceDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/dao/ITechnologyInterfaceDao.java @@ -17,8 +17,6 @@ package edu.kit.datamanager.idoris.dao; import edu.kit.datamanager.idoris.domain.entities.TechnologyInterface; -import org.springframework.data.rest.core.annotation.RepositoryRestResource; -@RepositoryRestResource(collectionResourceRel = "technologyInterfaces", path = "technologyInterfaces") public interface ITechnologyInterfaceDao extends IGenericRepo { } diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/ITypeProfileDao.java b/src/main/java/edu/kit/datamanager/idoris/dao/ITypeProfileDao.java index c996067..452f8ec 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/ITypeProfileDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/dao/ITypeProfileDao.java @@ -19,10 +19,8 @@ import edu.kit.datamanager.idoris.domain.entities.TypeProfile; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import org.springframework.data.neo4j.repository.query.Query; -import org.springframework.data.rest.core.annotation.RepositoryRestResource; @OpenAPIDefinition -@RepositoryRestResource(collectionResourceRel = "typeProfiles", path = "typeProfiles") public interface ITypeProfileDao extends IGenericRepo { @Query("MATCH (d:TypeProfile {pid: $pid})-[i:inheritsFrom*]->(d2:TypeProfile)-[profileAttribute:attributes]->(dataType:DataType) RETURN i, d2, collect(profileAttribute), collect(dataType)") Iterable findAllTypeProfilesWithTheirAttributesInInheritanceChain(String pid); diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/IUserDao.java b/src/main/java/edu/kit/datamanager/idoris/dao/IUserDao.java index 5c51f60..3dbd3ff 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/IUserDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/dao/IUserDao.java @@ -21,9 +21,7 @@ import org.springframework.data.neo4j.repository.query.Query; import org.springframework.data.repository.ListCrudRepository; import org.springframework.data.repository.PagingAndSortingRepository; -import org.springframework.data.rest.core.annotation.RepositoryRestResource; -@RepositoryRestResource(collectionResourceRel = "users", path = "users") public interface IUserDao extends Neo4jRepository, ListCrudRepository, PagingAndSortingRepository { @Query("MATCH (u:ORCiDUser) RETURN u") Iterable findAllORCiDUsers(); diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/package-info.java b/src/main/java/edu/kit/datamanager/idoris/domain/package-info.java new file mode 100644 index 0000000..635b504 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/domain/package-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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. + */ + +/** + * Domain module for IDORIS. + * This module contains entity definitions, domain services, and business logic. + * It is responsible for the core domain concepts and their relationships. + * + *

The domain module depends on the core module for base abstractions and interfaces. + * It should not depend on infrastructure concerns like repositories or web controllers.

+ */ +@org.springframework.modulith.ApplicationModule( + displayName = "IDORIS Domain", + allowedDependencies = {"core"} +) +package edu.kit.datamanager.idoris.domain; \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/services/AtomicDataTypeService.java b/src/main/java/edu/kit/datamanager/idoris/domain/services/AtomicDataTypeService.java new file mode 100644 index 0000000..aa0d0b4 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/domain/services/AtomicDataTypeService.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.domain.services; + +import edu.kit.datamanager.idoris.core.events.EventPublisherService; +import edu.kit.datamanager.idoris.dao.IAtomicDataTypeDao; +import edu.kit.datamanager.idoris.domain.entities.AtomicDataType; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +/** + * Service for managing AtomicDataType entities. + * This service provides methods for creating, updating, and retrieving AtomicDataType entities. + * It publishes domain events when entities are created, updated, or deleted. + */ +@Service +@Slf4j +public class AtomicDataTypeService { + private final IAtomicDataTypeDao atomicDataTypeDao; + private final EventPublisherService eventPublisher; + + /** + * Creates a new AtomicDataTypeService with the given dependencies. + * + * @param atomicDataTypeDao the AtomicDataType repository + * @param eventPublisher the event publisher service + */ + public AtomicDataTypeService(IAtomicDataTypeDao atomicDataTypeDao, EventPublisherService eventPublisher) { + this.atomicDataTypeDao = atomicDataTypeDao; + this.eventPublisher = eventPublisher; + } + + /** + * Creates a new AtomicDataType entity. + * + * @param atomicDataType the AtomicDataType entity to create + * @return the created AtomicDataType entity + */ + @Transactional + public AtomicDataType createAtomicDataType(AtomicDataType atomicDataType) { + log.debug("Creating AtomicDataType: {}", atomicDataType); + AtomicDataType saved = atomicDataTypeDao.save(atomicDataType); + eventPublisher.publishEntityCreated(saved); + log.info("Created AtomicDataType with PID: {}", saved.getPid()); + return saved; + } + + /** + * Updates an existing AtomicDataType entity. + * + * @param atomicDataType the AtomicDataType entity to update + * @return the updated AtomicDataType entity + * @throws IllegalArgumentException if the AtomicDataType does not exist + */ + @Transactional + public AtomicDataType updateAtomicDataType(AtomicDataType atomicDataType) { + log.debug("Updating AtomicDataType: {}", atomicDataType); + + if (atomicDataType.getPid() == null || atomicDataType.getPid().isEmpty()) { + throw new IllegalArgumentException("AtomicDataType must have a PID to be updated"); + } + + // Get the current version before updating + AtomicDataType existing = atomicDataTypeDao.findById(atomicDataType.getPid()) + .orElseThrow(() -> new IllegalArgumentException("AtomicDataType not found with PID: " + atomicDataType.getPid())); + + Long previousVersion = existing.getVersion(); + + AtomicDataType saved = atomicDataTypeDao.save(atomicDataType); + eventPublisher.publishEntityUpdated(saved, previousVersion); + log.info("Updated AtomicDataType with PID: {}", saved.getPid()); + return saved; + } + + /** + * Deletes an AtomicDataType entity. + * + * @param pid the PID of the AtomicDataType to delete + * @throws IllegalArgumentException if the AtomicDataType does not exist + */ + @Transactional + public void deleteAtomicDataType(String pid) { + log.debug("Deleting AtomicDataType with PID: {}", pid); + + AtomicDataType atomicDataType = atomicDataTypeDao.findById(pid) + .orElseThrow(() -> new IllegalArgumentException("AtomicDataType not found with PID: " + pid)); + + atomicDataTypeDao.delete(atomicDataType); + eventPublisher.publishEntityDeleted(atomicDataType); + log.info("Deleted AtomicDataType with PID: {}", pid); + } + + /** + * Retrieves an AtomicDataType entity by its PID. + * + * @param pid the PID of the AtomicDataType to retrieve + * @return an Optional containing the AtomicDataType, or empty if not found + */ + @Transactional(readOnly = true) + public Optional getAtomicDataType(String pid) { + log.debug("Retrieving AtomicDataType with PID: {}", pid); + return atomicDataTypeDao.findById(pid); + } + + /** + * Retrieves all AtomicDataType entities. + * + * @return a list of all AtomicDataType entities + */ + @Transactional(readOnly = true) + public List getAllAtomicDataTypes() { + log.debug("Retrieving all AtomicDataTypes"); + return atomicDataTypeDao.findAll(); + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/services/AttributeMappingService.java b/src/main/java/edu/kit/datamanager/idoris/domain/services/AttributeMappingService.java new file mode 100644 index 0000000..c096f9b --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/domain/services/AttributeMappingService.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.domain.services; + +import edu.kit.datamanager.idoris.core.events.EventPublisherService; +import edu.kit.datamanager.idoris.dao.IAttributeMappingDao; +import edu.kit.datamanager.idoris.domain.entities.AttributeMapping; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +/** + * Service for managing AttributeMapping entities. + * This service provides methods for creating, updating, and retrieving AttributeMapping entities. + * It publishes domain events when entities are created, updated, or deleted. + */ +@Service +@Slf4j +public class AttributeMappingService { + private final IAttributeMappingDao attributeMappingDao; + private final EventPublisherService eventPublisher; + + /** + * Creates a new AttributeMappingService with the given dependencies. + * + * @param attributeMappingDao the AttributeMapping repository + * @param eventPublisher the event publisher service + */ + public AttributeMappingService(IAttributeMappingDao attributeMappingDao, EventPublisherService eventPublisher) { + this.attributeMappingDao = attributeMappingDao; + this.eventPublisher = eventPublisher; + } + + /** + * Creates a new AttributeMapping entity. + * + * @param attributeMapping the AttributeMapping entity to create + * @return the created AttributeMapping entity + */ + @Transactional + public AttributeMapping createAttributeMapping(AttributeMapping attributeMapping) { + log.debug("Creating AttributeMapping: {}", attributeMapping); + AttributeMapping saved = attributeMappingDao.save(attributeMapping); + eventPublisher.publishEntityCreated(saved, "AttributeMapping"); + log.info("Created AttributeMapping with ID: {}", saved.getId()); + return saved; + } + + /** + * Updates an existing AttributeMapping entity. + * + * @param attributeMapping the AttributeMapping entity to update + * @return the updated AttributeMapping entity + * @throws IllegalArgumentException if the AttributeMapping does not exist + */ + @Transactional + public AttributeMapping updateAttributeMapping(AttributeMapping attributeMapping) { + log.debug("Updating AttributeMapping: {}", attributeMapping); + + if (attributeMapping.getId() == null || attributeMapping.getId().isEmpty()) { + throw new IllegalArgumentException("AttributeMapping must have an ID to be updated"); + } + + // Check if the entity exists + if (!attributeMappingDao.existsById(attributeMapping.getId())) { + throw new IllegalArgumentException("AttributeMapping not found with ID: " + attributeMapping.getId()); + } + + AttributeMapping saved = attributeMappingDao.save(attributeMapping); + eventPublisher.publishEntityUpdated(saved, "AttributeMapping"); + log.info("Updated AttributeMapping with ID: {}", saved.getId()); + return saved; + } + + /** + * Deletes an AttributeMapping entity. + * + * @param id the ID of the AttributeMapping to delete + * @throws IllegalArgumentException if the AttributeMapping does not exist + */ + @Transactional + public void deleteAttributeMapping(String id) { + log.debug("Deleting AttributeMapping with ID: {}", id); + + AttributeMapping attributeMapping = attributeMappingDao.findById(id) + .orElseThrow(() -> new IllegalArgumentException("AttributeMapping not found with ID: " + id)); + + attributeMappingDao.delete(attributeMapping); + eventPublisher.publishEntityDeleted(attributeMapping, "AttributeMapping"); + log.info("Deleted AttributeMapping with ID: {}", id); + } + + /** + * Retrieves an AttributeMapping entity by its ID. + * + * @param id the ID of the AttributeMapping to retrieve + * @return an Optional containing the AttributeMapping, or empty if not found + */ + @Transactional(readOnly = true) + public Optional getAttributeMapping(String id) { + log.debug("Retrieving AttributeMapping with ID: {}", id); + return attributeMappingDao.findById(id); + } + + /** + * Retrieves all AttributeMapping entities. + * + * @return a list of all AttributeMapping entities + */ + @Transactional(readOnly = true) + public List getAllAttributeMappings() { + log.debug("Retrieving all AttributeMappings"); + return attributeMappingDao.findAll(); + } + + /** + * Finds AttributeMapping entities by input attribute PID. + * + * @param pid the PID of the input attribute + * @return a list of AttributeMapping entities + */ + @Transactional(readOnly = true) + public List findByInputAttributePid(String pid) { + log.debug("Finding AttributeMappings by input attribute PID: {}", pid); + return (List) attributeMappingDao.findByInputAttributePid(pid); + } + + /** + * Finds AttributeMapping entities by output attribute PID. + * + * @param pid the PID of the output attribute + * @return a list of AttributeMapping entities + */ + @Transactional(readOnly = true) + public List findByOutputAttributePid(String pid) { + log.debug("Finding AttributeMappings by output attribute PID: {}", pid); + return (List) attributeMappingDao.findByOutputAttributePid(pid); + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/services/AttributeService.java b/src/main/java/edu/kit/datamanager/idoris/domain/services/AttributeService.java new file mode 100644 index 0000000..7a69df1 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/domain/services/AttributeService.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.domain.services; + +import edu.kit.datamanager.idoris.core.events.EventPublisherService; +import edu.kit.datamanager.idoris.dao.IAttributeDao; +import edu.kit.datamanager.idoris.domain.entities.Attribute; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +/** + * Service for managing Attribute entities. + * This service provides methods for creating, updating, and retrieving Attribute entities. + * It publishes domain events when entities are created, updated, or deleted. + */ +@Service +@Slf4j +public class AttributeService { + private final IAttributeDao attributeDao; + private final EventPublisherService eventPublisher; + + /** + * Creates a new AttributeService with the given dependencies. + * + * @param attributeDao the Attribute repository + * @param eventPublisher the event publisher service + */ + public AttributeService(IAttributeDao attributeDao, EventPublisherService eventPublisher) { + this.attributeDao = attributeDao; + this.eventPublisher = eventPublisher; + } + + /** + * Creates a new Attribute entity. + * + * @param attribute the Attribute entity to create + * @return the created Attribute entity + */ + @Transactional + public Attribute createAttribute(Attribute attribute) { + log.debug("Creating Attribute: {}", attribute); + Attribute saved = attributeDao.save(attribute); + eventPublisher.publishEntityCreated(saved); + log.info("Created Attribute with PID: {}", saved.getPid()); + return saved; + } + + /** + * Updates an existing Attribute entity. + * + * @param attribute the Attribute entity to update + * @return the updated Attribute entity + * @throws IllegalArgumentException if the Attribute does not exist + */ + @Transactional + public Attribute updateAttribute(Attribute attribute) { + log.debug("Updating Attribute: {}", attribute); + + if (attribute.getPid() == null || attribute.getPid().isEmpty()) { + throw new IllegalArgumentException("Attribute must have a PID to be updated"); + } + + // Get the current version before updating + Attribute existing = attributeDao.findById(attribute.getPid()) + .orElseThrow(() -> new IllegalArgumentException("Attribute not found with PID: " + attribute.getPid())); + + Long previousVersion = existing.getVersion(); + + Attribute saved = attributeDao.save(attribute); + eventPublisher.publishEntityUpdated(saved, previousVersion); + log.info("Updated Attribute with PID: {}", saved.getPid()); + return saved; + } + + /** + * Deletes an Attribute entity. + * + * @param pid the PID of the Attribute to delete + * @throws IllegalArgumentException if the Attribute does not exist + */ + @Transactional + public void deleteAttribute(String pid) { + log.debug("Deleting Attribute with PID: {}", pid); + + Attribute attribute = attributeDao.findById(pid) + .orElseThrow(() -> new IllegalArgumentException("Attribute not found with PID: " + pid)); + + attributeDao.delete(attribute); + eventPublisher.publishEntityDeleted(attribute); + log.info("Deleted Attribute with PID: {}", pid); + } + + /** + * Retrieves an Attribute entity by its PID. + * + * @param pid the PID of the Attribute to retrieve + * @return an Optional containing the Attribute, or empty if not found + */ + @Transactional(readOnly = true) + public Optional getAttribute(String pid) { + log.debug("Retrieving Attribute with PID: {}", pid); + return attributeDao.findById(pid); + } + + /** + * Retrieves all Attribute entities. + * + * @return a list of all Attribute entities + */ + @Transactional(readOnly = true) + public List getAllAttributes() { + log.debug("Retrieving all Attributes"); + return attributeDao.findAll(); + } + + /** + * Deletes orphaned Attribute entities. + * An orphaned Attribute is one that has a dataType relationship but is not referenced by any other node. + */ + @Transactional + public void deleteOrphanedAttributes() { + log.debug("Deleting orphaned Attributes"); + attributeDao.deleteOrphanedAttributes(); + log.info("Deleted orphaned Attributes"); + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/services/OperationService.java b/src/main/java/edu/kit/datamanager/idoris/domain/services/OperationService.java new file mode 100644 index 0000000..1b67525 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/domain/services/OperationService.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.domain.services; + +import edu.kit.datamanager.idoris.core.events.EventPublisherService; +import edu.kit.datamanager.idoris.dao.IOperationDao; +import edu.kit.datamanager.idoris.domain.entities.Operation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +/** + * Service for managing Operation entities. + * This service provides methods for creating, updating, and retrieving Operation entities. + * It publishes domain events when entities are created, updated, or deleted. + */ +@Service +@Slf4j +public class OperationService { + private final IOperationDao operationDao; + private final EventPublisherService eventPublisher; + + /** + * Creates a new OperationService with the given dependencies. + * + * @param operationDao the Operation repository + * @param eventPublisher the event publisher service + */ + public OperationService(IOperationDao operationDao, EventPublisherService eventPublisher) { + this.operationDao = operationDao; + this.eventPublisher = eventPublisher; + } + + /** + * Creates a new Operation entity. + * + * @param operation the Operation entity to create + * @return the created Operation entity + */ + @Transactional + public Operation createOperation(Operation operation) { + log.debug("Creating Operation: {}", operation); + Operation saved = operationDao.save(operation); + eventPublisher.publishEntityCreated(saved); + log.info("Created Operation with PID: {}", saved.getPid()); + return saved; + } + + /** + * Updates an existing Operation entity. + * + * @param operation the Operation entity to update + * @return the updated Operation entity + * @throws IllegalArgumentException if the Operation does not exist + */ + @Transactional + public Operation updateOperation(Operation operation) { + log.debug("Updating Operation: {}", operation); + + if (operation.getPid() == null || operation.getPid().isEmpty()) { + throw new IllegalArgumentException("Operation must have a PID to be updated"); + } + + // Get the current version before updating + Operation existing = operationDao.findById(operation.getPid()) + .orElseThrow(() -> new IllegalArgumentException("Operation not found with PID: " + operation.getPid())); + + Long previousVersion = existing.getVersion(); + + Operation saved = operationDao.save(operation); + eventPublisher.publishEntityUpdated(saved, previousVersion); + log.info("Updated Operation with PID: {}", saved.getPid()); + return saved; + } + + /** + * Deletes an Operation entity. + * + * @param pid the PID of the Operation to delete + * @throws IllegalArgumentException if the Operation does not exist + */ + @Transactional + public void deleteOperation(String pid) { + log.debug("Deleting Operation with PID: {}", pid); + + Operation operation = operationDao.findById(pid) + .orElseThrow(() -> new IllegalArgumentException("Operation not found with PID: " + pid)); + + operationDao.delete(operation); + eventPublisher.publishEntityDeleted(operation); + log.info("Deleted Operation with PID: {}", pid); + } + + /** + * Retrieves an Operation entity by its PID. + * + * @param pid the PID of the Operation to retrieve + * @return an Optional containing the Operation, or empty if not found + */ + @Transactional(readOnly = true) + public Optional getOperation(String pid) { + log.debug("Retrieving Operation with PID: {}", pid); + return operationDao.findById(pid); + } + + /** + * Retrieves all Operation entities. + * + * @return a list of all Operation entities + */ + @Transactional(readOnly = true) + public List getAllOperations() { + log.debug("Retrieving all Operations"); + return operationDao.findAll(); + } + + /** + * Retrieves all Operations for a DataType. + * + * @param dataTypePid the PID of the DataType + * @return an iterable of Operations for the DataType + */ + @Transactional(readOnly = true) + public Iterable getOperationsForDataType(String dataTypePid) { + log.debug("Retrieving Operations for DataType with PID: {}", dataTypePid); + return operationDao.getOperationsForDataType(dataTypePid); + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/services/TechnologyInterfaceService.java b/src/main/java/edu/kit/datamanager/idoris/domain/services/TechnologyInterfaceService.java new file mode 100644 index 0000000..0498bff --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/domain/services/TechnologyInterfaceService.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.domain.services; + +import edu.kit.datamanager.idoris.core.events.EventPublisherService; +import edu.kit.datamanager.idoris.dao.ITechnologyInterfaceDao; +import edu.kit.datamanager.idoris.domain.entities.TechnologyInterface; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +/** + * Service for managing TechnologyInterface entities. + * This service provides methods for creating, updating, and retrieving TechnologyInterface entities. + * It publishes domain events when entities are created, updated, or deleted. + */ +@Service +@Slf4j +public class TechnologyInterfaceService { + private final ITechnologyInterfaceDao technologyInterfaceDao; + private final EventPublisherService eventPublisher; + + /** + * Creates a new TechnologyInterfaceService with the given dependencies. + * + * @param technologyInterfaceDao the TechnologyInterface repository + * @param eventPublisher the event publisher service + */ + public TechnologyInterfaceService(ITechnologyInterfaceDao technologyInterfaceDao, EventPublisherService eventPublisher) { + this.technologyInterfaceDao = technologyInterfaceDao; + this.eventPublisher = eventPublisher; + } + + /** + * Creates a new TechnologyInterface entity. + * + * @param technologyInterface the TechnologyInterface entity to create + * @return the created TechnologyInterface entity + */ + @Transactional + public TechnologyInterface createTechnologyInterface(TechnologyInterface technologyInterface) { + log.debug("Creating TechnologyInterface: {}", technologyInterface); + TechnologyInterface saved = technologyInterfaceDao.save(technologyInterface); + eventPublisher.publishEntityCreated(saved); + log.info("Created TechnologyInterface with PID: {}", saved.getPid()); + return saved; + } + + /** + * Updates an existing TechnologyInterface entity. + * + * @param technologyInterface the TechnologyInterface entity to update + * @return the updated TechnologyInterface entity + * @throws IllegalArgumentException if the TechnologyInterface does not exist + */ + @Transactional + public TechnologyInterface updateTechnologyInterface(TechnologyInterface technologyInterface) { + log.debug("Updating TechnologyInterface: {}", technologyInterface); + + if (technologyInterface.getPid() == null || technologyInterface.getPid().isEmpty()) { + throw new IllegalArgumentException("TechnologyInterface must have a PID to be updated"); + } + + // Get the current version before updating + TechnologyInterface existing = technologyInterfaceDao.findById(technologyInterface.getPid()) + .orElseThrow(() -> new IllegalArgumentException("TechnologyInterface not found with PID: " + technologyInterface.getPid())); + + Long previousVersion = existing.getVersion(); + + TechnologyInterface saved = technologyInterfaceDao.save(technologyInterface); + eventPublisher.publishEntityUpdated(saved, previousVersion); + log.info("Updated TechnologyInterface with PID: {}", saved.getPid()); + return saved; + } + + /** + * Deletes a TechnologyInterface entity. + * + * @param pid the PID of the TechnologyInterface to delete + * @throws IllegalArgumentException if the TechnologyInterface does not exist + */ + @Transactional + public void deleteTechnologyInterface(String pid) { + log.debug("Deleting TechnologyInterface with PID: {}", pid); + + TechnologyInterface technologyInterface = technologyInterfaceDao.findById(pid) + .orElseThrow(() -> new IllegalArgumentException("TechnologyInterface not found with PID: " + pid)); + + technologyInterfaceDao.delete(technologyInterface); + eventPublisher.publishEntityDeleted(technologyInterface); + log.info("Deleted TechnologyInterface with PID: {}", pid); + } + + /** + * Retrieves a TechnologyInterface entity by its PID. + * + * @param pid the PID of the TechnologyInterface to retrieve + * @return an Optional containing the TechnologyInterface, or empty if not found + */ + @Transactional(readOnly = true) + public Optional getTechnologyInterface(String pid) { + log.debug("Retrieving TechnologyInterface with PID: {}", pid); + return technologyInterfaceDao.findById(pid); + } + + /** + * Retrieves all TechnologyInterface entities. + * + * @return a list of all TechnologyInterface entities + */ + @Transactional(readOnly = true) + public List getAllTechnologyInterfaces() { + log.debug("Retrieving all TechnologyInterfaces"); + return technologyInterfaceDao.findAll(); + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/services/TypeProfileService.java b/src/main/java/edu/kit/datamanager/idoris/domain/services/TypeProfileService.java new file mode 100644 index 0000000..77ad78c --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/domain/services/TypeProfileService.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.domain.services; + +import edu.kit.datamanager.idoris.core.events.EventPublisherService; +import edu.kit.datamanager.idoris.dao.ITypeProfileDao; +import edu.kit.datamanager.idoris.domain.entities.TypeProfile; +import edu.kit.datamanager.idoris.rules.validation.ValidationPolicyValidator; +import edu.kit.datamanager.idoris.rules.validation.ValidationResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +/** + * Service for managing TypeProfile entities. + * This service provides methods for creating, updating, and retrieving TypeProfile entities. + * It publishes domain events when entities are created, updated, or deleted. + */ +@Service +@Slf4j +public class TypeProfileService { + private final ITypeProfileDao typeProfileDao; + private final EventPublisherService eventPublisher; + + /** + * Creates a new TypeProfileService with the given dependencies. + * + * @param typeProfileDao the TypeProfile repository + * @param eventPublisher the event publisher service + */ + public TypeProfileService(ITypeProfileDao typeProfileDao, EventPublisherService eventPublisher) { + this.typeProfileDao = typeProfileDao; + this.eventPublisher = eventPublisher; + } + + /** + * Creates a new TypeProfile entity. + * + * @param typeProfile the TypeProfile entity to create + * @return the created TypeProfile entity + */ + @Transactional + public TypeProfile createTypeProfile(TypeProfile typeProfile) { + log.debug("Creating TypeProfile: {}", typeProfile); + TypeProfile saved = typeProfileDao.save(typeProfile); + eventPublisher.publishEntityCreated(saved); + log.info("Created TypeProfile with PID: {}", saved.getPid()); + return saved; + } + + /** + * Updates an existing TypeProfile entity. + * + * @param typeProfile the TypeProfile entity to update + * @return the updated TypeProfile entity + * @throws IllegalArgumentException if the TypeProfile does not exist + */ + @Transactional + public TypeProfile updateTypeProfile(TypeProfile typeProfile) { + log.debug("Updating TypeProfile: {}", typeProfile); + + if (typeProfile.getPid() == null || typeProfile.getPid().isEmpty()) { + throw new IllegalArgumentException("TypeProfile must have a PID to be updated"); + } + + // Get the current version before updating + TypeProfile existing = typeProfileDao.findById(typeProfile.getPid()) + .orElseThrow(() -> new IllegalArgumentException("TypeProfile not found with PID: " + typeProfile.getPid())); + + Long previousVersion = existing.getVersion(); + + TypeProfile saved = typeProfileDao.save(typeProfile); + eventPublisher.publishEntityUpdated(saved, previousVersion); + log.info("Updated TypeProfile with PID: {}", saved.getPid()); + return saved; + } + + /** + * Deletes a TypeProfile entity. + * + * @param pid the PID of the TypeProfile to delete + * @throws IllegalArgumentException if the TypeProfile does not exist + */ + @Transactional + public void deleteTypeProfile(String pid) { + log.debug("Deleting TypeProfile with PID: {}", pid); + + TypeProfile typeProfile = typeProfileDao.findById(pid) + .orElseThrow(() -> new IllegalArgumentException("TypeProfile not found with PID: " + pid)); + + typeProfileDao.delete(typeProfile); + eventPublisher.publishEntityDeleted(typeProfile); + log.info("Deleted TypeProfile with PID: {}", pid); + } + + /** + * Retrieves a TypeProfile entity by its PID. + * + * @param pid the PID of the TypeProfile to retrieve + * @return an Optional containing the TypeProfile, or empty if not found + */ + @Transactional(readOnly = true) + public Optional getTypeProfile(String pid) { + log.debug("Retrieving TypeProfile with PID: {}", pid); + return typeProfileDao.findById(pid); + } + + /** + * Retrieves all TypeProfile entities. + * + * @return a list of all TypeProfile entities + */ + @Transactional(readOnly = true) + public List getAllTypeProfiles() { + log.debug("Retrieving all TypeProfiles"); + return typeProfileDao.findAll(); + } + + /** + * Validates a TypeProfile entity. + * + * @param pid the PID of the TypeProfile to validate + * @return the validation result + * @throws IllegalArgumentException if the TypeProfile does not exist + */ + @Transactional(readOnly = true) + public ValidationResult validateTypeProfile(String pid) { + log.debug("Validating TypeProfile with PID: {}", pid); + + TypeProfile typeProfile = typeProfileDao.findById(pid) + .orElseThrow(() -> new IllegalArgumentException("TypeProfile not found with PID: " + pid)); + + ValidationPolicyValidator validator = new ValidationPolicyValidator(); + return typeProfile.execute(validator); + } + + /** + * Retrieves all TypeProfiles in the inheritance chain of a TypeProfile. + * + * @param pid the PID of the TypeProfile + * @return an Iterable of TypeProfiles in the inheritance chain + * @throws IllegalArgumentException if the TypeProfile does not exist + */ + @Transactional(readOnly = true) + public Iterable getInheritanceChain(String pid) { + log.debug("Retrieving inheritance chain for TypeProfile with PID: {}", pid); + + // Check if the TypeProfile exists + if (!typeProfileDao.existsById(pid)) { + throw new IllegalArgumentException("TypeProfile not found with PID: " + pid); + } + + return typeProfileDao.findAllTypeProfilesInInheritanceChain(pid); + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/services/package-info.java b/src/main/java/edu/kit/datamanager/idoris/domain/services/package-info.java new file mode 100644 index 0000000..3fe3c79 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/domain/services/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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. + */ + +/** + * Service layer for the domain module. + * This package contains service classes that implement business logic for domain entities. + * These services use repositories for data access and publish domain events when entities change. + */ +package edu.kit.datamanager.idoris.domain.services; \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeNotifier.java b/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeNotifier.java new file mode 100644 index 0000000..dca91a3 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeNotifier.java @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.notification; + +import edu.kit.datamanager.idoris.core.events.EntityCreatedEvent; +import edu.kit.datamanager.idoris.core.events.EntityDeletedEvent; +import edu.kit.datamanager.idoris.core.events.EntityUpdatedEvent; +import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * Component that listens for entity change events and notifies subscribers. + * This is a simple implementation of a callback system for entity changes. + * In a real-world scenario, this would likely use a more sophisticated notification mechanism. + */ +@Component +@Slf4j +public class EntityChangeNotifier { + + // Map of entity type to set of subscribers for that type + private final Map> typeSubscribers = new ConcurrentHashMap<>(); + + // Map of entity PID to set of subscribers for that specific entity + private final Map> entitySubscribers = new ConcurrentHashMap<>(); + + /** + * Subscribes to changes for a specific entity type. + * + * @param entityType the entity type to subscribe to + * @param subscriber the subscriber to notify + */ + public void subscribeToType(String entityType, EntityChangeSubscriber subscriber) { + log.debug("Subscribing to changes for entity type: {}", entityType); + typeSubscribers.computeIfAbsent(entityType, k -> new CopyOnWriteArraySet<>()).add(subscriber); + } + + /** + * Subscribes to changes for a specific entity. + * + * @param entityPid the PID of the entity to subscribe to + * @param subscriber the subscriber to notify + */ + public void subscribeToEntity(String entityPid, EntityChangeSubscriber subscriber) { + log.debug("Subscribing to changes for entity with PID: {}", entityPid); + entitySubscribers.computeIfAbsent(entityPid, k -> new CopyOnWriteArraySet<>()).add(subscriber); + } + + /** + * Unsubscribes from changes for a specific entity type. + * + * @param entityType the entity type to unsubscribe from + * @param subscriber the subscriber to remove + */ + public void unsubscribeFromType(String entityType, EntityChangeSubscriber subscriber) { + log.debug("Unsubscribing from changes for entity type: {}", entityType); + Set subscribers = typeSubscribers.get(entityType); + if (subscribers != null) { + subscribers.remove(subscriber); + } + } + + /** + * Unsubscribes from changes for a specific entity. + * + * @param entityPid the PID of the entity to unsubscribe from + * @param subscriber the subscriber to remove + */ + public void unsubscribeFromEntity(String entityPid, EntityChangeSubscriber subscriber) { + log.debug("Unsubscribing from changes for entity with PID: {}", entityPid); + Set subscribers = entitySubscribers.get(entityPid); + if (subscribers != null) { + subscribers.remove(subscriber); + } + } + + /** + * Handles entity created events. + * + * @param event the entity created event + */ + @EventListener + public void handleEntityCreated(EntityCreatedEvent event) { + GenericIDORISEntity entity = event.getEntity(); + String entityType = entity.getClass().getSimpleName(); + String entityPid = entity.getPid(); + + log.debug("Handling EntityCreatedEvent for entity type: {}, PID: {}", entityType, entityPid); + + // Notify type subscribers + Set typeSubscribersSet = typeSubscribers.get(entityType); + if (typeSubscribersSet != null) { + for (EntityChangeSubscriber subscriber : typeSubscribersSet) { + try { + subscriber.onEntityCreated(entity); + } catch (Exception e) { + log.error("Error notifying subscriber for entity creation: {}", e.getMessage(), e); + } + } + } + + // Notify entity subscribers (unlikely for creation, but included for completeness) + // Skip if entityPid is null to avoid NullPointerException + if (entityPid != null) { + Set entitySubscribersSet = entitySubscribers.get(entityPid); + if (entitySubscribersSet != null) { + for (EntityChangeSubscriber subscriber : entitySubscribersSet) { + try { + subscriber.onEntityCreated(entity); + } catch (Exception e) { + log.error("Error notifying subscriber for entity creation: {}", e.getMessage(), e); + } + } + } + } + } + + /** + * Handles entity updated events. + * + * @param event the entity updated event + */ + @EventListener + public void handleEntityUpdated(EntityUpdatedEvent event) { + GenericIDORISEntity entity = event.getEntity(); + String entityType = entity.getClass().getSimpleName(); + String entityPid = entity.getPid(); + + log.debug("Handling EntityUpdatedEvent for entity type: {}, PID: {}", entityType, entityPid); + + // Notify type subscribers + Set typeSubscribersSet = typeSubscribers.get(entityType); + if (typeSubscribersSet != null) { + for (EntityChangeSubscriber subscriber : typeSubscribersSet) { + try { + subscriber.onEntityUpdated(entity, event.getPreviousVersion()); + } catch (Exception e) { + log.error("Error notifying subscriber for entity update: {}", e.getMessage(), e); + } + } + } + + // Notify entity subscribers + // Skip if entityPid is null to avoid NullPointerException + if (entityPid != null) { + Set entitySubscribersSet = entitySubscribers.get(entityPid); + if (entitySubscribersSet != null) { + for (EntityChangeSubscriber subscriber : entitySubscribersSet) { + try { + subscriber.onEntityUpdated(entity, event.getPreviousVersion()); + } catch (Exception e) { + log.error("Error notifying subscriber for entity update: {}", e.getMessage(), e); + } + } + } + } + } + + /** + * Handles entity deleted events. + * + * @param event the entity deleted event + */ + @EventListener + public void handleEntityDeleted(EntityDeletedEvent event) { + GenericIDORISEntity entity = event.getEntity(); + String entityType = event.getEntityType(); + String entityPid = event.getEntityPid(); + + log.debug("Handling EntityDeletedEvent for entity type: {}, PID: {}", entityType, entityPid); + + // Notify type subscribers + Set typeSubscribersSet = typeSubscribers.get(entityType); + if (typeSubscribersSet != null) { + for (EntityChangeSubscriber subscriber : typeSubscribersSet) { + try { + subscriber.onEntityDeleted(entity); + } catch (Exception e) { + log.error("Error notifying subscriber for entity deletion: {}", e.getMessage(), e); + } + } + } + + // Notify entity subscribers + // Skip if entityPid is null to avoid NullPointerException + if (entityPid != null) { + Set entitySubscribersSet = entitySubscribers.get(entityPid); + if (entitySubscribersSet != null) { + for (EntityChangeSubscriber subscriber : entitySubscribersSet) { + try { + subscriber.onEntityDeleted(entity); + } catch (Exception e) { + log.error("Error notifying subscriber for entity deletion: {}", e.getMessage(), e); + } + } + + // Remove subscribers for this entity since it no longer exists + entitySubscribers.remove(entityPid); + } + } + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeSubscriber.java b/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeSubscriber.java new file mode 100644 index 0000000..dc97846 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeSubscriber.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.notification; + +import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; + +/** + * Interface for subscribers that want to be notified of entity changes. + * Implementations of this interface can be registered with the EntityChangeNotifier + * to receive callbacks when entities are created, updated, or deleted. + */ +public interface EntityChangeSubscriber { + + /** + * Called when an entity is created. + * + * @param entity the created entity + */ + void onEntityCreated(GenericIDORISEntity entity); + + /** + * Called when an entity is updated. + * + * @param entity the updated entity + * @param previousVersion the version of the entity before the update + */ + void onEntityUpdated(GenericIDORISEntity entity, Long previousVersion); + + /** + * Called when an entity is deleted. + * + * @param entity the deleted entity + */ + void onEntityDeleted(GenericIDORISEntity entity); +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/notification/LoggingEntityChangeSubscriber.java b/src/main/java/edu/kit/datamanager/idoris/notification/LoggingEntityChangeSubscriber.java new file mode 100644 index 0000000..3b60dc7 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/notification/LoggingEntityChangeSubscriber.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.notification; + +import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * A simple implementation of EntityChangeSubscriber that logs entity changes. + * This class is provided as an example of how to implement the EntityChangeSubscriber interface. + * In a real-world scenario, subscribers might send notifications via email, webhooks, or other channels. + */ +@Component +@Slf4j +public class LoggingEntityChangeSubscriber implements EntityChangeSubscriber { + + /** + * Called when an entity is created. + * Logs information about the created entity. + * + * @param entity the created entity + */ + @Override + public void onEntityCreated(GenericIDORISEntity entity) { + log.info("Entity created: type={}, pid={}, name={}", + entity.getClass().getSimpleName(), + entity.getPid(), + entity.getName()); + } + + /** + * Called when an entity is updated. + * Logs information about the updated entity and its previous version. + * + * @param entity the updated entity + * @param previousVersion the version of the entity before the update + */ + @Override + public void onEntityUpdated(GenericIDORISEntity entity, Long previousVersion) { + log.info("Entity updated: type={}, pid={}, name={}, previousVersion={}, newVersion={}", + entity.getClass().getSimpleName(), + entity.getPid(), + entity.getName(), + previousVersion, + entity.getVersion()); + } + + /** + * Called when an entity is deleted. + * Logs information about the deleted entity. + * + * @param entity the deleted entity + */ + @Override + public void onEntityDeleted(GenericIDORISEntity entity) { + log.info("Entity deleted: type={}, pid={}, name={}", + entity.getClass().getSimpleName(), + entity.getPid(), + entity.getName()); + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/notification/package-info.java b/src/main/java/edu/kit/datamanager/idoris/notification/package-info.java new file mode 100644 index 0000000..2faccd1 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/notification/package-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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. + */ + +/** + * Notification module for IDORIS. + * This module is responsible for notifying subscribers about entity changes. + * It provides a callback mechanism for external systems to be notified when entities are created, updated, or deleted. + * + *

The notification module depends on the core module for event infrastructure and the domain module for entity definitions. + * It listens for entity lifecycle events and notifies subscribers.

+ */ +@org.springframework.modulith.ApplicationModule( + displayName = "IDORIS Notification", + allowedDependencies = {"core", "domain"} +) +package edu.kit.datamanager.idoris.notification; \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/PIDGenerationEventListener.java b/src/main/java/edu/kit/datamanager/idoris/pids/PIDGenerationEventListener.java new file mode 100644 index 0000000..d313efb --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/pids/PIDGenerationEventListener.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.pids; + +import edu.kit.datamanager.idoris.core.events.EntityCreatedEvent; +import edu.kit.datamanager.idoris.core.events.EventPublisherService; +import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** + * Event listener that generates PIDs for newly created entities. + * This listener subscribes to EntityCreatedEvent and uses the TypedPIDMakerIDGenerator + * to generate PIDs for entities that don't already have one. + */ +@Component +@Slf4j +public class PIDGenerationEventListener { + private final TypedPIDMakerIDGenerator pidGenerator; + private final EventPublisherService eventPublisher; + + /** + * Creates a new PIDGenerationEventListener with the given dependencies. + * + * @param pidGenerator the PID generator to use + * @param eventPublisher the event publisher service + */ + public PIDGenerationEventListener(TypedPIDMakerIDGenerator pidGenerator, EventPublisherService eventPublisher) { + this.pidGenerator = pidGenerator; + this.eventPublisher = eventPublisher; + } + + /** + * Handles EntityCreatedEvent by generating a PID for the entity if it doesn't already have one. + * This method is executed in a new transaction to ensure that the PID generation is isolated + * from the transaction that created the entity. + * + * @param event the entity created event + */ + @EventListener + @Transactional + public void handleEntityCreatedEvent(EntityCreatedEvent event) { + GenericIDORISEntity entity = event.getEntity(); + log.debug("Handling EntityCreatedEvent for entity: {}", entity); + + if (entity.getPid() == null || entity.getPid().isEmpty()) { + log.info("Generating PID for entity: {}", entity); + String pid = pidGenerator.generateId(entity.getClass().getSimpleName(), entity); + entity.setPid(pid); + log.info("Generated PID: {} for entity: {}", pid, entity); + + // Publish a PID generated event + eventPublisher.publishPIDGenerated(entity, pid); + } else { + log.debug("Entity already has a PID: {}", entity.getPid()); + } + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/TypedPIDMakerIDGenerator.java b/src/main/java/edu/kit/datamanager/idoris/pids/TypedPIDMakerIDGenerator.java index f8f3f87..9f4b67c 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/TypedPIDMakerIDGenerator.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/TypedPIDMakerIDGenerator.java @@ -43,7 +43,7 @@ @Component @Slf4j @ConditionalOnBean(TypedPIDMakerConfig.class) -public final class TypedPIDMakerIDGenerator implements IdGenerator { +public class TypedPIDMakerIDGenerator implements IdGenerator { private final TypedPIDMakerClient client; private final TypedPIDMakerConfig config; diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/package-info.java b/src/main/java/edu/kit/datamanager/idoris/pids/package-info.java new file mode 100644 index 0000000..2e2d5dc --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/pids/package-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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. + */ + +/** + * PID module for IDORIS. + * This module is responsible for PID generation and management. + * It provides services for generating PIDs, creating PID records, and managing PID-related operations. + * + *

The PID module depends on the core module for event infrastructure and the domain module for entity definitions. + * It listens for entity lifecycle events and generates PIDs for entities as needed.

+ */ +@org.springframework.modulith.ApplicationModule( + displayName = "IDORIS PID Management", + allowedDependencies = {"core", "domain"} +) +package edu.kit.datamanager.idoris.pids; \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/repository/package-info.java b/src/main/java/edu/kit/datamanager/idoris/repository/package-info.java new file mode 100644 index 0000000..d304608 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/repository/package-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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. + */ + +/** + * Repository module for IDORIS. + * This module is responsible for data access and persistence. + * It provides repositories for accessing and manipulating entities in the database. + * + *

The repository module depends on the core module for base abstractions and the domain module for entity definitions. + * It should not depend on web or service concerns.

+ */ +@org.springframework.modulith.ApplicationModule( + displayName = "IDORIS Repository", + allowedDependencies = {"core", "domain"} +) +package edu.kit.datamanager.idoris.repository; \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/web/ValidationException.java b/src/main/java/edu/kit/datamanager/idoris/web/ValidationException.java new file mode 100644 index 0000000..d49e78b --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/web/ValidationException.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.web; + +import edu.kit.datamanager.idoris.rules.validation.ValidationResult; +import lombok.Getter; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@Getter +@ResponseStatus(HttpStatus.BAD_REQUEST) +public class ValidationException extends RuntimeException { + private final ValidationResult validationResult; + + public ValidationException(String message, ValidationResult validationResult) { + super(message); + this.validationResult = validationResult; + } + +} diff --git a/src/main/java/edu/kit/datamanager/idoris/web/ValidationExceptionHandler.java b/src/main/java/edu/kit/datamanager/idoris/web/ValidationExceptionHandler.java new file mode 100644 index 0000000..58e2fdb --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/web/ValidationExceptionHandler.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.web;/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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. + */ + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import java.util.HashMap; +import java.util.Map; + +@ControllerAdvice +public class ValidationExceptionHandler { + + @ExceptionHandler(ValidationException.class) + public ResponseEntity> handleValidationException(ValidationException ex) { + Map response = new HashMap<>(); + response.put("error", "Validation failed"); + response.put("message", ex.getMessage()); + response.put("validationResult", ex.getValidationResult().getOutputMessages()); + + return ResponseEntity.badRequest().body(response); + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/web/api/IAttributeApi.java b/src/main/java/edu/kit/datamanager/idoris/web/api/IAttributeApi.java new file mode 100644 index 0000000..ec1bafc --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/web/api/IAttributeApi.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.web.api; + +import edu.kit.datamanager.idoris.domain.entities.Attribute; +import edu.kit.datamanager.idoris.domain.entities.DataType; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.EntityModel; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +/** + * API interface for Attribute endpoints. + * This interface defines the REST API for managing Attribute entities. + */ +@Tag(name = "Attribute", description = "API for managing Attributes") +public interface IAttributeApi { + + /** + * Gets all Attribute entities. + * + * @return a collection of all Attribute entities + */ + @GetMapping + @Operation( + summary = "Get all Attributes", + description = "Returns a collection of all Attribute entities", + responses = { + @ApiResponse(responseCode = "200", description = "Attributes found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = Attribute.class))) + } + ) + ResponseEntity>> getAllAttributes(); + + /** + * Gets an Attribute entity by its PID. + * + * @param pid the PID of the Attribute to retrieve + * @return the Attribute entity + */ + @GetMapping("/{pid}") + @Operation( + summary = "Get an Attribute by PID", + description = "Returns an Attribute entity by its PID", + responses = { + @ApiResponse(responseCode = "200", description = "Attribute found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = Attribute.class))), + @ApiResponse(responseCode = "404", description = "Attribute not found") + } + ) + ResponseEntity> getAttribute( + @Parameter(description = "PID of the Attribute", required = true) + @PathVariable String pid); + + /** + * Gets the DataType of an Attribute. + * + * @param pid the PID of the Attribute + * @return the DataType of the Attribute + */ + @GetMapping("/{pid}/dataType") + @Operation( + summary = "Get the DataType of an Attribute", + description = "Returns the DataType of an Attribute", + responses = { + @ApiResponse(responseCode = "200", description = "DataType found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = DataType.class))), + @ApiResponse(responseCode = "404", description = "Attribute not found") + } + ) + ResponseEntity> getDataType( + @Parameter(description = "PID of the Attribute", required = true) + @PathVariable String pid); + + /** + * Creates a new Attribute entity. + * + * @param attribute the Attribute entity to create + * @return the created Attribute entity + */ + @PostMapping + @Operation( + summary = "Create a new Attribute", + description = "Creates a new Attribute entity", + responses = { + @ApiResponse(responseCode = "201", description = "Attribute created", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = Attribute.class))), + @ApiResponse(responseCode = "400", description = "Invalid input") + } + ) + ResponseEntity> createAttribute( + @Parameter(description = "Attribute to create", required = true) + @Valid @RequestBody Attribute attribute); + + /** + * Updates an existing Attribute entity. + * + * @param pid the PID of the Attribute to update + * @param attribute the updated Attribute entity + * @return the updated Attribute entity + */ + @PutMapping("/{pid}") + @Operation( + summary = "Update an Attribute", + description = "Updates an existing Attribute entity", + responses = { + @ApiResponse(responseCode = "200", description = "Attribute updated", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = Attribute.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "404", description = "Attribute not found") + } + ) + ResponseEntity> updateAttribute( + @Parameter(description = "PID of the Attribute", required = true) + @PathVariable String pid, + @Parameter(description = "Updated Attribute", required = true) + @Valid @RequestBody Attribute attribute); + + /** + * Deletes an Attribute entity. + * + * @param pid the PID of the Attribute to delete + * @return no content + */ + @DeleteMapping("/{pid}") + @Operation( + summary = "Delete an Attribute", + description = "Deletes an Attribute entity", + responses = { + @ApiResponse(responseCode = "204", description = "Attribute deleted"), + @ApiResponse(responseCode = "404", description = "Attribute not found") + } + ) + ResponseEntity deleteAttribute( + @Parameter(description = "PID of the Attribute", required = true) + @PathVariable String pid); + + /** + * Deletes orphaned Attribute entities. + * An orphaned Attribute is one that has a dataType relationship but is not referenced by any other node. + * + * @return no content + */ + @DeleteMapping("/orphaned") + @Operation( + summary = "Delete orphaned Attributes", + description = "Deletes Attribute entities that are not referenced by any other node", + responses = { + @ApiResponse(responseCode = "204", description = "Orphaned Attributes deleted") + } + ) + ResponseEntity deleteOrphanedAttributes(); +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/web/api/ITechnologyInterfaceApi.java b/src/main/java/edu/kit/datamanager/idoris/web/api/ITechnologyInterfaceApi.java new file mode 100644 index 0000000..d4eaedd --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/web/api/ITechnologyInterfaceApi.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.web.api; + +import edu.kit.datamanager.idoris.domain.entities.Attribute; +import edu.kit.datamanager.idoris.domain.entities.TechnologyInterface; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.EntityModel; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +/** + * API interface for TechnologyInterface endpoints. + * This interface defines the REST API for managing TechnologyInterface entities. + */ +@Tag(name = "TechnologyInterface", description = "API for managing TechnologyInterfaces") +public interface ITechnologyInterfaceApi { + + /** + * Gets all TechnologyInterface entities. + * + * @return a collection of all TechnologyInterface entities + */ + @GetMapping + @Operation( + summary = "Get all TechnologyInterfaces", + description = "Returns a collection of all TechnologyInterface entities", + responses = { + @ApiResponse(responseCode = "200", description = "TechnologyInterfaces found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = TechnologyInterface.class))) + } + ) + ResponseEntity>> getAllTechnologyInterfaces(); + + /** + * Gets a TechnologyInterface entity by its PID. + * + * @param pid the PID of the TechnologyInterface to retrieve + * @return the TechnologyInterface entity + */ + @GetMapping("/{pid}") + @Operation( + summary = "Get a TechnologyInterface by PID", + description = "Returns a TechnologyInterface entity by its PID", + responses = { + @ApiResponse(responseCode = "200", description = "TechnologyInterface found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = TechnologyInterface.class))), + @ApiResponse(responseCode = "404", description = "TechnologyInterface not found") + } + ) + ResponseEntity> getTechnologyInterface( + @Parameter(description = "PID of the TechnologyInterface", required = true) + @PathVariable String pid); + + /** + * Gets the attributes of a TechnologyInterface. + * + * @param pid the PID of the TechnologyInterface + * @return a collection of attributes + */ + @GetMapping("/{pid}/attributes") + @Operation( + summary = "Get attributes of a TechnologyInterface", + description = "Returns a collection of attributes of a TechnologyInterface", + responses = { + @ApiResponse(responseCode = "200", description = "Attributes found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = Attribute.class))), + @ApiResponse(responseCode = "404", description = "TechnologyInterface not found") + } + ) + ResponseEntity>> getAttributes( + @Parameter(description = "PID of the TechnologyInterface", required = true) + @PathVariable String pid); + + /** + * Gets the outputs of a TechnologyInterface. + * + * @param pid the PID of the TechnologyInterface + * @return a collection of outputs + */ + @GetMapping("/{pid}/outputs") + @Operation( + summary = "Get outputs of a TechnologyInterface", + description = "Returns a collection of outputs of a TechnologyInterface", + responses = { + @ApiResponse(responseCode = "200", description = "Outputs found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = Attribute.class))), + @ApiResponse(responseCode = "404", description = "TechnologyInterface not found") + } + ) + ResponseEntity>> getOutputs( + @Parameter(description = "PID of the TechnologyInterface", required = true) + @PathVariable String pid); + + /** + * Creates a new TechnologyInterface entity. + * + * @param technologyInterface the TechnologyInterface entity to create + * @return the created TechnologyInterface entity + */ + @PostMapping + @Operation( + summary = "Create a new TechnologyInterface", + description = "Creates a new TechnologyInterface entity", + responses = { + @ApiResponse(responseCode = "201", description = "TechnologyInterface created", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = TechnologyInterface.class))), + @ApiResponse(responseCode = "400", description = "Invalid input") + } + ) + ResponseEntity> createTechnologyInterface( + @Parameter(description = "TechnologyInterface to create", required = true) + @Valid @RequestBody TechnologyInterface technologyInterface); + + /** + * Updates an existing TechnologyInterface entity. + * + * @param pid the PID of the TechnologyInterface to update + * @param technologyInterface the updated TechnologyInterface entity + * @return the updated TechnologyInterface entity + */ + @PutMapping("/{pid}") + @Operation( + summary = "Update a TechnologyInterface", + description = "Updates an existing TechnologyInterface entity", + responses = { + @ApiResponse(responseCode = "200", description = "TechnologyInterface updated", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = TechnologyInterface.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "404", description = "TechnologyInterface not found") + } + ) + ResponseEntity> updateTechnologyInterface( + @Parameter(description = "PID of the TechnologyInterface", required = true) + @PathVariable String pid, + @Parameter(description = "Updated TechnologyInterface", required = true) + @Valid @RequestBody TechnologyInterface technologyInterface); + + /** + * Deletes a TechnologyInterface entity. + * + * @param pid the PID of the TechnologyInterface to delete + * @return no content + */ + @DeleteMapping("/{pid}") + @Operation( + summary = "Delete a TechnologyInterface", + description = "Deletes a TechnologyInterface entity", + responses = { + @ApiResponse(responseCode = "204", description = "TechnologyInterface deleted"), + @ApiResponse(responseCode = "404", description = "TechnologyInterface not found") + } + ) + ResponseEntity deleteTechnologyInterface( + @Parameter(description = "PID of the TechnologyInterface", required = true) + @PathVariable String pid); +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/web/hateoas/AtomicDataTypeModelAssembler.java b/src/main/java/edu/kit/datamanager/idoris/web/hateoas/AtomicDataTypeModelAssembler.java new file mode 100644 index 0000000..a8dd898 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/web/hateoas/AtomicDataTypeModelAssembler.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.web.hateoas; + +import edu.kit.datamanager.idoris.domain.entities.AtomicDataType; +import edu.kit.datamanager.idoris.web.v1.AtomicDataTypeController; +import org.springframework.hateoas.EntityModel; +import org.springframework.stereotype.Component; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +/** + * Assembler for converting AtomicDataType entities to EntityModel objects with HATEOAS links. + */ +@Component +public class AtomicDataTypeModelAssembler implements EntityModelAssembler { + + /** + * Converts an AtomicDataType entity to an EntityModel with HATEOAS links. + * + * @param atomicDataType the AtomicDataType entity to convert + * @return an EntityModel containing the AtomicDataType and links + */ + @Override + public EntityModel toModel(AtomicDataType atomicDataType) { + EntityModel entityModel = toModelWithoutLinks(atomicDataType); + + // Add self link + entityModel.add(linkTo(methodOn(AtomicDataTypeController.class).getAtomicDataType(atomicDataType.getPid())).withSelfRel()); + + // Add link to all atomic data types + entityModel.add(linkTo(methodOn(AtomicDataTypeController.class).getAllAtomicDataTypes()).withRel("atomicDataTypes")); + + // Add link to operations + entityModel.add(linkTo(methodOn(AtomicDataTypeController.class).getOperationsForAtomicDataType(atomicDataType.getPid())).withRel("operations")); + + // Add link to inherits from if present + if (atomicDataType.getInheritsFrom() != null) { + entityModel.add(linkTo(methodOn(AtomicDataTypeController.class).getAtomicDataType(atomicDataType.getInheritsFrom().getPid())).withRel("inheritsFrom")); + } + + return entityModel; + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/web/hateoas/AttributeModelAssembler.java b/src/main/java/edu/kit/datamanager/idoris/web/hateoas/AttributeModelAssembler.java new file mode 100644 index 0000000..a80b425 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/web/hateoas/AttributeModelAssembler.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.web.hateoas; + +import edu.kit.datamanager.idoris.domain.entities.Attribute; +import edu.kit.datamanager.idoris.web.v1.AttributeController; +import org.springframework.hateoas.EntityModel; +import org.springframework.stereotype.Component; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +/** + * Assembler for converting Attribute entities to EntityModel objects with HATEOAS links. + */ +@Component +public class AttributeModelAssembler implements EntityModelAssembler { + + /** + * Converts an Attribute entity to an EntityModel with HATEOAS links. + * + * @param attribute the Attribute entity to convert + * @return an EntityModel containing the Attribute and links + */ + @Override + public EntityModel toModel(Attribute attribute) { + EntityModel entityModel = toModelWithoutLinks(attribute); + + // Add self link + entityModel.add(linkTo(methodOn(AttributeController.class).getAttribute(attribute.getPid())).withSelfRel()); + + // Add link to data type + if (attribute.getDataType() != null) { + entityModel.add(linkTo(methodOn(AttributeController.class).getDataType(attribute.getPid())).withRel("dataType")); + } + + // Add link to override attribute if it exists + if (attribute.getOverride() != null) { + entityModel.add(linkTo(methodOn(AttributeController.class).getAttribute(attribute.getOverride().getPid())).withRel("override")); + } + + // Add link to all attributes + entityModel.add(linkTo(methodOn(AttributeController.class).getAllAttributes()).withRel("attributes")); + + return entityModel; + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/web/hateoas/DataTypeModelAssembler.java b/src/main/java/edu/kit/datamanager/idoris/web/hateoas/DataTypeModelAssembler.java new file mode 100644 index 0000000..21a164c --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/web/hateoas/DataTypeModelAssembler.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.web.hateoas; + +import edu.kit.datamanager.idoris.domain.entities.AtomicDataType; +import edu.kit.datamanager.idoris.domain.entities.DataType; +import edu.kit.datamanager.idoris.domain.entities.TypeProfile; +import edu.kit.datamanager.idoris.web.v1.AtomicDataTypeController; +import edu.kit.datamanager.idoris.web.v1.TypeProfileController; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.hateoas.EntityModel; +import org.springframework.stereotype.Component; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +/** + * Assembler for converting DataType entities to EntityModel objects with HATEOAS links. + * This assembler delegates to specific assemblers based on the type of DataType. + */ +@Component +public class DataTypeModelAssembler implements EntityModelAssembler { + + @Autowired + private AtomicDataTypeModelAssembler atomicDataTypeModelAssembler; + + @Autowired + private TypeProfileModelAssembler typeProfileModelAssembler; + + /** + * Converts a DataType entity to an EntityModel with HATEOAS links. + * Delegates to specific assemblers based on the type of DataType. + * + * @param dataType the DataType entity to convert + * @return an EntityModel containing the DataType and links + */ + @Override + public EntityModel toModel(DataType dataType) { + if (dataType instanceof AtomicDataType) { + return EntityModel.of(dataType, atomicDataTypeModelAssembler.toModel((AtomicDataType) dataType).getLinks()); + } else if (dataType instanceof TypeProfile) { + return EntityModel.of(dataType, typeProfileModelAssembler.toModel((TypeProfile) dataType).getLinks()); + } else { + // Generic DataType handling + EntityModel entityModel = toModelWithoutLinks(dataType); + + // Add self link based on the type + if (dataType instanceof AtomicDataType) { + entityModel.add(linkTo(methodOn(AtomicDataTypeController.class).getAtomicDataType(dataType.getPid())).withSelfRel()); + } else if (dataType instanceof TypeProfile) { + entityModel.add(linkTo(methodOn(TypeProfileController.class).getTypeProfile(dataType.getPid())).withSelfRel()); + } + + return entityModel; + } + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/web/hateoas/EntityModelAssembler.java b/src/main/java/edu/kit/datamanager/idoris/web/hateoas/EntityModelAssembler.java new file mode 100644 index 0000000..5842a8b --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/web/hateoas/EntityModelAssembler.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.web.hateoas; + +import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.server.RepresentationModelAssembler; + +/** + * Base interface for entity model assemblers. + * This interface defines the contract for assemblers that convert entities to EntityModel objects with HATEOAS links. + * + * @param the entity type, must extend GenericIDORISEntity + */ +public interface EntityModelAssembler extends RepresentationModelAssembler> { + + /** + * Converts an entity to an EntityModel with HATEOAS links. + * This method is implemented by the RepresentationModelAssembler interface. + * + * @param entity the entity to convert + * @return an EntityModel containing the entity and links + */ + @Override + EntityModel toModel(T entity); + + /** + * Creates an EntityModel for the given entity without adding links. + * This method can be used as a base for the toModel method. + * + * @param entity the entity to convert + * @return an EntityModel containing the entity without links + */ + default EntityModel toModelWithoutLinks(T entity) { + return EntityModel.of(entity); + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/web/hateoas/OperationModelAssembler.java b/src/main/java/edu/kit/datamanager/idoris/web/hateoas/OperationModelAssembler.java new file mode 100644 index 0000000..3c48f00 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/web/hateoas/OperationModelAssembler.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.web.hateoas; + +import edu.kit.datamanager.idoris.domain.entities.Operation; +import edu.kit.datamanager.idoris.web.v1.OperationController; +import org.springframework.hateoas.EntityModel; +import org.springframework.stereotype.Component; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +/** + * Assembler for converting Operation entities to EntityModel objects with HATEOAS links. + */ +@Component +public class OperationModelAssembler implements EntityModelAssembler { + + /** + * Converts an Operation entity to an EntityModel with HATEOAS links. + * + * @param operation the Operation entity to convert + * @return an EntityModel containing the Operation and links + */ + @Override + public EntityModel toModel(Operation operation) { + EntityModel entityModel = toModelWithoutLinks(operation); + + // Add self link + entityModel.add(linkTo(methodOn(OperationController.class).getOperation(operation.getPid())).withSelfRel()); + + // Add link to all operations + entityModel.add(linkTo(methodOn(OperationController.class).getAllOperations()).withRel("operations")); + + // Add link to executable on data type + if (operation.getExecutableOn() != null && operation.getExecutableOn().getDataType() != null) { + entityModel.add(linkTo(methodOn(OperationController.class).getOperation(operation.getExecutableOn().getDataType().getPid())).withRel("executableOn")); + } + + return entityModel; + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/web/hateoas/TechnologyInterfaceModelAssembler.java b/src/main/java/edu/kit/datamanager/idoris/web/hateoas/TechnologyInterfaceModelAssembler.java new file mode 100644 index 0000000..1b964ec --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/web/hateoas/TechnologyInterfaceModelAssembler.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.web.hateoas; + +import edu.kit.datamanager.idoris.domain.entities.TechnologyInterface; +import edu.kit.datamanager.idoris.web.v1.TechnologyInterfaceController; +import org.springframework.hateoas.EntityModel; +import org.springframework.stereotype.Component; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +/** + * Assembler for converting TechnologyInterface entities to EntityModel objects with HATEOAS links. + */ +@Component +public class TechnologyInterfaceModelAssembler implements EntityModelAssembler { + + /** + * Converts a TechnologyInterface entity to an EntityModel with HATEOAS links. + * + * @param technologyInterface the TechnologyInterface entity to convert + * @return an EntityModel containing the TechnologyInterface and links + */ + @Override + public EntityModel toModel(TechnologyInterface technologyInterface) { + EntityModel entityModel = toModelWithoutLinks(technologyInterface); + + // Add self link + entityModel.add(linkTo(methodOn(TechnologyInterfaceController.class).getTechnologyInterface(technologyInterface.getPid())).withSelfRel()); + + // Add link to attributes + entityModel.add(linkTo(methodOn(TechnologyInterfaceController.class).getAttributes(technologyInterface.getPid())).withRel("attributes")); + + // Add link to outputs + entityModel.add(linkTo(methodOn(TechnologyInterfaceController.class).getOutputs(technologyInterface.getPid())).withRel("outputs")); + + // Add link to all technology interfaces + entityModel.add(linkTo(methodOn(TechnologyInterfaceController.class).getAllTechnologyInterfaces()).withRel("technologyInterfaces")); + + return entityModel; + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/web/hateoas/TypeProfileModelAssembler.java b/src/main/java/edu/kit/datamanager/idoris/web/hateoas/TypeProfileModelAssembler.java new file mode 100644 index 0000000..90267d7 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/web/hateoas/TypeProfileModelAssembler.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.web.hateoas; + +import edu.kit.datamanager.idoris.domain.entities.TypeProfile; +import edu.kit.datamanager.idoris.web.v1.TypeProfileController; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder; +import org.springframework.stereotype.Component; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +/** + * Assembler for converting TypeProfile entities to EntityModel objects with HATEOAS links. + */ +@Component +public class TypeProfileModelAssembler implements EntityModelAssembler { + + /** + * Converts a TypeProfile entity to an EntityModel with HATEOAS links. + * + * @param typeProfile the TypeProfile entity to convert + * @return an EntityModel containing the TypeProfile and links + */ + @Override + public EntityModel toModel(TypeProfile typeProfile) { + EntityModel entityModel = toModelWithoutLinks(typeProfile); + + // Add self link + entityModel.add(linkTo(methodOn(TypeProfileController.class).getTypeProfile(typeProfile.getPid())).withSelfRel()); + + // Add link to validate + entityModel.add(linkTo(methodOn(TypeProfileController.class).validate(typeProfile.getPid())).withRel("validate")); + + // Add link to inherited attributes + entityModel.add(linkTo(methodOn(TypeProfileController.class).getInheritedAttributes(typeProfile.getPid())).withRel("inheritedAttributes")); + + // Add link to inheritance tree + entityModel.add(linkTo(methodOn(TypeProfileController.class).getInheritanceTree(typeProfile.getPid())).withRel("inheritanceTree")); + + // Add link to operations + WebMvcLinkBuilder operationsLinkBuilder = linkTo(methodOn(TypeProfileController.class).getOperationsForTypeProfile(typeProfile.getPid())); + entityModel.add(operationsLinkBuilder.withRel("operations")); + + // Add link to all type profiles + entityModel.add(linkTo(methodOn(TypeProfileController.class).getAllTypeProfiles()).withRel("typeProfiles")); + + return entityModel; + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/web/package-info.java b/src/main/java/edu/kit/datamanager/idoris/web/package-info.java new file mode 100644 index 0000000..05f8e2d --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/web/package-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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. + */ + +/** + * Web module for IDORIS. + * This module is responsible for the web API and controllers. + * It provides REST endpoints for accessing and manipulating entities. + * + *

The web module depends on the core module for base abstractions, the domain module for entity definitions, + * and the domain.services package for business logic. It should not depend on repository or other infrastructure concerns directly.

+ */ +@org.springframework.modulith.ApplicationModule( + displayName = "IDORIS Web", + allowedDependencies = {"core", "domain", "domain.services"} +) +package edu.kit.datamanager.idoris.web; \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/web/v1/AtomicDataTypeController.java b/src/main/java/edu/kit/datamanager/idoris/web/v1/AtomicDataTypeController.java new file mode 100644 index 0000000..d165159 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/web/v1/AtomicDataTypeController.java @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.web.v1; + +import edu.kit.datamanager.idoris.configuration.ApplicationProperties; +import edu.kit.datamanager.idoris.domain.entities.AtomicDataType; +import edu.kit.datamanager.idoris.domain.entities.Operation; +import edu.kit.datamanager.idoris.domain.services.AtomicDataTypeService; +import edu.kit.datamanager.idoris.domain.services.OperationService; +import edu.kit.datamanager.idoris.rules.logic.RuleService; +import edu.kit.datamanager.idoris.rules.logic.RuleTask; +import edu.kit.datamanager.idoris.rules.validation.ValidationResult; +import edu.kit.datamanager.idoris.web.ValidationException; +import edu.kit.datamanager.idoris.web.hateoas.AtomicDataTypeModelAssembler; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.EntityModel; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +/** + * REST controller for AtomicDataType entities. + * This controller provides endpoints for managing AtomicDataType entities. + */ +@RestController +@RequestMapping("/v1/api/atomicDataTypes") +@Tag(name = "AtomicDataType", description = "API for managing AtomicDataTypes") +@Slf4j +public class AtomicDataTypeController { + + private final AtomicDataTypeService atomicDataTypeService; + private final OperationService operationService; + private final AtomicDataTypeModelAssembler atomicDataTypeModelAssembler; + private final RuleService ruleService; + private final ApplicationProperties applicationProperties; + + public AtomicDataTypeController(AtomicDataTypeService atomicDataTypeService, OperationService operationService, AtomicDataTypeModelAssembler atomicDataTypeModelAssembler, RuleService ruleService, ApplicationProperties applicationProperties) { + this.atomicDataTypeService = atomicDataTypeService; + this.operationService = operationService; + this.atomicDataTypeModelAssembler = atomicDataTypeModelAssembler; + this.ruleService = ruleService; + this.applicationProperties = applicationProperties; + } + + /** + * Gets all AtomicDataType entities. + * + * @return a collection of all AtomicDataType entities + */ + @GetMapping + @io.swagger.v3.oas.annotations.Operation( + summary = "Get all AtomicDataTypes", + description = "Returns a collection of all AtomicDataType entities", + responses = { + @ApiResponse(responseCode = "200", description = "AtomicDataTypes found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = AtomicDataType.class))) + } + ) + public ResponseEntity>> getAllAtomicDataTypes() { + List> atomicDataTypes = StreamSupport.stream(atomicDataTypeService.getAllAtomicDataTypes().spliterator(), false) + .map(atomicDataTypeModelAssembler::toModel) + .collect(Collectors.toList()); + + CollectionModel> collectionModel = CollectionModel.of( + atomicDataTypes, + linkTo(methodOn(AtomicDataTypeController.class).getAllAtomicDataTypes()).withSelfRel() + ); + + return ResponseEntity.ok(collectionModel); + } + + /** + * Gets an AtomicDataType entity by its PID. + * + * @param pid the PID of the AtomicDataType to retrieve + * @return the AtomicDataType entity + */ + @GetMapping("/{pid}") + @io.swagger.v3.oas.annotations.Operation( + summary = "Get an AtomicDataType by PID", + description = "Returns an AtomicDataType entity by its PID", + responses = { + @ApiResponse(responseCode = "200", description = "AtomicDataType found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = AtomicDataType.class))), + @ApiResponse(responseCode = "404", description = "AtomicDataType not found") + } + ) + public ResponseEntity> getAtomicDataType( + @Parameter(description = "PID of the AtomicDataType", required = true) + @PathVariable String pid) { + return atomicDataTypeService.getAtomicDataType(pid) + .map(atomicDataTypeModelAssembler::toModel) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + /** + * Creates a new AtomicDataType entity. + * + * @param atomicDataType the AtomicDataType entity to create + * @return the created AtomicDataType entity + */ + @PostMapping + @io.swagger.v3.oas.annotations.Operation( + summary = "Create a new AtomicDataType", + description = "Creates a new AtomicDataType entity", + responses = { + @ApiResponse(responseCode = "201", description = "AtomicDataType created", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = AtomicDataType.class))), + @ApiResponse(responseCode = "400", description = "Invalid input") + } + ) + public ResponseEntity> createAtomicDataType( + @Parameter(description = "AtomicDataType to create", required = true) + @RequestBody AtomicDataType atomicDataType) { + + // Validate BEFORE saving + ValidationResult validationResult = ruleService.executeRules( + RuleTask.VALIDATE, + atomicDataType, + ValidationResult::new + ); + log.debug("Validation result for AtomicDataType {}: {}", atomicDataType, validationResult); + + // Check if validation failed based on your validation policy + if (hasValidationErrors(validationResult)) { + throw new ValidationException("Entity validation failed", validationResult); + } + + // Only save if validation passes + AtomicDataType saved = atomicDataTypeService.createAtomicDataType(atomicDataType); + return ResponseEntity.status(HttpStatus.CREATED).body(EntityModel.of(saved)); + + } + + /** + * Updates an existing AtomicDataType entity. + * + * @param pid the PID of the AtomicDataType to update + * @param atomicDataType the updated AtomicDataType entity + * @return the updated AtomicDataType entity + */ + @PutMapping("/{pid}") + @io.swagger.v3.oas.annotations.Operation( + summary = "Update an AtomicDataType", + description = "Updates an existing AtomicDataType entity", + responses = { + @ApiResponse(responseCode = "200", description = "AtomicDataType updated", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = AtomicDataType.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "404", description = "AtomicDataType not found") + } + ) + public ResponseEntity> updateAtomicDataType( + @Parameter(description = "PID of the AtomicDataType", required = true) + @PathVariable String pid, + @Parameter(description = "Updated AtomicDataType", required = true) + @Valid @RequestBody AtomicDataType atomicDataType) { + if (!atomicDataTypeService.getAtomicDataType(pid).isPresent()) { + return ResponseEntity.notFound().build(); + } + + atomicDataType.setPid(pid); + AtomicDataType updatedAtomicDataType = atomicDataTypeService.updateAtomicDataType(atomicDataType); + EntityModel entityModel = atomicDataTypeModelAssembler.toModel(updatedAtomicDataType); + return ResponseEntity.ok(entityModel); + } + + /** + * Deletes an AtomicDataType entity. + * + * @param pid the PID of the AtomicDataType to delete + * @return no content + */ + @DeleteMapping("/{pid}") + @io.swagger.v3.oas.annotations.Operation( + summary = "Delete an AtomicDataType", + description = "Deletes an AtomicDataType entity", + responses = { + @ApiResponse(responseCode = "204", description = "AtomicDataType deleted"), + @ApiResponse(responseCode = "404", description = "AtomicDataType not found") + } + ) + public ResponseEntity deleteAtomicDataType( + @Parameter(description = "PID of the AtomicDataType", required = true) + @PathVariable String pid) { + if (!atomicDataTypeService.getAtomicDataType(pid).isPresent()) { + return ResponseEntity.notFound().build(); + } + + atomicDataTypeService.deleteAtomicDataType(pid); + return ResponseEntity.noContent().build(); + } + + /** + * Gets operations for an AtomicDataType. + * + * @param pid the PID of the AtomicDataType + * @return a collection of operations for the AtomicDataType + */ + @GetMapping("/{pid}/operations") + @io.swagger.v3.oas.annotations.Operation( + summary = "Get operations for an AtomicDataType", + description = "Returns a collection of operations that can be executed on an AtomicDataType", + responses = { + @ApiResponse(responseCode = "200", description = "Operations found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = Operation.class))), + @ApiResponse(responseCode = "404", description = "AtomicDataType not found") + } + ) + public ResponseEntity>> getOperationsForAtomicDataType( + @Parameter(description = "PID of the AtomicDataType", required = true) + @PathVariable String pid) { + if (!atomicDataTypeService.getAtomicDataType(pid).isPresent()) { + return ResponseEntity.notFound().build(); + } + + List> operations = StreamSupport.stream(operationService.getOperationsForDataType(pid).spliterator(), false) + .map(operation -> EntityModel.of(operation, + linkTo(methodOn(AtomicDataTypeController.class).getOperationsForAtomicDataType(pid)).withSelfRel(), + linkTo(methodOn(AtomicDataTypeController.class).getAtomicDataType(pid)).withRel("atomicDataType"))) + .collect(Collectors.toList()); + + CollectionModel> collectionModel = CollectionModel.of( + operations, + linkTo(methodOn(AtomicDataTypeController.class).getOperationsForAtomicDataType(pid)).withSelfRel(), + linkTo(methodOn(AtomicDataTypeController.class).getAtomicDataType(pid)).withRel("atomicDataType") + ); + + return ResponseEntity.ok(collectionModel); + } + + private boolean hasValidationErrors(ValidationResult validationResult) { + return validationResult.getOutputMessages() + .entrySet() + .stream() + .anyMatch(entry -> entry.getKey().isHigherOrEqualTo(applicationProperties.getValidationLevel()) + && !entry.getValue().isEmpty()); + } + +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/web/v1/AttributeController.java b/src/main/java/edu/kit/datamanager/idoris/web/v1/AttributeController.java new file mode 100644 index 0000000..44cbd68 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/web/v1/AttributeController.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.web.v1; + +import edu.kit.datamanager.idoris.domain.entities.Attribute; +import edu.kit.datamanager.idoris.domain.entities.DataType; +import edu.kit.datamanager.idoris.domain.services.AttributeService; +import edu.kit.datamanager.idoris.web.api.IAttributeApi; +import edu.kit.datamanager.idoris.web.hateoas.AttributeModelAssembler; +import edu.kit.datamanager.idoris.web.hateoas.DataTypeModelAssembler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.EntityModel; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +/** + * REST controller for Attribute entities. + * This controller provides endpoints for managing Attribute entities. + */ +@RestController +@RequestMapping("/api/attributes") +public class AttributeController implements IAttributeApi { + + @Autowired + private AttributeService attributeService; + + @Autowired + private AttributeModelAssembler attributeModelAssembler; + + @Autowired + private DataTypeModelAssembler dataTypeModelAssembler; + + /** + * {@inheritDoc} + */ + @Override + public ResponseEntity>> getAllAttributes() { + List> attributes = StreamSupport.stream(attributeService.getAllAttributes().spliterator(), false) + .map(attributeModelAssembler::toModel) + .collect(Collectors.toList()); + + CollectionModel> collectionModel = CollectionModel.of( + attributes, + linkTo(methodOn(AttributeController.class).getAllAttributes()).withSelfRel() + ); + + return ResponseEntity.ok(collectionModel); + } + + /** + * {@inheritDoc} + */ + @Override + public ResponseEntity> getAttribute(String pid) { + return attributeService.getAttribute(pid) + .map(attributeModelAssembler::toModel) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + /** + * {@inheritDoc} + */ + @Override + public ResponseEntity> getDataType(String pid) { + return attributeService.getAttribute(pid) + .map(attribute -> attribute.getDataType()) + .map(dataTypeModelAssembler::toModel) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + /** + * {@inheritDoc} + */ + @Override + public ResponseEntity> createAttribute(Attribute attribute) { + Attribute createdAttribute = attributeService.createAttribute(attribute); + EntityModel entityModel = attributeModelAssembler.toModel(createdAttribute); + return ResponseEntity.status(HttpStatus.CREATED).body(entityModel); + } + + /** + * {@inheritDoc} + */ + @Override + public ResponseEntity> updateAttribute(String pid, Attribute attribute) { + if (!attributeService.getAttribute(pid).isPresent()) { + return ResponseEntity.notFound().build(); + } + + attribute.setPid(pid); + Attribute updatedAttribute = attributeService.updateAttribute(attribute); + EntityModel entityModel = attributeModelAssembler.toModel(updatedAttribute); + return ResponseEntity.ok(entityModel); + } + + /** + * {@inheritDoc} + */ + @Override + public ResponseEntity deleteAttribute(String pid) { + if (!attributeService.getAttribute(pid).isPresent()) { + return ResponseEntity.notFound().build(); + } + + attributeService.deleteAttribute(pid); + return ResponseEntity.noContent().build(); + } + + /** + * {@inheritDoc} + */ + @Override + public ResponseEntity deleteOrphanedAttributes() { + attributeService.deleteOrphanedAttributes(); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/web/v1/OperationController.java b/src/main/java/edu/kit/datamanager/idoris/web/v1/OperationController.java index db89b00..01bf3c5 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/v1/OperationController.java +++ b/src/main/java/edu/kit/datamanager/idoris/web/v1/OperationController.java @@ -16,34 +16,247 @@ package edu.kit.datamanager.idoris.web.v1; -import edu.kit.datamanager.idoris.dao.IDataTypeDao; -import edu.kit.datamanager.idoris.dao.IOperationDao; import edu.kit.datamanager.idoris.domain.entities.Operation; +import edu.kit.datamanager.idoris.domain.services.OperationService; import edu.kit.datamanager.idoris.rules.validation.ValidationPolicyValidator; import edu.kit.datamanager.idoris.rules.validation.ValidationResult; +import edu.kit.datamanager.idoris.web.hateoas.OperationModelAssembler; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.rest.webmvc.RepositoryRestController; +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.EntityModel; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.*; -@RepositoryRestController +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +/** + * REST controller for Operation entities. + * This controller provides endpoints for managing Operation entities. + */ +@RestController +@RequestMapping("/api/operations") +@Tag(name = "Operation", description = "API for managing Operations") public class OperationController { + @Autowired - IOperationDao operationDao; + private OperationService operationService; @Autowired - IDataTypeDao dataTypeDao; + private OperationModelAssembler operationModelAssembler; + + /** + * Gets all Operation entities. + * + * @return a collection of all Operation entities + */ + @GetMapping + @io.swagger.v3.oas.annotations.Operation( + summary = "Get all Operations", + description = "Returns a collection of all Operation entities", + responses = { + @ApiResponse(responseCode = "200", description = "Operations found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = Operation.class))) + } + ) + public ResponseEntity>> getAllOperations() { + List> operations = StreamSupport.stream(operationService.getAllOperations().spliterator(), false) + .map(operationModelAssembler::toModel) + .collect(Collectors.toList()); + + CollectionModel> collectionModel = CollectionModel.of( + operations, + linkTo(methodOn(OperationController.class).getAllOperations()).withSelfRel() + ); + + return ResponseEntity.ok(collectionModel); + } + + /** + * Gets an Operation entity by its PID. + * + * @param pid the PID of the Operation to retrieve + * @return the Operation entity + */ + @GetMapping("/{pid}") + @io.swagger.v3.oas.annotations.Operation( + summary = "Get an Operation by PID", + description = "Returns an Operation entity by its PID", + responses = { + @ApiResponse(responseCode = "200", description = "Operation found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = Operation.class))), + @ApiResponse(responseCode = "404", description = "Operation not found") + } + ) + public ResponseEntity> getOperation( + @Parameter(description = "PID of the Operation", required = true) + @PathVariable String pid) { + return operationService.getOperation(pid) + .map(operationModelAssembler::toModel) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + /** + * Creates a new Operation entity. + * + * @param operation the Operation entity to create + * @return the created Operation entity + */ + @PostMapping + @io.swagger.v3.oas.annotations.Operation( + summary = "Create a new Operation", + description = "Creates a new Operation entity", + responses = { + @ApiResponse(responseCode = "201", description = "Operation created", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = Operation.class))), + @ApiResponse(responseCode = "400", description = "Invalid input") + } + ) + public ResponseEntity> createOperation( + @Parameter(description = "Operation to create", required = true) + @Valid @RequestBody Operation operation) { + Operation createdOperation = operationService.createOperation(operation); + EntityModel entityModel = operationModelAssembler.toModel(createdOperation); + return ResponseEntity.status(HttpStatus.CREATED).body(entityModel); + } + + /** + * Updates an existing Operation entity. + * + * @param pid the PID of the Operation to update + * @param operation the updated Operation entity + * @return the updated Operation entity + */ + @PutMapping("/{pid}") + @io.swagger.v3.oas.annotations.Operation( + summary = "Update an Operation", + description = "Updates an existing Operation entity", + responses = { + @ApiResponse(responseCode = "200", description = "Operation updated", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = Operation.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "404", description = "Operation not found") + } + ) + public ResponseEntity> updateOperation( + @Parameter(description = "PID of the Operation", required = true) + @PathVariable String pid, + @Parameter(description = "Updated Operation", required = true) + @Valid @RequestBody Operation operation) { + if (!operationService.getOperation(pid).isPresent()) { + return ResponseEntity.notFound().build(); + } - @GetMapping("v1/operations/{pid}/validate") - public ResponseEntity validate(@PathVariable("pid") String pid) { - Operation operation = operationDao.findById(pid).orElseThrow(); + operation.setPid(pid); + Operation updatedOperation = operationService.updateOperation(operation); + EntityModel entityModel = operationModelAssembler.toModel(updatedOperation); + return ResponseEntity.ok(entityModel); + } + + /** + * Deletes an Operation entity. + * + * @param pid the PID of the Operation to delete + * @return no content + */ + @DeleteMapping("/{pid}") + @io.swagger.v3.oas.annotations.Operation( + summary = "Delete an Operation", + description = "Deletes an Operation entity", + responses = { + @ApiResponse(responseCode = "204", description = "Operation deleted"), + @ApiResponse(responseCode = "404", description = "Operation not found") + } + ) + public ResponseEntity deleteOperation( + @Parameter(description = "PID of the Operation", required = true) + @PathVariable String pid) { + if (!operationService.getOperation(pid).isPresent()) { + return ResponseEntity.notFound().build(); + } + + operationService.deleteOperation(pid); + return ResponseEntity.noContent().build(); + } + + /** + * Validates an Operation entity. + * + * @param pid the PID of the Operation to validate + * @return the validation result + */ + @GetMapping("/{pid}/validate") + @io.swagger.v3.oas.annotations.Operation( + summary = "Validate an Operation", + description = "Validates an Operation entity and returns the validation result", + responses = { + @ApiResponse(responseCode = "200", description = "Operation is valid"), + @ApiResponse(responseCode = "218", description = "Operation is invalid"), + @ApiResponse(responseCode = "404", description = "Operation not found") + } + ) + public ResponseEntity validate( + @Parameter(description = "PID of the Operation", required = true) + @PathVariable String pid) { + if (!operationService.getOperation(pid).isPresent()) { + return ResponseEntity.notFound().build(); + } + + Operation operation = operationService.getOperation(pid).get(); ValidationPolicyValidator validator = new ValidationPolicyValidator(); ValidationResult result = operation.execute(validator); + if (result.isValid()) { return ResponseEntity.ok(result); } else { return ResponseEntity.status(218).body(result); } } + + /** + * Gets operations for a data type. + * + * @param pid the PID of the data type + * @return a collection of operations for the data type + */ + @GetMapping("/search/getOperationsForDataType") + @io.swagger.v3.oas.annotations.Operation( + summary = "Get operations for a data type", + description = "Returns a collection of operations that can be executed on a data type", + responses = { + @ApiResponse(responseCode = "200", description = "Operations found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = Operation.class))) + } + ) + public ResponseEntity>> getOperationsForDataType( + @Parameter(description = "PID of the data type", required = true) + @RequestParam String pid) { + List> operations = StreamSupport.stream(operationService.getOperationsForDataType(pid).spliterator(), false) + .map(operationModelAssembler::toModel) + .collect(Collectors.toList()); + + CollectionModel> collectionModel = CollectionModel.of( + operations, + linkTo(methodOn(OperationController.class).getOperationsForDataType(pid)).withSelfRel() + ); + + return ResponseEntity.ok(collectionModel); + } } diff --git a/src/main/java/edu/kit/datamanager/idoris/web/v1/PidRedirectController.java b/src/main/java/edu/kit/datamanager/idoris/web/v1/PidRedirectController.java index 3c894c1..c35c8cb 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/v1/PidRedirectController.java +++ b/src/main/java/edu/kit/datamanager/idoris/web/v1/PidRedirectController.java @@ -31,7 +31,7 @@ import java.util.*; @Controller -@RequestMapping("/") +@RequestMapping("/pid") @Log public class PidRedirectController { diff --git a/src/main/java/edu/kit/datamanager/idoris/web/v1/TechnologyInterfaceController.java b/src/main/java/edu/kit/datamanager/idoris/web/v1/TechnologyInterfaceController.java new file mode 100644 index 0000000..32b81eb --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/web/v1/TechnologyInterfaceController.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.web.v1; + +import edu.kit.datamanager.idoris.domain.entities.Attribute; +import edu.kit.datamanager.idoris.domain.entities.TechnologyInterface; +import edu.kit.datamanager.idoris.domain.services.TechnologyInterfaceService; +import edu.kit.datamanager.idoris.web.api.ITechnologyInterfaceApi; +import edu.kit.datamanager.idoris.web.hateoas.AttributeModelAssembler; +import edu.kit.datamanager.idoris.web.hateoas.TechnologyInterfaceModelAssembler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.EntityModel; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +/** + * REST controller for TechnologyInterface entities. + * This controller provides endpoints for managing TechnologyInterface entities. + */ +@RestController +@RequestMapping("/api/technologyInterfaces") +public class TechnologyInterfaceController implements ITechnologyInterfaceApi { + + @Autowired + private TechnologyInterfaceService technologyInterfaceService; + + @Autowired + private TechnologyInterfaceModelAssembler technologyInterfaceModelAssembler; + + @Autowired + private AttributeModelAssembler attributeModelAssembler; + + /** + * {@inheritDoc} + */ + @Override + public ResponseEntity>> getAllTechnologyInterfaces() { + List> technologyInterfaces = StreamSupport.stream(technologyInterfaceService.getAllTechnologyInterfaces().spliterator(), false) + .map(technologyInterfaceModelAssembler::toModel) + .collect(Collectors.toList()); + + CollectionModel> collectionModel = CollectionModel.of( + technologyInterfaces, + linkTo(methodOn(TechnologyInterfaceController.class).getAllTechnologyInterfaces()).withSelfRel() + ); + + return ResponseEntity.ok(collectionModel); + } + + /** + * {@inheritDoc} + */ + @Override + public ResponseEntity> getTechnologyInterface(String pid) { + return technologyInterfaceService.getTechnologyInterface(pid) + .map(technologyInterfaceModelAssembler::toModel) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + /** + * {@inheritDoc} + */ + @Override + public ResponseEntity>> getAttributes(String pid) { + return technologyInterfaceService.getTechnologyInterface(pid) + .map(technologyInterface -> { + List> attributes = StreamSupport.stream(technologyInterface.getAttributes().spliterator(), false) + .map(attributeModelAssembler::toModel) + .collect(Collectors.toList()); + + CollectionModel> collectionModel = CollectionModel.of( + attributes, + linkTo(methodOn(TechnologyInterfaceController.class).getAttributes(pid)).withSelfRel(), + linkTo(methodOn(TechnologyInterfaceController.class).getTechnologyInterface(pid)).withRel("technologyInterface") + ); + + return ResponseEntity.ok(collectionModel); + }) + .orElse(ResponseEntity.notFound().build()); + } + + /** + * {@inheritDoc} + */ + @Override + public ResponseEntity>> getOutputs(String pid) { + return technologyInterfaceService.getTechnologyInterface(pid) + .map(technologyInterface -> { + List> outputs = StreamSupport.stream(technologyInterface.getOutputs().spliterator(), false) + .map(attributeModelAssembler::toModel) + .collect(Collectors.toList()); + + CollectionModel> collectionModel = CollectionModel.of( + outputs, + linkTo(methodOn(TechnologyInterfaceController.class).getOutputs(pid)).withSelfRel(), + linkTo(methodOn(TechnologyInterfaceController.class).getTechnologyInterface(pid)).withRel("technologyInterface") + ); + + return ResponseEntity.ok(collectionModel); + }) + .orElse(ResponseEntity.notFound().build()); + } + + /** + * {@inheritDoc} + */ + @Override + public ResponseEntity> createTechnologyInterface(TechnologyInterface technologyInterface) { + TechnologyInterface createdTechnologyInterface = technologyInterfaceService.createTechnologyInterface(technologyInterface); + EntityModel entityModel = technologyInterfaceModelAssembler.toModel(createdTechnologyInterface); + return ResponseEntity.status(HttpStatus.CREATED).body(entityModel); + } + + /** + * {@inheritDoc} + */ + @Override + public ResponseEntity> updateTechnologyInterface(String pid, TechnologyInterface technologyInterface) { + if (!technologyInterfaceService.getTechnologyInterface(pid).isPresent()) { + return ResponseEntity.notFound().build(); + } + + technologyInterface.setPid(pid); + TechnologyInterface updatedTechnologyInterface = technologyInterfaceService.updateTechnologyInterface(technologyInterface); + EntityModel entityModel = technologyInterfaceModelAssembler.toModel(updatedTechnologyInterface); + return ResponseEntity.ok(entityModel); + } + + /** + * {@inheritDoc} + */ + @Override + public ResponseEntity deleteTechnologyInterface(String pid) { + if (!technologyInterfaceService.getTechnologyInterface(pid).isPresent()) { + return ResponseEntity.notFound().build(); + } + + technologyInterfaceService.deleteTechnologyInterface(pid); + return ResponseEntity.noContent().build(); + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/web/v1/TypeProfileController.java b/src/main/java/edu/kit/datamanager/idoris/web/v1/TypeProfileController.java index 5d2173b..0bc5372 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/v1/TypeProfileController.java +++ b/src/main/java/edu/kit/datamanager/idoris/web/v1/TypeProfileController.java @@ -16,43 +16,165 @@ package edu.kit.datamanager.idoris.web.v1; -import edu.kit.datamanager.idoris.dao.IDataTypeDao; -import edu.kit.datamanager.idoris.dao.IOperationDao; -import edu.kit.datamanager.idoris.dao.ITypeProfileDao; import edu.kit.datamanager.idoris.domain.entities.Attribute; +import edu.kit.datamanager.idoris.domain.entities.Operation; import edu.kit.datamanager.idoris.domain.entities.TypeProfile; -import edu.kit.datamanager.idoris.rules.validation.ValidationPolicyValidator; +import edu.kit.datamanager.idoris.domain.services.OperationService; +import edu.kit.datamanager.idoris.domain.services.TypeProfileService; import edu.kit.datamanager.idoris.rules.validation.ValidationResult; +import edu.kit.datamanager.idoris.web.hateoas.TypeProfileModelAssembler; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.rest.webmvc.RepositoryRestController; import org.springframework.hateoas.CollectionModel; import org.springframework.hateoas.EntityModel; -import org.springframework.hateoas.Link; -import org.springframework.http.HttpEntity; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.*; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; -@RepositoryRestController +/** + * REST controller for TypeProfile entities. + * This controller provides endpoints for managing TypeProfile entities. + */ +@RestController +@RequestMapping("/api/typeProfiles") +@Tag(name = "TypeProfile", description = "API for managing TypeProfiles") public class TypeProfileController { @Autowired - ITypeProfileDao typeProfileDao; + private TypeProfileService typeProfileService; + + @Autowired + private OperationService operationService; @Autowired - IDataTypeDao dataTypeDao; + private TypeProfileModelAssembler typeProfileModelAssembler; + + /** + * Gets all TypeProfile entities. + * + * @return a collection of all TypeProfile entities + */ + @GetMapping + @io.swagger.v3.oas.annotations.Operation( + summary = "Get all TypeProfiles", + description = "Returns a collection of all TypeProfile entities", + responses = { + @ApiResponse(responseCode = "200", description = "TypeProfiles found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = TypeProfile.class))) + } + ) + public ResponseEntity>> getAllTypeProfiles() { + List> typeProfiles = StreamSupport.stream(typeProfileService.getAllTypeProfiles().spliterator(), false) + .map(typeProfileModelAssembler::toModel) + .collect(Collectors.toList()); + + CollectionModel> collectionModel = CollectionModel.of( + typeProfiles, + linkTo(methodOn(TypeProfileController.class).getAllTypeProfiles()).withSelfRel() + ); + + return ResponseEntity.ok(collectionModel); + } + + /** + * Gets a TypeProfile entity by its PID. + * + * @param pid the PID of the TypeProfile to retrieve + * @return the TypeProfile entity + */ + @GetMapping("/{pid}") + @io.swagger.v3.oas.annotations.Operation( + summary = "Get a TypeProfile by PID", + description = "Returns a TypeProfile entity by its PID", + responses = { + @ApiResponse(responseCode = "200", description = "TypeProfile found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = TypeProfile.class))), + @ApiResponse(responseCode = "404", description = "TypeProfile not found") + } + ) + public ResponseEntity> getTypeProfile( + @Parameter(description = "PID of the TypeProfile", required = true) + @PathVariable String pid) { + return typeProfileService.getTypeProfile(pid) + .map(typeProfileModelAssembler::toModel) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + /** + * Gets operations for a TypeProfile. + * + * @param pid the PID of the TypeProfile + * @return a collection of operations for the TypeProfile + */ + @GetMapping("/{pid}/operations") + @io.swagger.v3.oas.annotations.Operation( + summary = "Get operations for a TypeProfile", + description = "Returns a collection of operations that can be executed on a TypeProfile", + responses = { + @ApiResponse(responseCode = "200", description = "Operations found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = Operation.class))), + @ApiResponse(responseCode = "404", description = "TypeProfile not found") + } + ) + public ResponseEntity>> getOperationsForTypeProfile( + @Parameter(description = "PID of the TypeProfile", required = true) + @PathVariable String pid) { + if (!typeProfileService.getTypeProfile(pid).isPresent()) { + return ResponseEntity.notFound().build(); + } + + List> operations = StreamSupport.stream(operationService.getOperationsForDataType(pid).spliterator(), false) + .map(operation -> EntityModel.of(operation, + linkTo(methodOn(TypeProfileController.class).getOperationsForTypeProfile(pid)).withSelfRel(), + linkTo(methodOn(TypeProfileController.class).getTypeProfile(pid)).withRel("typeProfile"))) + .collect(Collectors.toList()); + + CollectionModel> collectionModel = CollectionModel.of( + operations, + linkTo(methodOn(TypeProfileController.class).getOperationsForTypeProfile(pid)).withSelfRel(), + linkTo(methodOn(TypeProfileController.class).getTypeProfile(pid)).withRel("typeProfile") + ); + + return ResponseEntity.ok(collectionModel); + } - @GetMapping("typeProfiles/{pid}/validate") - public ResponseEntity validate(@PathVariable("pid") String pid) { - TypeProfile typeProfile = typeProfileDao.findById(pid).orElseThrow(); - ValidationPolicyValidator validator = new ValidationPolicyValidator(); - ValidationResult result = typeProfile.execute(validator); + /** + * Validates a TypeProfile entity. + * + * @param pid the PID of the TypeProfile to validate + * @return the validation result + */ + @GetMapping("/{pid}/validate") + @io.swagger.v3.oas.annotations.Operation( + summary = "Validate a TypeProfile", + description = "Validates a TypeProfile entity and returns the validation result", + responses = { + @ApiResponse(responseCode = "200", description = "TypeProfile is valid"), + @ApiResponse(responseCode = "218", description = "TypeProfile is invalid"), + @ApiResponse(responseCode = "404", description = "TypeProfile not found") + } + ) + public ResponseEntity validate( + @Parameter(description = "PID of the TypeProfile", required = true) + @PathVariable String pid) { + ValidationResult result = typeProfileService.validateTypeProfile(pid); if (result.isValid()) { return ResponseEntity.ok(result); } else { @@ -60,35 +182,165 @@ public ResponseEntity validate(@PathVariable("pid") String pid) { } } - @GetMapping("typeProfiles/{pid}/inheritedAttributes") - public ResponseEntity getInheritedAttributes(@PathVariable("pid") String pid) { - Iterable inheritanceChain = typeProfileDao.findAllTypeProfilesInInheritanceChain(pid); + /** + * Gets the inherited attributes of a TypeProfile. + * + * @param pid the PID of the TypeProfile + * @return a collection of inherited attributes + */ + @GetMapping("/{pid}/inheritedAttributes") + @io.swagger.v3.oas.annotations.Operation( + summary = "Get inherited attributes of a TypeProfile", + description = "Returns a collection of attributes inherited by a TypeProfile", + responses = { + @ApiResponse(responseCode = "200", description = "Inherited attributes found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = Attribute.class))), + @ApiResponse(responseCode = "404", description = "TypeProfile not found") + } + ) + public ResponseEntity>> getInheritedAttributes( + @Parameter(description = "PID of the TypeProfile", required = true) + @PathVariable String pid) { + Iterable inheritanceChain = typeProfileService.getInheritanceChain(pid); List> attributes = new ArrayList<>(); inheritanceChain.forEach(typeProfile -> { - typeProfileDao.findById(typeProfile.getPid()).orElseThrow().getAttributes().forEach(profileAttribute -> { + typeProfileService.getTypeProfile(typeProfile.getPid()).orElseThrow().getAttributes().forEach(profileAttribute -> { EntityModel attribute = EntityModel.of(profileAttribute); - attribute.add(linkTo(IDataTypeDao.class).slash("api").slash("dataTypes").slash(profileAttribute.getDataType().getPid()).withRel("dataType")); + attribute.add(linkTo(methodOn(TypeProfileController.class).getTypeProfile(profileAttribute.getDataType().getPid())).withRel("dataType")); attributes.add(attribute); }); }); + CollectionModel> resources = CollectionModel.of(attributes); - resources.add(linkTo(TypeProfileController.class).slash("api").slash("typeProfiles").slash(pid).slash("inheritedAttributes").withSelfRel()); - resources.add(linkTo(ITypeProfileDao.class).slash("api").slash("typeProfiles").slash(pid).withRel("typeProfile")); + resources.add(linkTo(methodOn(TypeProfileController.class).getInheritedAttributes(pid)).withSelfRel()); + resources.add(linkTo(methodOn(TypeProfileController.class).getTypeProfile(pid)).withRel("typeProfile")); + return ResponseEntity.ok(resources); } - @GetMapping("typeProfiles/{pid}/inheritanceTree") - public HttpEntity> getInheritanceTree(@NotNull @PathVariable("pid") String pid) { - EntityModel resources = buildInheritanceTree(typeProfileDao.findById(pid).orElseThrow()); - resources.add(linkTo(TypeProfileController.class).slash("api").slash("typeProfiles").slash(pid).slash("inheritanceTree").withSelfRel()); - return new ResponseEntity<>(resources, HttpStatus.OK); + /** + * Creates a new TypeProfile entity. + * + * @param typeProfile the TypeProfile entity to create + * @return the created TypeProfile entity + */ + @PostMapping + @io.swagger.v3.oas.annotations.Operation( + summary = "Create a new TypeProfile", + description = "Creates a new TypeProfile entity", + responses = { + @ApiResponse(responseCode = "201", description = "TypeProfile created", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = TypeProfile.class))), + @ApiResponse(responseCode = "400", description = "Invalid input") + } + ) + public ResponseEntity> createTypeProfile( + @Parameter(description = "TypeProfile to create", required = true) + @Valid @RequestBody TypeProfile typeProfile) { + TypeProfile createdTypeProfile = typeProfileService.createTypeProfile(typeProfile); + EntityModel entityModel = typeProfileModelAssembler.toModel(createdTypeProfile); + return ResponseEntity.status(HttpStatus.CREATED).body(entityModel); + } + + /** + * Updates an existing TypeProfile entity. + * + * @param pid the PID of the TypeProfile to update + * @param typeProfile the updated TypeProfile entity + * @return the updated TypeProfile entity + */ + @PutMapping("/{pid}") + @io.swagger.v3.oas.annotations.Operation( + summary = "Update a TypeProfile", + description = "Updates an existing TypeProfile entity", + responses = { + @ApiResponse(responseCode = "200", description = "TypeProfile updated", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = TypeProfile.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "404", description = "TypeProfile not found") + } + ) + public ResponseEntity> updateTypeProfile( + @Parameter(description = "PID of the TypeProfile", required = true) + @PathVariable String pid, + @Parameter(description = "Updated TypeProfile", required = true) + @Valid @RequestBody TypeProfile typeProfile) { + if (!typeProfileService.getTypeProfile(pid).isPresent()) { + return ResponseEntity.notFound().build(); + } + + typeProfile.setPid(pid); + TypeProfile updatedTypeProfile = typeProfileService.updateTypeProfile(typeProfile); + EntityModel entityModel = typeProfileModelAssembler.toModel(updatedTypeProfile); + return ResponseEntity.ok(entityModel); + } + + /** + * Deletes a TypeProfile entity. + * + * @param pid the PID of the TypeProfile to delete + * @return no content + */ + @DeleteMapping("/{pid}") + @io.swagger.v3.oas.annotations.Operation( + summary = "Delete a TypeProfile", + description = "Deletes a TypeProfile entity", + responses = { + @ApiResponse(responseCode = "204", description = "TypeProfile deleted"), + @ApiResponse(responseCode = "404", description = "TypeProfile not found") + } + ) + public ResponseEntity deleteTypeProfile( + @Parameter(description = "PID of the TypeProfile", required = true) + @PathVariable String pid) { + if (!typeProfileService.getTypeProfile(pid).isPresent()) { + return ResponseEntity.notFound().build(); + } + + typeProfileService.deleteTypeProfile(pid); + return ResponseEntity.noContent().build(); + } + + /** + * Gets the inheritance tree of a TypeProfile. + * + * @param pid the PID of the TypeProfile + * @return the inheritance tree + */ + @GetMapping("/{pid}/inheritanceTree") + @io.swagger.v3.oas.annotations.Operation( + summary = "Get inheritance tree of a TypeProfile", + description = "Returns the inheritance tree of a TypeProfile", + responses = { + @ApiResponse(responseCode = "200", description = "Inheritance tree found", + content = @Content(mediaType = "application/hal+json")), + @ApiResponse(responseCode = "404", description = "TypeProfile not found") + } + ) + public ResponseEntity> getInheritanceTree( + @Parameter(description = "PID of the TypeProfile", required = true) + @NotNull @PathVariable String pid) { + EntityModel resources = buildInheritanceTree(typeProfileService.getTypeProfile(pid).orElseThrow()); + resources.add(linkTo(methodOn(TypeProfileController.class).getInheritanceTree(pid)).withSelfRel()); + resources.add(linkTo(methodOn(TypeProfileController.class).getTypeProfile(pid)).withRel("typeProfile")); + + return ResponseEntity.ok(resources); } + /** + * Builds an inheritance tree for a TypeProfile. + * + * @param typeProfile the TypeProfile to build the inheritance tree for + * @return an EntityModel containing the inheritance tree + */ private EntityModel buildInheritanceTree(TypeProfile typeProfile) { List> attributes = new ArrayList<>(); typeProfile.getAttributes().forEach(profileAttribute -> { EntityModel attribute = EntityModel.of(profileAttribute); - attribute.add(linkTo(IDataTypeDao.class).slash("api").slash("dataTypes").slash(profileAttribute.getDataType().getPid()).withRel("dataType")); + attribute.add(linkTo(methodOn(TypeProfileController.class).getTypeProfile(profileAttribute.getDataType().getPid())).withRel("dataType")); attributes.add(attribute); }); @@ -104,38 +356,12 @@ private EntityModel buildInheritanceTree(TypeProfile typ CollectionModel.of(attributes), CollectionModel.of(inheritsFrom))); - node.add(linkTo(TypeProfileController.class) - .slash("api") - .slash("typeProfiles") - .slash(typeProfile.getPid()) - .slash("inheritanceTree") - .withRel("inheritanceTree")); - node.add(linkTo(ITypeProfileDao.class) - .slash("api") - .slash("typeProfiles") - .slash(typeProfile.getPid()) - .withRel("typeProfile")); - node.add(linkTo(TypeProfileController.class) - .slash("api") - .slash("typeProfiles") - .slash(typeProfile.getPid()) - .slash("attributes") - .withRel("attributes")); - node.add(linkTo(TypeProfileController.class) - .slash("api") - .slash("typeProfiles") - .slash(typeProfile.getPid()) - .slash("inheritedAttributes") - .withRel("inheritedAttributes")); - node.add(Link.of( - linkTo(IOperationDao.class) - .slash("api") - .slash("operations") - .slash("search") - .slash("getOperationsForDataType") - .toUri() + "?pid=" + typeProfile.getPid(), - "operations") - ); + // Add links + node.add(linkTo(methodOn(TypeProfileController.class).getInheritanceTree(typeProfile.getPid())).withRel("inheritanceTree")); + node.add(linkTo(methodOn(TypeProfileController.class).getTypeProfile(typeProfile.getPid())).withRel("typeProfile")); + node.add(linkTo(methodOn(TypeProfileController.class).getInheritedAttributes(typeProfile.getPid())).withRel("inheritedAttributes")); + node.add(linkTo(methodOn(TypeProfileController.class).getOperationsForTypeProfile(typeProfile.getPid())).withRel("operations")); + return node; } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e3ccd0f..e965707 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -20,7 +20,17 @@ logging.level.edu.kit.datamanager=DEBUG spring.neo4j.uri=bolt://localhost:7687 spring.neo4j.authentication.username=neo4j spring.neo4j.authentication.password=superSecret -spring.data.rest.basePath=/api +management.endpoints.access.default=unrestricted +management.endpoints.web.exposure.include=* +# Spring Doc Settings for OpenAPI documentation +springdoc.show-actuator=true +springdoc.api-docs.path=/v1/api-docs +springdoc.swagger-ui.path=/swagger-ui.html +springdoc.swagger-ui.enabled=true +#spring.data.rest.base-path= +spring.hateoas.use-hal-as-default-json-media-type=true +server.servlet.context-path=/api +# IDORIS Settings server.port=8095 idoris.validation-level=info idoris.validation-policy=strict diff --git a/src/main/resources/test-config/application.properties b/src/main/resources/test-config/application.properties index e3ccd0f..071ba03 100644 --- a/src/main/resources/test-config/application.properties +++ b/src/main/resources/test-config/application.properties @@ -20,7 +20,8 @@ logging.level.edu.kit.datamanager=DEBUG spring.neo4j.uri=bolt://localhost:7687 spring.neo4j.authentication.username=neo4j spring.neo4j.authentication.password=superSecret -spring.data.rest.basePath=/api +# Base path for all REST endpoints +server.servlet.context-path=/api server.port=8095 idoris.validation-level=info idoris.validation-policy=strict diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index e3ccd0f..071ba03 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -20,7 +20,8 @@ logging.level.edu.kit.datamanager=DEBUG spring.neo4j.uri=bolt://localhost:7687 spring.neo4j.authentication.username=neo4j spring.neo4j.authentication.password=superSecret -spring.data.rest.basePath=/api +# Base path for all REST endpoints +server.servlet.context-path=/api server.port=8095 idoris.validation-level=info idoris.validation-policy=strict diff --git a/src/test/resources/test-config/application.properties b/src/test/resources/test-config/application.properties index e3ccd0f..071ba03 100644 --- a/src/test/resources/test-config/application.properties +++ b/src/test/resources/test-config/application.properties @@ -20,7 +20,8 @@ logging.level.edu.kit.datamanager=DEBUG spring.neo4j.uri=bolt://localhost:7687 spring.neo4j.authentication.username=neo4j spring.neo4j.authentication.password=superSecret -spring.data.rest.basePath=/api +# Base path for all REST endpoints +server.servlet.context-path=/api server.port=8095 idoris.validation-level=info idoris.validation-policy=strict From dc600546dc3878f22f945df42bf90ee4f7bbb84b Mon Sep 17 00:00:00 2001 From: Maximilian Inckmann Date: Fri, 11 Jul 2025 23:53:37 +0200 Subject: [PATCH 02/19] improved web controller and services Signed-off-by: Maximilian Inckmann --- build.gradle | 1 - .../idoris/configuration/WebConfig.java | 6 +- .../datamanager/idoris/dao/IGenericRepo.java | 2 - .../idoris/repository/package-info.java | 29 --- .../services/AtomicDataTypeService.java | 2 +- .../services/AttributeMappingService.java | 2 +- .../services/AttributeService.java | 2 +- .../services/OperationService.java | 2 +- .../services/TechnologyInterfaceService.java | 2 +- .../services/TypeProfileService.java | 2 +- .../{domain => }/services/package-info.java | 2 +- .../idoris/web/api/IAtomicDataTypeApi.java | 163 +++++++++++++ .../idoris/web/api/IOperationApi.java | 182 ++++++++++++++ .../idoris/web/api/ITypeProfileApi.java | 228 ++++++++++++++++++ .../web/v1/AtomicDataTypeController.java | 72 +++--- .../idoris/web/v1/AttributeController.java | 4 +- .../idoris/web/v1/OperationController.java | 79 +++--- .../idoris/web/v1/PidRedirectController.java | 2 +- .../web/v1/TechnologyInterfaceController.java | 4 +- .../idoris/web/v1/TypeProfileController.java | 143 +++++++---- src/main/resources/application.properties | 1 - 21 files changed, 760 insertions(+), 170 deletions(-) delete mode 100644 src/main/java/edu/kit/datamanager/idoris/repository/package-info.java rename src/main/java/edu/kit/datamanager/idoris/{domain => }/services/AtomicDataTypeService.java (98%) rename src/main/java/edu/kit/datamanager/idoris/{domain => }/services/AttributeMappingService.java (99%) rename src/main/java/edu/kit/datamanager/idoris/{domain => }/services/AttributeService.java (98%) rename src/main/java/edu/kit/datamanager/idoris/{domain => }/services/OperationService.java (98%) rename src/main/java/edu/kit/datamanager/idoris/{domain => }/services/TechnologyInterfaceService.java (99%) rename src/main/java/edu/kit/datamanager/idoris/{domain => }/services/TypeProfileService.java (99%) rename src/main/java/edu/kit/datamanager/idoris/{domain => }/services/package-info.java (94%) create mode 100644 src/main/java/edu/kit/datamanager/idoris/web/api/IAtomicDataTypeApi.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/web/api/IOperationApi.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/web/api/ITypeProfileApi.java diff --git a/build.gradle b/build.gradle index 00fddcd..d41463a 100644 --- a/build.gradle +++ b/build.gradle @@ -81,7 +81,6 @@ dependencies { implementation "org.springframework.boot:spring-boot-starter-hateoas" implementation "org.springframework.boot:spring-boot-starter-validation" implementation "org.springframework:spring-web" - implementation 'org.springframework.data:spring-data-rest-hal-explorer' implementation "org.springframework.modulith:spring-modulith-starter-core" implementation "org.springframework.modulith:spring-modulith-starter-neo4j:${springModulithVersion}" implementation "org.springframework.modulith:spring-modulith-events-api:${springModulithVersion}" diff --git a/src/main/java/edu/kit/datamanager/idoris/configuration/WebConfig.java b/src/main/java/edu/kit/datamanager/idoris/configuration/WebConfig.java index 5a63713..a916a2e 100644 --- a/src/main/java/edu/kit/datamanager/idoris/configuration/WebConfig.java +++ b/src/main/java/edu/kit/datamanager/idoris/configuration/WebConfig.java @@ -29,7 +29,7 @@ */ @Configuration @EnableHypermediaSupport(type = {EnableHypermediaSupport.HypermediaType.HAL, EnableHypermediaSupport.HypermediaType.HAL_FORMS, EnableHypermediaSupport.HypermediaType.COLLECTION_JSON}) -public class WebConfig { +public class WebConfig implements WebMvcConfigurer { /** * Configures CORS settings. @@ -49,8 +49,8 @@ public void addCorsMappings(CorsRegistry registry) { }; } + @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("forward:/swagger-ui.html"); - registry.addViewController("/explorer").setViewName("forward:/explorer/index.html"); } -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/IGenericRepo.java b/src/main/java/edu/kit/datamanager/idoris/dao/IGenericRepo.java index 91a1318..e195d5b 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/IGenericRepo.java +++ b/src/main/java/edu/kit/datamanager/idoris/dao/IGenericRepo.java @@ -20,9 +20,7 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.repository.ListCrudRepository; import org.springframework.data.repository.PagingAndSortingRepository; -import org.springframework.data.rest.core.annotation.RepositoryRestResource; -@RepositoryRestResource(exported = false) public interface IGenericRepo extends Neo4jRepository, ListCrudRepository, PagingAndSortingRepository { // This interface serves as a marker for generic repositories. // It can be extended by specific repositories to inherit common methods. diff --git a/src/main/java/edu/kit/datamanager/idoris/repository/package-info.java b/src/main/java/edu/kit/datamanager/idoris/repository/package-info.java deleted file mode 100644 index d304608..0000000 --- a/src/main/java/edu/kit/datamanager/idoris/repository/package-info.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2025 Karlsruhe Institute of Technology - * - * 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. - */ - -/** - * Repository module for IDORIS. - * This module is responsible for data access and persistence. - * It provides repositories for accessing and manipulating entities in the database. - * - *

The repository module depends on the core module for base abstractions and the domain module for entity definitions. - * It should not depend on web or service concerns.

- */ -@org.springframework.modulith.ApplicationModule( - displayName = "IDORIS Repository", - allowedDependencies = {"core", "domain"} -) -package edu.kit.datamanager.idoris.repository; \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/services/AtomicDataTypeService.java b/src/main/java/edu/kit/datamanager/idoris/services/AtomicDataTypeService.java similarity index 98% rename from src/main/java/edu/kit/datamanager/idoris/domain/services/AtomicDataTypeService.java rename to src/main/java/edu/kit/datamanager/idoris/services/AtomicDataTypeService.java index aa0d0b4..ab9102c 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/services/AtomicDataTypeService.java +++ b/src/main/java/edu/kit/datamanager/idoris/services/AtomicDataTypeService.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.services; +package edu.kit.datamanager.idoris.services; import edu.kit.datamanager.idoris.core.events.EventPublisherService; import edu.kit.datamanager.idoris.dao.IAtomicDataTypeDao; diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/services/AttributeMappingService.java b/src/main/java/edu/kit/datamanager/idoris/services/AttributeMappingService.java similarity index 99% rename from src/main/java/edu/kit/datamanager/idoris/domain/services/AttributeMappingService.java rename to src/main/java/edu/kit/datamanager/idoris/services/AttributeMappingService.java index c096f9b..cef8089 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/services/AttributeMappingService.java +++ b/src/main/java/edu/kit/datamanager/idoris/services/AttributeMappingService.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.services; +package edu.kit.datamanager.idoris.services; import edu.kit.datamanager.idoris.core.events.EventPublisherService; import edu.kit.datamanager.idoris.dao.IAttributeMappingDao; diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/services/AttributeService.java b/src/main/java/edu/kit/datamanager/idoris/services/AttributeService.java similarity index 98% rename from src/main/java/edu/kit/datamanager/idoris/domain/services/AttributeService.java rename to src/main/java/edu/kit/datamanager/idoris/services/AttributeService.java index 7a69df1..fda9a77 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/services/AttributeService.java +++ b/src/main/java/edu/kit/datamanager/idoris/services/AttributeService.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.services; +package edu.kit.datamanager.idoris.services; import edu.kit.datamanager.idoris.core.events.EventPublisherService; import edu.kit.datamanager.idoris.dao.IAttributeDao; diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/services/OperationService.java b/src/main/java/edu/kit/datamanager/idoris/services/OperationService.java similarity index 98% rename from src/main/java/edu/kit/datamanager/idoris/domain/services/OperationService.java rename to src/main/java/edu/kit/datamanager/idoris/services/OperationService.java index 1b67525..b97285d 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/services/OperationService.java +++ b/src/main/java/edu/kit/datamanager/idoris/services/OperationService.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.services; +package edu.kit.datamanager.idoris.services; import edu.kit.datamanager.idoris.core.events.EventPublisherService; import edu.kit.datamanager.idoris.dao.IOperationDao; diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/services/TechnologyInterfaceService.java b/src/main/java/edu/kit/datamanager/idoris/services/TechnologyInterfaceService.java similarity index 99% rename from src/main/java/edu/kit/datamanager/idoris/domain/services/TechnologyInterfaceService.java rename to src/main/java/edu/kit/datamanager/idoris/services/TechnologyInterfaceService.java index 0498bff..3ea80ba 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/services/TechnologyInterfaceService.java +++ b/src/main/java/edu/kit/datamanager/idoris/services/TechnologyInterfaceService.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.services; +package edu.kit.datamanager.idoris.services; import edu.kit.datamanager.idoris.core.events.EventPublisherService; import edu.kit.datamanager.idoris.dao.ITechnologyInterfaceDao; diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/services/TypeProfileService.java b/src/main/java/edu/kit/datamanager/idoris/services/TypeProfileService.java similarity index 99% rename from src/main/java/edu/kit/datamanager/idoris/domain/services/TypeProfileService.java rename to src/main/java/edu/kit/datamanager/idoris/services/TypeProfileService.java index 77ad78c..ad28f00 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/services/TypeProfileService.java +++ b/src/main/java/edu/kit/datamanager/idoris/services/TypeProfileService.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.services; +package edu.kit.datamanager.idoris.services; import edu.kit.datamanager.idoris.core.events.EventPublisherService; import edu.kit.datamanager.idoris.dao.ITypeProfileDao; diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/services/package-info.java b/src/main/java/edu/kit/datamanager/idoris/services/package-info.java similarity index 94% rename from src/main/java/edu/kit/datamanager/idoris/domain/services/package-info.java rename to src/main/java/edu/kit/datamanager/idoris/services/package-info.java index 3fe3c79..bcf921c 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/services/package-info.java +++ b/src/main/java/edu/kit/datamanager/idoris/services/package-info.java @@ -19,4 +19,4 @@ * This package contains service classes that implement business logic for domain entities. * These services use repositories for data access and publish domain events when entities change. */ -package edu.kit.datamanager.idoris.domain.services; \ No newline at end of file +package edu.kit.datamanager.idoris.services; \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/web/api/IAtomicDataTypeApi.java b/src/main/java/edu/kit/datamanager/idoris/web/api/IAtomicDataTypeApi.java new file mode 100644 index 0000000..19dee97 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/web/api/IAtomicDataTypeApi.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.web.api; + +import edu.kit.datamanager.idoris.domain.entities.AtomicDataType; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.EntityModel; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +/** + * API interface for AtomicDataType endpoints. + * This interface defines the REST API for managing AtomicDataType entities. + */ +@Tag(name = "AtomicDataType", description = "API for managing AtomicDataTypes") +public interface IAtomicDataTypeApi { + + /** + * Gets all AtomicDataType entities. + * + * @return a collection of all AtomicDataType entities + */ + @GetMapping + @io.swagger.v3.oas.annotations.Operation( + summary = "Get all AtomicDataTypes", + description = "Returns a collection of all AtomicDataType entities", + responses = { + @ApiResponse(responseCode = "200", description = "AtomicDataTypes found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = AtomicDataType.class))) + } + ) + ResponseEntity>> getAllAtomicDataTypes(); + + /** + * Gets an AtomicDataType entity by its PID. + * + * @param pid the PID of the AtomicDataType to retrieve + * @return the AtomicDataType entity + */ + @GetMapping("/{pid}") + @io.swagger.v3.oas.annotations.Operation( + summary = "Get an AtomicDataType by PID", + description = "Returns an AtomicDataType entity by its PID", + responses = { + @ApiResponse(responseCode = "200", description = "AtomicDataType found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = AtomicDataType.class))), + @ApiResponse(responseCode = "404", description = "AtomicDataType not found") + } + ) + ResponseEntity> getAtomicDataType( + @Parameter(description = "PID of the AtomicDataType", required = true) + @PathVariable String pid); + + /** + * Creates a new AtomicDataType entity. + * The entity is validated before saving. + * + * @param atomicDataType the AtomicDataType entity to create + * @return the created AtomicDataType entity + */ + @PostMapping + @io.swagger.v3.oas.annotations.Operation( + summary = "Create a new AtomicDataType", + description = "Creates a new AtomicDataType entity after validating it", + responses = { + @ApiResponse(responseCode = "201", description = "AtomicDataType created", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = AtomicDataType.class))), + @ApiResponse(responseCode = "400", description = "Invalid input or validation failed") + } + ) + ResponseEntity> createAtomicDataType( + @Parameter(description = "AtomicDataType to create", required = true) + @Valid @RequestBody AtomicDataType atomicDataType); + + /** + * Updates an existing AtomicDataType entity. + * The entity is validated before saving. + * + * @param pid the PID of the AtomicDataType to update + * @param atomicDataType the updated AtomicDataType entity + * @return the updated AtomicDataType entity + */ + @PutMapping("/{pid}") + @io.swagger.v3.oas.annotations.Operation( + summary = "Update an AtomicDataType", + description = "Updates an existing AtomicDataType entity after validating it", + responses = { + @ApiResponse(responseCode = "200", description = "AtomicDataType updated", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = AtomicDataType.class))), + @ApiResponse(responseCode = "400", description = "Invalid input or validation failed"), + @ApiResponse(responseCode = "404", description = "AtomicDataType not found") + } + ) + ResponseEntity> updateAtomicDataType( + @Parameter(description = "PID of the AtomicDataType", required = true) + @PathVariable String pid, + @Parameter(description = "Updated AtomicDataType", required = true) + @Valid @RequestBody AtomicDataType atomicDataType); + + /** + * Deletes an AtomicDataType entity. + * + * @param pid the PID of the AtomicDataType to delete + * @return no content + */ + @DeleteMapping("/{pid}") + @io.swagger.v3.oas.annotations.Operation( + summary = "Delete an AtomicDataType", + description = "Deletes an AtomicDataType entity", + responses = { + @ApiResponse(responseCode = "204", description = "AtomicDataType deleted"), + @ApiResponse(responseCode = "404", description = "AtomicDataType not found") + } + ) + ResponseEntity deleteAtomicDataType( + @Parameter(description = "PID of the AtomicDataType", required = true) + @PathVariable String pid); + + /** + * Gets operations for an AtomicDataType. + * + * @param pid the PID of the AtomicDataType + * @return a collection of operations for the AtomicDataType + */ + @GetMapping("/{pid}/operations") + @io.swagger.v3.oas.annotations.Operation( + summary = "Get operations for an AtomicDataType", + description = "Returns a collection of operations that can be executed on an AtomicDataType", + responses = { + @ApiResponse(responseCode = "200", description = "Operations found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = edu.kit.datamanager.idoris.domain.entities.Operation.class))), + @ApiResponse(responseCode = "404", description = "AtomicDataType not found") + } + ) + ResponseEntity>> getOperationsForAtomicDataType( + @Parameter(description = "PID of the AtomicDataType", required = true) + @PathVariable String pid); +} diff --git a/src/main/java/edu/kit/datamanager/idoris/web/api/IOperationApi.java b/src/main/java/edu/kit/datamanager/idoris/web/api/IOperationApi.java new file mode 100644 index 0000000..572cbbe --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/web/api/IOperationApi.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.web.api; + +import edu.kit.datamanager.idoris.domain.entities.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.EntityModel; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +/** + * API interface for Operation endpoints. + * This interface defines the REST API for managing Operation entities. + */ +@Tag(name = "Operation", description = "API for managing Operations") +public interface IOperationApi { + + /** + * Gets all Operation entities. + * + * @return a collection of all Operation entities + */ + @GetMapping + @io.swagger.v3.oas.annotations.Operation( + summary = "Get all Operations", + description = "Returns a collection of all Operation entities", + responses = { + @ApiResponse(responseCode = "200", description = "Operations found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = Operation.class))) + } + ) + ResponseEntity>> getAllOperations(); + + /** + * Gets an Operation entity by its PID. + * + * @param pid the PID of the Operation to retrieve + * @return the Operation entity + */ + @GetMapping("/{pid}") + @io.swagger.v3.oas.annotations.Operation( + summary = "Get an Operation by PID", + description = "Returns an Operation entity by its PID", + responses = { + @ApiResponse(responseCode = "200", description = "Operation found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = Operation.class))), + @ApiResponse(responseCode = "404", description = "Operation not found") + } + ) + ResponseEntity> getOperation( + @Parameter(description = "PID of the Operation", required = true) + @PathVariable String pid); + + /** + * Creates a new Operation entity. + * The entity is validated before saving. + * + * @param operation the Operation entity to create + * @return the created Operation entity + */ + @PostMapping + @io.swagger.v3.oas.annotations.Operation( + summary = "Create a new Operation", + description = "Creates a new Operation entity after validating it", + responses = { + @ApiResponse(responseCode = "201", description = "Operation created", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = Operation.class))), + @ApiResponse(responseCode = "400", description = "Invalid input or validation failed") + } + ) + ResponseEntity> createOperation( + @Parameter(description = "Operation to create", required = true) + @Valid @RequestBody Operation operation); + + /** + * Updates an existing Operation entity. + * The entity is validated before saving. + * + * @param pid the PID of the Operation to update + * @param operation the updated Operation entity + * @return the updated Operation entity + */ + @PutMapping("/{pid}") + @io.swagger.v3.oas.annotations.Operation( + summary = "Update an Operation", + description = "Updates an existing Operation entity after validating it", + responses = { + @ApiResponse(responseCode = "200", description = "Operation updated", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = Operation.class))), + @ApiResponse(responseCode = "400", description = "Invalid input or validation failed"), + @ApiResponse(responseCode = "404", description = "Operation not found") + } + ) + ResponseEntity> updateOperation( + @Parameter(description = "PID of the Operation", required = true) + @PathVariable String pid, + @Parameter(description = "Updated Operation", required = true) + @Valid @RequestBody Operation operation); + + /** + * Deletes an Operation entity. + * + * @param pid the PID of the Operation to delete + * @return no content + */ + @DeleteMapping("/{pid}") + @io.swagger.v3.oas.annotations.Operation( + summary = "Delete an Operation", + description = "Deletes an Operation entity", + responses = { + @ApiResponse(responseCode = "204", description = "Operation deleted"), + @ApiResponse(responseCode = "404", description = "Operation not found") + } + ) + ResponseEntity deleteOperation( + @Parameter(description = "PID of the Operation", required = true) + @PathVariable String pid); + + /** + * Validates an Operation entity. + * + * @param pid the PID of the Operation to validate + * @return the validation result + */ + @GetMapping("/{pid}/validate") + @io.swagger.v3.oas.annotations.Operation( + summary = "Validate an Operation", + description = "Validates an Operation entity and returns the validation result", + responses = { + @ApiResponse(responseCode = "200", description = "Operation is valid"), + @ApiResponse(responseCode = "218", description = "Operation is invalid"), + @ApiResponse(responseCode = "404", description = "Operation not found") + } + ) + ResponseEntity validate( + @Parameter(description = "PID of the Operation", required = true) + @PathVariable String pid); + + /** + * Gets operations for a data type. + * + * @param pid the PID of the data type + * @return a collection of operations for the data type + */ + @GetMapping("/search/getOperationsForDataType") + @io.swagger.v3.oas.annotations.Operation( + summary = "Get operations for a data type", + description = "Returns a collection of operations that can be executed on a data type", + responses = { + @ApiResponse(responseCode = "200", description = "Operations found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = Operation.class))) + } + ) + ResponseEntity>> getOperationsForDataType( + @Parameter(description = "PID of the data type", required = true) + @RequestParam String pid); +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/web/api/ITypeProfileApi.java b/src/main/java/edu/kit/datamanager/idoris/web/api/ITypeProfileApi.java new file mode 100644 index 0000000..5813fcc --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/web/api/ITypeProfileApi.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.web.api; + +import edu.kit.datamanager.idoris.domain.entities.Attribute; +import edu.kit.datamanager.idoris.domain.entities.Operation; +import edu.kit.datamanager.idoris.domain.entities.TypeProfile; +import edu.kit.datamanager.idoris.web.v1.TypeProfileController.TypeProfileInheritance; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.EntityModel; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +/** + * API interface for TypeProfile endpoints. + * This interface defines the REST API for managing TypeProfile entities. + */ +@Tag(name = "TypeProfile", description = "API for managing TypeProfiles") +public interface ITypeProfileApi { + + /** + * Gets all TypeProfile entities. + * + * @return a collection of all TypeProfile entities + */ + @GetMapping + @io.swagger.v3.oas.annotations.Operation( + summary = "Get all TypeProfiles", + description = "Returns a collection of all TypeProfile entities", + responses = { + @ApiResponse(responseCode = "200", description = "TypeProfiles found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = TypeProfile.class))) + } + ) + ResponseEntity>> getAllTypeProfiles(); + + /** + * Gets a TypeProfile entity by its PID. + * + * @param pid the PID of the TypeProfile to retrieve + * @return the TypeProfile entity + */ + @GetMapping("/{pid}") + @io.swagger.v3.oas.annotations.Operation( + summary = "Get a TypeProfile by PID", + description = "Returns a TypeProfile entity by its PID", + responses = { + @ApiResponse(responseCode = "200", description = "TypeProfile found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = TypeProfile.class))), + @ApiResponse(responseCode = "404", description = "TypeProfile not found") + } + ) + ResponseEntity> getTypeProfile( + @Parameter(description = "PID of the TypeProfile", required = true) + @PathVariable String pid); + + /** + * Gets operations for a TypeProfile. + * + * @param pid the PID of the TypeProfile + * @return a collection of operations for the TypeProfile + */ + @GetMapping("/{pid}/operations") + @io.swagger.v3.oas.annotations.Operation( + summary = "Get operations for a TypeProfile", + description = "Returns a collection of operations that can be executed on a TypeProfile", + responses = { + @ApiResponse(responseCode = "200", description = "Operations found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = Operation.class))), + @ApiResponse(responseCode = "404", description = "TypeProfile not found") + } + ) + ResponseEntity>> getOperationsForTypeProfile( + @Parameter(description = "PID of the TypeProfile", required = true) + @PathVariable String pid); + + /** + * Validates a TypeProfile entity. + * + * @param pid the PID of the TypeProfile to validate + * @return the validation result + */ + @GetMapping("/{pid}/validate") + @io.swagger.v3.oas.annotations.Operation( + summary = "Validate a TypeProfile", + description = "Validates a TypeProfile entity and returns the validation result", + responses = { + @ApiResponse(responseCode = "200", description = "TypeProfile is valid"), + @ApiResponse(responseCode = "218", description = "TypeProfile is invalid"), + @ApiResponse(responseCode = "404", description = "TypeProfile not found") + } + ) + ResponseEntity validate( + @Parameter(description = "PID of the TypeProfile", required = true) + @PathVariable String pid); + + /** + * Gets inherited attributes for a TypeProfile. + * + * @param pid the PID of the TypeProfile + * @return a collection of inherited attributes + */ + @GetMapping("/{pid}/inheritedAttributes") + @io.swagger.v3.oas.annotations.Operation( + summary = "Get inherited attributes of a TypeProfile", + description = "Returns a collection of attributes inherited by a TypeProfile", + responses = { + @ApiResponse(responseCode = "200", description = "Inherited attributes found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = Attribute.class))), + @ApiResponse(responseCode = "404", description = "TypeProfile not found") + } + ) + ResponseEntity>> getInheritedAttributes( + @Parameter(description = "PID of the TypeProfile", required = true) + @PathVariable String pid); + + /** + * Creates a new TypeProfile entity. + * The entity is validated before saving. + * + * @param typeProfile the TypeProfile entity to create + * @return the created TypeProfile entity + */ + @PostMapping + @io.swagger.v3.oas.annotations.Operation( + summary = "Create a new TypeProfile", + description = "Creates a new TypeProfile entity after validating it", + responses = { + @ApiResponse(responseCode = "201", description = "TypeProfile created", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = TypeProfile.class))), + @ApiResponse(responseCode = "400", description = "Invalid input or validation failed") + } + ) + ResponseEntity> createTypeProfile( + @Parameter(description = "TypeProfile to create", required = true) + @Valid @RequestBody TypeProfile typeProfile); + + /** + * Updates an existing TypeProfile entity. + * The entity is validated before saving. + * + * @param pid the PID of the TypeProfile to update + * @param typeProfile the updated TypeProfile entity + * @return the updated TypeProfile entity + */ + @PutMapping("/{pid}") + @io.swagger.v3.oas.annotations.Operation( + summary = "Update a TypeProfile", + description = "Updates an existing TypeProfile entity after validating it", + responses = { + @ApiResponse(responseCode = "200", description = "TypeProfile updated", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = TypeProfile.class))), + @ApiResponse(responseCode = "400", description = "Invalid input or validation failed"), + @ApiResponse(responseCode = "404", description = "TypeProfile not found") + } + ) + ResponseEntity> updateTypeProfile( + @Parameter(description = "PID of the TypeProfile", required = true) + @PathVariable String pid, + @Parameter(description = "Updated TypeProfile", required = true) + @Valid @RequestBody TypeProfile typeProfile); + + /** + * Deletes a TypeProfile entity. + * + * @param pid the PID of the TypeProfile to delete + * @return no content + */ + @DeleteMapping("/{pid}") + @io.swagger.v3.oas.annotations.Operation( + summary = "Delete a TypeProfile", + description = "Deletes a TypeProfile entity", + responses = { + @ApiResponse(responseCode = "204", description = "TypeProfile deleted"), + @ApiResponse(responseCode = "404", description = "TypeProfile not found") + } + ) + ResponseEntity deleteTypeProfile( + @Parameter(description = "PID of the TypeProfile", required = true) + @PathVariable String pid); + + /** + * Gets the inheritance tree of a TypeProfile. + * + * @param pid the PID of the TypeProfile + * @return the inheritance tree + */ + @GetMapping("/{pid}/inheritanceTree") + @io.swagger.v3.oas.annotations.Operation( + summary = "Get inheritance tree of a TypeProfile", + description = "Returns the inheritance tree of a TypeProfile", + responses = { + @ApiResponse(responseCode = "200", description = "Inheritance tree found", + content = @Content(mediaType = "application/hal+json")), + @ApiResponse(responseCode = "404", description = "TypeProfile not found") + } + ) + ResponseEntity> getInheritanceTree( + @Parameter(description = "PID of the TypeProfile", required = true) + @NotNull @PathVariable String pid); +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/web/v1/AtomicDataTypeController.java b/src/main/java/edu/kit/datamanager/idoris/web/v1/AtomicDataTypeController.java index d165159..62073bb 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/v1/AtomicDataTypeController.java +++ b/src/main/java/edu/kit/datamanager/idoris/web/v1/AtomicDataTypeController.java @@ -19,12 +19,13 @@ import edu.kit.datamanager.idoris.configuration.ApplicationProperties; import edu.kit.datamanager.idoris.domain.entities.AtomicDataType; import edu.kit.datamanager.idoris.domain.entities.Operation; -import edu.kit.datamanager.idoris.domain.services.AtomicDataTypeService; -import edu.kit.datamanager.idoris.domain.services.OperationService; import edu.kit.datamanager.idoris.rules.logic.RuleService; import edu.kit.datamanager.idoris.rules.logic.RuleTask; import edu.kit.datamanager.idoris.rules.validation.ValidationResult; +import edu.kit.datamanager.idoris.services.AtomicDataTypeService; +import edu.kit.datamanager.idoris.services.OperationService; import edu.kit.datamanager.idoris.web.ValidationException; +import edu.kit.datamanager.idoris.web.api.IAtomicDataTypeApi; import edu.kit.datamanager.idoris.web.hateoas.AtomicDataTypeModelAssembler; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -51,10 +52,10 @@ * This controller provides endpoints for managing AtomicDataType entities. */ @RestController -@RequestMapping("/v1/api/atomicDataTypes") +@RequestMapping("/v1/atomicDataTypes") @Tag(name = "AtomicDataType", description = "API for managing AtomicDataTypes") @Slf4j -public class AtomicDataTypeController { +public class AtomicDataTypeController implements IAtomicDataTypeApi { private final AtomicDataTypeService atomicDataTypeService; private final OperationService operationService; @@ -71,10 +72,9 @@ public AtomicDataTypeController(AtomicDataTypeService atomicDataTypeService, Ope } /** - * Gets all AtomicDataType entities. - * - * @return a collection of all AtomicDataType entities + * {@inheritDoc} */ + @Override @GetMapping @io.swagger.v3.oas.annotations.Operation( summary = "Get all AtomicDataTypes", @@ -99,11 +99,9 @@ public ResponseEntity>> getAllAtomic } /** - * Gets an AtomicDataType entity by its PID. - * - * @param pid the PID of the AtomicDataType to retrieve - * @return the AtomicDataType entity + * {@inheritDoc} */ + @Override @GetMapping("/{pid}") @io.swagger.v3.oas.annotations.Operation( summary = "Get an AtomicDataType by PID", @@ -125,25 +123,23 @@ public ResponseEntity> getAtomicDataType( } /** - * Creates a new AtomicDataType entity. - * - * @param atomicDataType the AtomicDataType entity to create - * @return the created AtomicDataType entity + * {@inheritDoc} */ + @Override @PostMapping @io.swagger.v3.oas.annotations.Operation( summary = "Create a new AtomicDataType", - description = "Creates a new AtomicDataType entity", + description = "Creates a new AtomicDataType entity after validating it", responses = { @ApiResponse(responseCode = "201", description = "AtomicDataType created", content = @Content(mediaType = "application/hal+json", schema = @Schema(implementation = AtomicDataType.class))), - @ApiResponse(responseCode = "400", description = "Invalid input") + @ApiResponse(responseCode = "400", description = "Invalid input or validation failed") } ) public ResponseEntity> createAtomicDataType( @Parameter(description = "AtomicDataType to create", required = true) - @RequestBody AtomicDataType atomicDataType) { + @Valid @RequestBody AtomicDataType atomicDataType) { // Validate BEFORE saving ValidationResult validationResult = ruleService.executeRules( @@ -165,21 +161,18 @@ public ResponseEntity> createAtomicDataType( } /** - * Updates an existing AtomicDataType entity. - * - * @param pid the PID of the AtomicDataType to update - * @param atomicDataType the updated AtomicDataType entity - * @return the updated AtomicDataType entity + * {@inheritDoc} */ + @Override @PutMapping("/{pid}") @io.swagger.v3.oas.annotations.Operation( summary = "Update an AtomicDataType", - description = "Updates an existing AtomicDataType entity", + description = "Updates an existing AtomicDataType entity after validating it", responses = { @ApiResponse(responseCode = "200", description = "AtomicDataType updated", content = @Content(mediaType = "application/hal+json", schema = @Schema(implementation = AtomicDataType.class))), - @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "400", description = "Invalid input or validation failed"), @ApiResponse(responseCode = "404", description = "AtomicDataType not found") } ) @@ -193,17 +186,30 @@ public ResponseEntity> updateAtomicDataType( } atomicDataType.setPid(pid); + + // Validate BEFORE saving + ValidationResult validationResult = ruleService.executeRules( + RuleTask.VALIDATE, + atomicDataType, + ValidationResult::new + ); + log.debug("Validation result for AtomicDataType {}: {}", atomicDataType, validationResult); + + // Check if validation failed based on your validation policy + if (hasValidationErrors(validationResult)) { + throw new ValidationException("Entity validation failed", validationResult); + } + + // Only save if validation passes AtomicDataType updatedAtomicDataType = atomicDataTypeService.updateAtomicDataType(atomicDataType); EntityModel entityModel = atomicDataTypeModelAssembler.toModel(updatedAtomicDataType); return ResponseEntity.ok(entityModel); } /** - * Deletes an AtomicDataType entity. - * - * @param pid the PID of the AtomicDataType to delete - * @return no content + * {@inheritDoc} */ + @Override @DeleteMapping("/{pid}") @io.swagger.v3.oas.annotations.Operation( summary = "Delete an AtomicDataType", @@ -225,11 +231,9 @@ public ResponseEntity deleteAtomicDataType( } /** - * Gets operations for an AtomicDataType. - * - * @param pid the PID of the AtomicDataType - * @return a collection of operations for the AtomicDataType + * {@inheritDoc} */ + @Override @GetMapping("/{pid}/operations") @io.swagger.v3.oas.annotations.Operation( summary = "Get operations for an AtomicDataType", @@ -271,4 +275,4 @@ private boolean hasValidationErrors(ValidationResult validationResult) { && !entry.getValue().isEmpty()); } -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/idoris/web/v1/AttributeController.java b/src/main/java/edu/kit/datamanager/idoris/web/v1/AttributeController.java index 44cbd68..bcb6584 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/v1/AttributeController.java +++ b/src/main/java/edu/kit/datamanager/idoris/web/v1/AttributeController.java @@ -18,7 +18,7 @@ import edu.kit.datamanager.idoris.domain.entities.Attribute; import edu.kit.datamanager.idoris.domain.entities.DataType; -import edu.kit.datamanager.idoris.domain.services.AttributeService; +import edu.kit.datamanager.idoris.services.AttributeService; import edu.kit.datamanager.idoris.web.api.IAttributeApi; import edu.kit.datamanager.idoris.web.hateoas.AttributeModelAssembler; import edu.kit.datamanager.idoris.web.hateoas.DataTypeModelAssembler; @@ -42,7 +42,7 @@ * This controller provides endpoints for managing Attribute entities. */ @RestController -@RequestMapping("/api/attributes") +@RequestMapping("/v1/attributes") public class AttributeController implements IAttributeApi { @Autowired diff --git a/src/main/java/edu/kit/datamanager/idoris/web/v1/OperationController.java b/src/main/java/edu/kit/datamanager/idoris/web/v1/OperationController.java index 01bf3c5..f810312 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/v1/OperationController.java +++ b/src/main/java/edu/kit/datamanager/idoris/web/v1/OperationController.java @@ -17,9 +17,11 @@ package edu.kit.datamanager.idoris.web.v1; import edu.kit.datamanager.idoris.domain.entities.Operation; -import edu.kit.datamanager.idoris.domain.services.OperationService; import edu.kit.datamanager.idoris.rules.validation.ValidationPolicyValidator; import edu.kit.datamanager.idoris.rules.validation.ValidationResult; +import edu.kit.datamanager.idoris.services.OperationService; +import edu.kit.datamanager.idoris.web.ValidationException; +import edu.kit.datamanager.idoris.web.api.IOperationApi; import edu.kit.datamanager.idoris.web.hateoas.OperationModelAssembler; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -46,9 +48,9 @@ * This controller provides endpoints for managing Operation entities. */ @RestController -@RequestMapping("/api/operations") +@RequestMapping("/v1/operations") @Tag(name = "Operation", description = "API for managing Operations") -public class OperationController { +public class OperationController implements IOperationApi { @Autowired private OperationService operationService; @@ -57,10 +59,9 @@ public class OperationController { private OperationModelAssembler operationModelAssembler; /** - * Gets all Operation entities. - * - * @return a collection of all Operation entities + * {@inheritDoc} */ + @Override @GetMapping @io.swagger.v3.oas.annotations.Operation( summary = "Get all Operations", @@ -85,11 +86,9 @@ public ResponseEntity>> getAllOperations( } /** - * Gets an Operation entity by its PID. - * - * @param pid the PID of the Operation to retrieve - * @return the Operation entity + * {@inheritDoc} */ + @Override @GetMapping("/{pid}") @io.swagger.v3.oas.annotations.Operation( summary = "Get an Operation by PID", @@ -111,46 +110,51 @@ public ResponseEntity> getOperation( } /** - * Creates a new Operation entity. - * - * @param operation the Operation entity to create - * @return the created Operation entity + * {@inheritDoc} */ + @Override @PostMapping @io.swagger.v3.oas.annotations.Operation( summary = "Create a new Operation", - description = "Creates a new Operation entity", + description = "Creates a new Operation entity after validating it", responses = { @ApiResponse(responseCode = "201", description = "Operation created", content = @Content(mediaType = "application/hal+json", schema = @Schema(implementation = Operation.class))), - @ApiResponse(responseCode = "400", description = "Invalid input") + @ApiResponse(responseCode = "400", description = "Invalid input or validation failed") } ) public ResponseEntity> createOperation( @Parameter(description = "Operation to create", required = true) @Valid @RequestBody Operation operation) { + // Validate the operation using the ValidationPolicyValidator + ValidationPolicyValidator validator = new ValidationPolicyValidator(); + ValidationResult validationResult = operation.execute(validator); + + // Check if validation failed + if (!validationResult.isValid()) { + throw new ValidationException("Operation validation failed", validationResult); + } + + // Only save if validation passes Operation createdOperation = operationService.createOperation(operation); EntityModel entityModel = operationModelAssembler.toModel(createdOperation); return ResponseEntity.status(HttpStatus.CREATED).body(entityModel); } /** - * Updates an existing Operation entity. - * - * @param pid the PID of the Operation to update - * @param operation the updated Operation entity - * @return the updated Operation entity + * {@inheritDoc} */ + @Override @PutMapping("/{pid}") @io.swagger.v3.oas.annotations.Operation( summary = "Update an Operation", - description = "Updates an existing Operation entity", + description = "Updates an existing Operation entity after validating it", responses = { @ApiResponse(responseCode = "200", description = "Operation updated", content = @Content(mediaType = "application/hal+json", schema = @Schema(implementation = Operation.class))), - @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "400", description = "Invalid input or validation failed"), @ApiResponse(responseCode = "404", description = "Operation not found") } ) @@ -164,17 +168,26 @@ public ResponseEntity> updateOperation( } operation.setPid(pid); + + // Validate the operation using the ValidationPolicyValidator + ValidationPolicyValidator validator = new ValidationPolicyValidator(); + ValidationResult validationResult = operation.execute(validator); + + // Check if validation failed + if (!validationResult.isValid()) { + throw new ValidationException("Operation validation failed", validationResult); + } + + // Only save if validation passes Operation updatedOperation = operationService.updateOperation(operation); EntityModel entityModel = operationModelAssembler.toModel(updatedOperation); return ResponseEntity.ok(entityModel); } /** - * Deletes an Operation entity. - * - * @param pid the PID of the Operation to delete - * @return no content + * {@inheritDoc} */ + @Override @DeleteMapping("/{pid}") @io.swagger.v3.oas.annotations.Operation( summary = "Delete an Operation", @@ -196,11 +209,9 @@ public ResponseEntity deleteOperation( } /** - * Validates an Operation entity. - * - * @param pid the PID of the Operation to validate - * @return the validation result + * {@inheritDoc} */ + @Override @GetMapping("/{pid}/validate") @io.swagger.v3.oas.annotations.Operation( summary = "Validate an Operation", @@ -230,11 +241,9 @@ public ResponseEntity validate( } /** - * Gets operations for a data type. - * - * @param pid the PID of the data type - * @return a collection of operations for the data type + * {@inheritDoc} */ + @Override @GetMapping("/search/getOperationsForDataType") @io.swagger.v3.oas.annotations.Operation( summary = "Get operations for a data type", diff --git a/src/main/java/edu/kit/datamanager/idoris/web/v1/PidRedirectController.java b/src/main/java/edu/kit/datamanager/idoris/web/v1/PidRedirectController.java index c35c8cb..bb9cc61 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/v1/PidRedirectController.java +++ b/src/main/java/edu/kit/datamanager/idoris/web/v1/PidRedirectController.java @@ -31,7 +31,7 @@ import java.util.*; @Controller -@RequestMapping("/pid") +@RequestMapping("/v1/pid") @Log public class PidRedirectController { diff --git a/src/main/java/edu/kit/datamanager/idoris/web/v1/TechnologyInterfaceController.java b/src/main/java/edu/kit/datamanager/idoris/web/v1/TechnologyInterfaceController.java index 32b81eb..c945499 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/v1/TechnologyInterfaceController.java +++ b/src/main/java/edu/kit/datamanager/idoris/web/v1/TechnologyInterfaceController.java @@ -18,7 +18,7 @@ import edu.kit.datamanager.idoris.domain.entities.Attribute; import edu.kit.datamanager.idoris.domain.entities.TechnologyInterface; -import edu.kit.datamanager.idoris.domain.services.TechnologyInterfaceService; +import edu.kit.datamanager.idoris.services.TechnologyInterfaceService; import edu.kit.datamanager.idoris.web.api.ITechnologyInterfaceApi; import edu.kit.datamanager.idoris.web.hateoas.AttributeModelAssembler; import edu.kit.datamanager.idoris.web.hateoas.TechnologyInterfaceModelAssembler; @@ -42,7 +42,7 @@ * This controller provides endpoints for managing TechnologyInterface entities. */ @RestController -@RequestMapping("/api/technologyInterfaces") +@RequestMapping("/v1/technologyInterfaces") public class TechnologyInterfaceController implements ITechnologyInterfaceApi { @Autowired diff --git a/src/main/java/edu/kit/datamanager/idoris/web/v1/TypeProfileController.java b/src/main/java/edu/kit/datamanager/idoris/web/v1/TypeProfileController.java index 0bc5372..c42e046 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/v1/TypeProfileController.java +++ b/src/main/java/edu/kit/datamanager/idoris/web/v1/TypeProfileController.java @@ -16,12 +16,17 @@ package edu.kit.datamanager.idoris.web.v1; +import edu.kit.datamanager.idoris.configuration.ApplicationProperties; import edu.kit.datamanager.idoris.domain.entities.Attribute; import edu.kit.datamanager.idoris.domain.entities.Operation; import edu.kit.datamanager.idoris.domain.entities.TypeProfile; -import edu.kit.datamanager.idoris.domain.services.OperationService; -import edu.kit.datamanager.idoris.domain.services.TypeProfileService; +import edu.kit.datamanager.idoris.rules.logic.RuleService; +import edu.kit.datamanager.idoris.rules.logic.RuleTask; import edu.kit.datamanager.idoris.rules.validation.ValidationResult; +import edu.kit.datamanager.idoris.services.OperationService; +import edu.kit.datamanager.idoris.services.TypeProfileService; +import edu.kit.datamanager.idoris.web.ValidationException; +import edu.kit.datamanager.idoris.web.api.ITypeProfileApi; import edu.kit.datamanager.idoris.web.hateoas.TypeProfileModelAssembler; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -30,7 +35,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.hateoas.CollectionModel; import org.springframework.hateoas.EntityModel; import org.springframework.http.HttpStatus; @@ -50,23 +54,31 @@ * This controller provides endpoints for managing TypeProfile entities. */ @RestController -@RequestMapping("/api/typeProfiles") +@RequestMapping("/v1/typeProfiles") @Tag(name = "TypeProfile", description = "API for managing TypeProfiles") -public class TypeProfileController { - @Autowired - private TypeProfileService typeProfileService; - - @Autowired - private OperationService operationService; - - @Autowired - private TypeProfileModelAssembler typeProfileModelAssembler; +public class TypeProfileController implements ITypeProfileApi { + private final TypeProfileService typeProfileService; + private final OperationService operationService; + private final TypeProfileModelAssembler typeProfileModelAssembler; + private final RuleService ruleService; + private final ApplicationProperties applicationProperties; + + public TypeProfileController(TypeProfileService typeProfileService, + OperationService operationService, + TypeProfileModelAssembler typeProfileModelAssembler, + RuleService ruleService, + ApplicationProperties applicationProperties) { + this.typeProfileService = typeProfileService; + this.operationService = operationService; + this.typeProfileModelAssembler = typeProfileModelAssembler; + this.ruleService = ruleService; + this.applicationProperties = applicationProperties; + } /** - * Gets all TypeProfile entities. - * - * @return a collection of all TypeProfile entities + * {@inheritDoc} */ + @Override @GetMapping @io.swagger.v3.oas.annotations.Operation( summary = "Get all TypeProfiles", @@ -91,11 +103,9 @@ public ResponseEntity>> getAllTypeProfi } /** - * Gets a TypeProfile entity by its PID. - * - * @param pid the PID of the TypeProfile to retrieve - * @return the TypeProfile entity + * {@inheritDoc} */ + @Override @GetMapping("/{pid}") @io.swagger.v3.oas.annotations.Operation( summary = "Get a TypeProfile by PID", @@ -117,11 +127,9 @@ public ResponseEntity> getTypeProfile( } /** - * Gets operations for a TypeProfile. - * - * @param pid the PID of the TypeProfile - * @return a collection of operations for the TypeProfile + * {@inheritDoc} */ + @Override @GetMapping("/{pid}/operations") @io.swagger.v3.oas.annotations.Operation( summary = "Get operations for a TypeProfile", @@ -156,11 +164,9 @@ public ResponseEntity>> getOperationsForT } /** - * Validates a TypeProfile entity. - * - * @param pid the PID of the TypeProfile to validate - * @return the validation result + * {@inheritDoc} */ + @Override @GetMapping("/{pid}/validate") @io.swagger.v3.oas.annotations.Operation( summary = "Validate a TypeProfile", @@ -183,11 +189,9 @@ public ResponseEntity validate( } /** - * Gets the inherited attributes of a TypeProfile. - * - * @param pid the PID of the TypeProfile - * @return a collection of inherited attributes + * {@inheritDoc} */ + @Override @GetMapping("/{pid}/inheritedAttributes") @io.swagger.v3.oas.annotations.Operation( summary = "Get inherited attributes of a TypeProfile", @@ -220,46 +224,55 @@ public ResponseEntity>> getInheritedAttri } /** - * Creates a new TypeProfile entity. - * - * @param typeProfile the TypeProfile entity to create - * @return the created TypeProfile entity + * {@inheritDoc} */ + @Override @PostMapping @io.swagger.v3.oas.annotations.Operation( summary = "Create a new TypeProfile", - description = "Creates a new TypeProfile entity", + description = "Creates a new TypeProfile entity after validating it", responses = { @ApiResponse(responseCode = "201", description = "TypeProfile created", content = @Content(mediaType = "application/hal+json", schema = @Schema(implementation = TypeProfile.class))), - @ApiResponse(responseCode = "400", description = "Invalid input") + @ApiResponse(responseCode = "400", description = "Invalid input or validation failed") } ) public ResponseEntity> createTypeProfile( @Parameter(description = "TypeProfile to create", required = true) @Valid @RequestBody TypeProfile typeProfile) { + + // Validate BEFORE saving + ValidationResult validationResult = ruleService.executeRules( + RuleTask.VALIDATE, + typeProfile, + ValidationResult::new + ); + + // Check if validation failed based on your validation policy + if (hasValidationErrors(validationResult)) { + throw new ValidationException("Entity validation failed", validationResult); + } + + // Only save if validation passes TypeProfile createdTypeProfile = typeProfileService.createTypeProfile(typeProfile); EntityModel entityModel = typeProfileModelAssembler.toModel(createdTypeProfile); return ResponseEntity.status(HttpStatus.CREATED).body(entityModel); } /** - * Updates an existing TypeProfile entity. - * - * @param pid the PID of the TypeProfile to update - * @param typeProfile the updated TypeProfile entity - * @return the updated TypeProfile entity + * {@inheritDoc} */ + @Override @PutMapping("/{pid}") @io.swagger.v3.oas.annotations.Operation( summary = "Update a TypeProfile", - description = "Updates an existing TypeProfile entity", + description = "Updates an existing TypeProfile entity after validating it", responses = { @ApiResponse(responseCode = "200", description = "TypeProfile updated", content = @Content(mediaType = "application/hal+json", schema = @Schema(implementation = TypeProfile.class))), - @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "400", description = "Invalid input or validation failed"), @ApiResponse(responseCode = "404", description = "TypeProfile not found") } ) @@ -273,17 +286,29 @@ public ResponseEntity> updateTypeProfile( } typeProfile.setPid(pid); + + // Validate BEFORE saving + ValidationResult validationResult = ruleService.executeRules( + RuleTask.VALIDATE, + typeProfile, + ValidationResult::new + ); + + // Check if validation failed based on your validation policy + if (hasValidationErrors(validationResult)) { + throw new ValidationException("Entity validation failed", validationResult); + } + + // Only save if validation passes TypeProfile updatedTypeProfile = typeProfileService.updateTypeProfile(typeProfile); EntityModel entityModel = typeProfileModelAssembler.toModel(updatedTypeProfile); return ResponseEntity.ok(entityModel); } /** - * Deletes a TypeProfile entity. - * - * @param pid the PID of the TypeProfile to delete - * @return no content + * {@inheritDoc} */ + @Override @DeleteMapping("/{pid}") @io.swagger.v3.oas.annotations.Operation( summary = "Delete a TypeProfile", @@ -305,11 +330,9 @@ public ResponseEntity deleteTypeProfile( } /** - * Gets the inheritance tree of a TypeProfile. - * - * @param pid the PID of the TypeProfile - * @return the inheritance tree + * {@inheritDoc} */ + @Override @GetMapping("/{pid}/inheritanceTree") @io.swagger.v3.oas.annotations.Operation( summary = "Get inheritance tree of a TypeProfile", @@ -365,6 +388,20 @@ private EntityModel buildInheritanceTree(TypeProfile typ return node; } + /** + * Checks if a validation result contains errors based on the configured validation level. + * + * @param validationResult the validation result to check + * @return true if the validation result contains errors, false otherwise + */ + private boolean hasValidationErrors(ValidationResult validationResult) { + return validationResult.getOutputMessages() + .entrySet() + .stream() + .anyMatch(entry -> entry.getKey().isHigherOrEqualTo(applicationProperties.getValidationLevel()) + && !entry.getValue().isEmpty()); + } + public record TypeProfileInheritance( String pid, String name, diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e965707..3a5b36a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -27,7 +27,6 @@ springdoc.show-actuator=true springdoc.api-docs.path=/v1/api-docs springdoc.swagger-ui.path=/swagger-ui.html springdoc.swagger-ui.enabled=true -#spring.data.rest.base-path= spring.hateoas.use-hal-as-default-json-media-type=true server.servlet.context-path=/api # IDORIS Settings From 3b53d93f289069ce6c5e513db8bf2df3a0f1d372 Mon Sep 17 00:00:00 2001 From: Maximilian Inckmann Date: Wed, 16 Jul 2025 16:58:37 +0200 Subject: [PATCH 03/19] refactored into Spring modulith Signed-off-by: Maximilian Inckmann --- README.md | 23 - event-architecture-design.md | 284 -------- event-architecture-implementation.md | 112 --- event-based-architecture.md | 148 ---- modulith-migration-plan.md | 202 ------ openapi.json | 655 ------------------ response.md | 98 --- .../{ => attributes}/dao/IAttributeDao.java | 7 +- .../entities/Attribute.java | 9 +- .../{domain => attributes}/package-info.java | 13 +- .../services/AttributeService.java | 64 +- .../web/api/IAttributeApi.java | 33 +- .../web/hateoas/AttributeModelAssembler.java | 7 +- .../web/v1/AttributeController.java | 55 +- .../configuration/ETagConfiguration.java | 77 ++ .../configuration/ETagControllerAdvice.java | 82 +++ .../idoris/configuration/HateoasConfig.java | 12 +- .../configuration/TypedPIDMakerConfig.java | 4 +- .../configuration/ValidationConfig.java | 4 +- .../domain/AdministrativeMetadata.java} | 12 +- .../{ => core}/domain/VisitableElement.java | 4 +- .../{ => core/domain}/dao/IGenericRepo.java | 8 +- .../{ => core}/domain/entities/Reference.java | 4 +- .../domain/enums/CombinationOptions.java | 4 +- .../exceptions}/ValidationException.java | 5 +- .../ValidationExceptionHandler.java | 18 +- .../domain}/package-info.java | 11 +- .../web/hateoas/EntityModelAssembler.java | 8 +- .../core/events/EntityCreatedEvent.java | 8 +- .../core/events/EntityDeletedEvent.java | 8 +- .../core/events/EntityImportedEvent.java | 8 +- .../core/events/EntityPatchedEvent.java | 44 ++ .../core/events/EntityUpdatedEvent.java | 8 +- .../core/events/EventPublisherService.java | 43 +- .../events/GenericEntityCreatedEvent.java | 2 +- .../events/GenericEntityDeletedEvent.java | 2 +- .../events/GenericEntityPatchedEvent.java | 41 ++ .../events/GenericEntityUpdatedEvent.java | 2 +- .../idoris/core/events/PIDGeneratedEvent.java | 8 +- .../core/events/SchemaGeneratedEvent.java | 10 +- .../core/events/VersionCreatedEvent.java | 8 +- .../dao/IAtomicDataTypeDao.java | 7 +- .../{ => datatypes}/dao/IDataTypeDao.java | 7 +- .../{ => datatypes}/dao/ITypeProfileDao.java | 7 +- .../entities/AtomicDataType.java | 7 +- .../entities/DataType.java | 6 +- .../entities/TypeProfile.java | 7 +- .../enums/PrimitiveDataTypes.java | 4 +- .../{web => datatypes}/package-info.java | 15 +- .../services/AtomicDataTypeService.java | 68 +- .../services/TypeProfileService.java | 61 +- .../web/api/IAtomicDataTypeApi.java | 33 +- .../web/api/ITypeProfileApi.java | 37 +- .../hateoas/AtomicDataTypeModelAssembler.java | 7 +- .../web/hateoas/DataTypeModelAssembler.java | 15 +- .../hateoas/TypeProfileModelAssembler.java | 7 +- .../web/v1/AtomicDataTypeController.java | 85 ++- .../web/v1/TypeProfileController.java | 74 +- .../notification/EntityChangeNotifier.java | 14 +- .../notification/EntityChangeSubscriber.java | 10 +- .../LoggingEntityChangeSubscriber.java | 10 +- .../dao/IAttributeMappingDao.java | 6 +- .../{ => operations}/dao/IOperationDao.java | 7 +- .../entities/AttributeMapping.java | 7 +- .../entities/Operation.java | 7 +- .../entities/OperationStep.java | 9 +- .../entities}/enums/ExecutionMode.java | 4 +- .../idoris/operations/package-info.java | 28 + .../services/AttributeMappingService.java | 6 +- .../services/OperationService.java | 56 +- .../web/api/IOperationApi.java | 31 +- .../web/hateoas/OperationModelAssembler.java | 7 +- .../web/v1/OperationController.java | 71 +- .../idoris/pids/ConfigurablePIDGenerator.java | 12 +- .../pids/PIDGenerationEventListener.java | 8 +- .../idoris/pids/TypedPIDMakerIDGenerator.java | 16 +- .../web/v1/PidRedirectController.java | 24 +- .../idoris/rules/logic/Visitor.java | 12 +- .../validation/InheritanceValidator.java | 8 +- .../rules/validation/SyntaxValidator.java | 17 +- .../validation/ValidationPolicyValidator.java | 8 +- .../rules/validation/ValidationVisitor.java | 2 +- .../dao/ITechnologyInterfaceDao.java | 7 +- .../entities/TechnologyInterface.java | 9 +- .../technologyinterfaces/package-info.java | 28 + .../services/TechnologyInterfaceService.java | 55 +- .../web/api/ITechnologyInterfaceApi.java | 33 +- .../TechnologyInterfaceModelAssembler.java | 7 +- .../web/v1/TechnologyInterfaceController.java | 32 +- .../idoris/{ => users}/dao/IUserDao.java | 6 +- .../{domain => users}/entities/ORCiDUser.java | 4 +- .../{domain => users}/entities/TextUser.java | 6 +- .../{domain => users}/entities/User.java | 9 +- .../idoris/users/package-info.java | 28 + src/main/resources/application.properties | 1 - 95 files changed, 1315 insertions(+), 1892 deletions(-) delete mode 100644 event-architecture-design.md delete mode 100644 event-architecture-implementation.md delete mode 100644 event-based-architecture.md delete mode 100644 modulith-migration-plan.md delete mode 100644 openapi.json delete mode 100644 response.md rename src/main/java/edu/kit/datamanager/idoris/{ => attributes}/dao/IAttributeDao.java (86%) rename src/main/java/edu/kit/datamanager/idoris/{domain => attributes}/entities/Attribute.java (87%) rename src/main/java/edu/kit/datamanager/idoris/{domain => attributes}/package-info.java (67%) rename src/main/java/edu/kit/datamanager/idoris/{ => attributes}/services/AttributeService.java (65%) rename src/main/java/edu/kit/datamanager/idoris/{ => attributes}/web/api/IAttributeApi.java (83%) rename src/main/java/edu/kit/datamanager/idoris/{ => attributes}/web/hateoas/AttributeModelAssembler.java (89%) rename src/main/java/edu/kit/datamanager/idoris/{ => attributes}/web/v1/AttributeController.java (68%) create mode 100644 src/main/java/edu/kit/datamanager/idoris/configuration/ETagConfiguration.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/configuration/ETagControllerAdvice.java rename src/main/java/edu/kit/datamanager/idoris/{domain/GenericIDORISEntity.java => core/domain/AdministrativeMetadata.java} (83%) rename src/main/java/edu/kit/datamanager/idoris/{ => core}/domain/VisitableElement.java (98%) rename src/main/java/edu/kit/datamanager/idoris/{ => core/domain}/dao/IGenericRepo.java (78%) rename src/main/java/edu/kit/datamanager/idoris/{ => core}/domain/entities/Reference.java (92%) rename src/main/java/edu/kit/datamanager/idoris/{ => core}/domain/enums/CombinationOptions.java (93%) rename src/main/java/edu/kit/datamanager/idoris/{web => core/domain/exceptions}/ValidationException.java (95%) rename src/main/java/edu/kit/datamanager/idoris/{web => core/domain/exceptions}/ValidationExceptionHandler.java (67%) rename src/main/java/edu/kit/datamanager/idoris/{services => core/domain}/package-info.java (66%) rename src/main/java/edu/kit/datamanager/idoris/{ => core/domain}/web/hateoas/EntityModelAssembler.java (83%) create mode 100644 src/main/java/edu/kit/datamanager/idoris/core/events/EntityPatchedEvent.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityPatchedEvent.java rename src/main/java/edu/kit/datamanager/idoris/{ => datatypes}/dao/IAtomicDataTypeDao.java (87%) rename src/main/java/edu/kit/datamanager/idoris/{ => datatypes}/dao/IDataTypeDao.java (87%) rename src/main/java/edu/kit/datamanager/idoris/{ => datatypes}/dao/ITypeProfileDao.java (87%) rename src/main/java/edu/kit/datamanager/idoris/{domain => datatypes}/entities/AtomicDataType.java (91%) rename src/main/java/edu/kit/datamanager/idoris/{domain => datatypes}/entities/DataType.java (86%) rename src/main/java/edu/kit/datamanager/idoris/{domain => datatypes}/entities/TypeProfile.java (89%) rename src/main/java/edu/kit/datamanager/idoris/{domain => datatypes}/enums/PrimitiveDataTypes.java (97%) rename src/main/java/edu/kit/datamanager/idoris/{web => datatypes}/package-info.java (55%) rename src/main/java/edu/kit/datamanager/idoris/{ => datatypes}/services/AtomicDataTypeService.java (61%) rename src/main/java/edu/kit/datamanager/idoris/{ => datatypes}/services/TypeProfileService.java (74%) rename src/main/java/edu/kit/datamanager/idoris/{ => datatypes}/web/api/IAtomicDataTypeApi.java (81%) rename src/main/java/edu/kit/datamanager/idoris/{ => datatypes}/web/api/ITypeProfileApi.java (85%) rename src/main/java/edu/kit/datamanager/idoris/{ => datatypes}/web/hateoas/AtomicDataTypeModelAssembler.java (89%) rename src/main/java/edu/kit/datamanager/idoris/{ => datatypes}/web/hateoas/DataTypeModelAssembler.java (84%) rename src/main/java/edu/kit/datamanager/idoris/{ => datatypes}/web/hateoas/TypeProfileModelAssembler.java (90%) rename src/main/java/edu/kit/datamanager/idoris/{ => datatypes}/web/v1/AtomicDataTypeController.java (71%) rename src/main/java/edu/kit/datamanager/idoris/{ => datatypes}/web/v1/TypeProfileController.java (83%) rename src/main/java/edu/kit/datamanager/idoris/{ => operations}/dao/IAttributeMappingDao.java (92%) rename src/main/java/edu/kit/datamanager/idoris/{ => operations}/dao/IOperationDao.java (92%) rename src/main/java/edu/kit/datamanager/idoris/{domain => operations}/entities/AttributeMapping.java (90%) rename src/main/java/edu/kit/datamanager/idoris/{domain => operations}/entities/Operation.java (87%) rename src/main/java/edu/kit/datamanager/idoris/{domain => operations}/entities/OperationStep.java (88%) rename src/main/java/edu/kit/datamanager/idoris/{domain => operations/entities}/enums/ExecutionMode.java (91%) create mode 100644 src/main/java/edu/kit/datamanager/idoris/operations/package-info.java rename src/main/java/edu/kit/datamanager/idoris/{ => operations}/services/AttributeMappingService.java (96%) rename src/main/java/edu/kit/datamanager/idoris/{ => operations}/services/OperationService.java (69%) rename src/main/java/edu/kit/datamanager/idoris/{ => operations}/web/api/IOperationApi.java (84%) rename src/main/java/edu/kit/datamanager/idoris/{ => operations}/web/hateoas/OperationModelAssembler.java (88%) rename src/main/java/edu/kit/datamanager/idoris/{ => operations}/web/v1/OperationController.java (75%) rename src/main/java/edu/kit/datamanager/idoris/{ => pids}/web/v1/PidRedirectController.java (91%) rename src/main/java/edu/kit/datamanager/idoris/{ => technologyinterfaces}/dao/ITechnologyInterfaceDao.java (77%) rename src/main/java/edu/kit/datamanager/idoris/{domain => technologyinterfaces}/entities/TechnologyInterface.java (85%) create mode 100644 src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/package-info.java rename src/main/java/edu/kit/datamanager/idoris/{ => technologyinterfaces}/services/TechnologyInterfaceService.java (68%) rename src/main/java/edu/kit/datamanager/idoris/{ => technologyinterfaces}/web/api/ITechnologyInterfaceApi.java (83%) rename src/main/java/edu/kit/datamanager/idoris/{ => technologyinterfaces}/web/hateoas/TechnologyInterfaceModelAssembler.java (87%) rename src/main/java/edu/kit/datamanager/idoris/{ => technologyinterfaces}/web/v1/TechnologyInterfaceController.java (84%) rename src/main/java/edu/kit/datamanager/idoris/{ => users}/dao/IUserDao.java (93%) rename src/main/java/edu/kit/datamanager/idoris/{domain => users}/entities/ORCiDUser.java (94%) rename src/main/java/edu/kit/datamanager/idoris/{domain => users}/entities/TextUser.java (88%) rename src/main/java/edu/kit/datamanager/idoris/{domain => users}/entities/User.java (80%) create mode 100644 src/main/java/edu/kit/datamanager/idoris/users/package-info.java diff --git a/README.md b/README.md index 5746de6..c5dd738 100644 --- a/README.md +++ b/README.md @@ -4,18 +4,6 @@ IDORIS is an **Integrated Data Type and Operations Registry with Inheritance System**. -## Cloning this repository - -This repository includes files that are stored using Git LFS. -Please install Git LFS before cloning this repository. -For more information, see https://git-lfs.com/. -Then execute the following command to clone this repository: - -``` -git lfs install -git lfs clone https://github.com/maximiliani/idoris.git -``` - ## Installation of Neo4j IDORIS relies on the Neo4j graph database. @@ -77,17 +65,6 @@ You can access the IDORIS API at http://localhost:8095/api. IDORIS is built using Spring Boot and follows a modular, event-driven architecture using Spring Modulith. -### Event-Based Architecture - -IDORIS uses an event-based architecture to decouple components and improve maintainability. Key events include: - -- EntityCreatedEvent: Published when a new entity is created -- EntityUpdatedEvent: Published when an entity is updated -- EntityDeletedEvent: Published when an entity is deleted -- PIDGeneratedEvent: Published when a PID is generated for an entity - -For more details, see [event-based-architecture.md](event-based-architecture.md). - ### API Documentation IDORIS provides comprehensive API documentation using OpenAPI/Swagger. You can access the API documentation diff --git a/event-architecture-design.md b/event-architecture-design.md deleted file mode 100644 index c619d2c..0000000 --- a/event-architecture-design.md +++ /dev/null @@ -1,284 +0,0 @@ -# IDORIS Event-Driven Architecture Design - -## Introduction - -This document outlines the design for implementing an event-driven architecture in IDORIS using Spring Modulith. The -design is inspired by the approach used -in [piomin/sample-spring-modulith](https://github.com/piomin/sample-spring-modulith) and aims to support various use -cases including PID record creation, versioning, callbacks for entity changes, importing entities from existing DTRs, -and schema generation. - -## Goals - -- Implement a loosely coupled, event-driven architecture -- Support cross-module communication through domain events -- Enable asynchronous processing of business operations -- Provide a foundation for future microservices extraction -- Support specific use cases mentioned in the requirements - -## Event-Driven Architecture Overview - -The event-driven architecture will be based on Spring Modulith's ApplicationModuleListener mechanism, which provides: - -1. **Module Boundaries**: Clear separation between modules -2. **Event Publication**: Standardized way to publish domain events -3. **Event Subscription**: Type-safe event handling across module boundaries -4. **Transaction Management**: Events can be processed in the same or separate transactions - -## Domain Events - -We will define a hierarchy of domain events: - -``` -DomainEvent (base interface) -├── EntityCreatedEvent -├── EntityUpdatedEvent -├── EntityDeletedEvent -├── PIDGeneratedEvent -├── SchemaGeneratedEvent -└── EntityImportedEvent -``` - -Each event will contain relevant data and metadata about the operation that triggered it. - -## Module Structure - -The event-driven architecture will be organized around the following modules: - -1. **Core Module** - - Event definitions - - Common interfaces - - Base abstractions - -2. **Domain Module** - - Entity definitions - - Domain services - - Domain event publishers - -3. **PID Module** - - PID generation services - - PID record management - - Event listeners for entity lifecycle events - -4. **Versioning Module** - - Version tracking - - Change history - - Event listeners for entity updates - -5. **Schema Module** - - Schema generation - - Schema validation - - Event listeners for schema-related events - -6. **Import Module** - - Entity import services - - DTR connectors - - Event listeners for import-related events - -7. **Notification Module** - - Callback management - - Subscription services - - Event listeners for entity changes - -## Event Flow Examples - -### PID Record Creation - -1. An entity is created or updated in the Domain Module -2. The Domain Module publishes an EntityCreatedEvent or EntityUpdatedEvent -3. The PID Module listens for these events -4. The PID Module generates a PID using TypedPID-Maker -5. The PID Module publishes a PIDGeneratedEvent -6. Other modules can react to the PIDGeneratedEvent - -### Entity Versioning - -1. An entity is updated in the Domain Module -2. The Domain Module publishes an EntityUpdatedEvent -3. The Versioning Module listens for this event -4. The Versioning Module creates a new version record -5. The Versioning Module publishes a VersionCreatedEvent - -### Callbacks for Entity Changes - -1. A client subscribes to changes for a specific entity type -2. The entity is modified in the Domain Module -3. The Domain Module publishes an EntityUpdatedEvent -4. The Notification Module listens for this event -5. The Notification Module checks for subscriptions -6. The Notification Module sends callbacks to subscribers - -## Implementation Approach - -### 1. Spring Modulith Setup - -Add Spring Modulith dependencies to the project: - -```gradle -implementation 'org.springframework.experimental:spring-modulith-starter:1.1.0' -implementation 'org.springframework.experimental:spring-modulith-events:1.1.0' -testImplementation 'org.springframework.experimental:spring-modulith-test:1.1.0' -``` - -### 2. Domain Event Definitions - -Create base domain event interfaces and implementations: - -```java -public interface DomainEvent { - Instant getTimestamp(); - String getEventId(); -} - -public abstract class AbstractDomainEvent implements DomainEvent { - private final Instant timestamp = Instant.now(); - private final String eventId = UUID.randomUUID().toString(); - - @Override - public Instant getTimestamp() { - return timestamp; - } - - @Override - public String getEventId() { - return eventId; - } -} - -public class EntityCreatedEvent extends AbstractDomainEvent { - private final T entity; - - public EntityCreatedEvent(T entity) { - this.entity = entity; - } - - public T getEntity() { - return entity; - } -} -``` - -### 3. Event Publishers - -Implement event publishers in the domain services: - -```java -@Service -public class TypeProfileService { - private final ApplicationEventPublisher eventPublisher; - private final TypeProfileRepository repository; - - @Autowired - public TypeProfileService(ApplicationEventPublisher eventPublisher, TypeProfileRepository repository) { - this.eventPublisher = eventPublisher; - this.repository = repository; - } - - @Transactional - public TypeProfile createTypeProfile(TypeProfile typeProfile) { - TypeProfile saved = repository.save(typeProfile); - eventPublisher.publishEvent(new EntityCreatedEvent<>(saved)); - return saved; - } - - @Transactional - public TypeProfile updateTypeProfile(TypeProfile typeProfile) { - TypeProfile saved = repository.save(typeProfile); - eventPublisher.publishEvent(new EntityUpdatedEvent<>(saved)); - return saved; - } -} -``` - -### 4. Event Listeners - -Implement event listeners in the appropriate modules: - -```java -@Component -public class PIDGenerationEventListener { - private final TypedPIDMakerIDGenerator pidGenerator; - private final ApplicationEventPublisher eventPublisher; - - @Autowired - public PIDGenerationEventListener(TypedPIDMakerIDGenerator pidGenerator, ApplicationEventPublisher eventPublisher) { - this.pidGenerator = pidGenerator; - this.eventPublisher = eventPublisher; - } - - @EventListener - @Transactional - public void handleEntityCreatedEvent(EntityCreatedEvent event) { - GenericIDORISEntity entity = event.getEntity(); - if (entity.getPid() == null || entity.getPid().isEmpty()) { - String pid = pidGenerator.generateId(entity.getClass().getSimpleName(), entity); - entity.setPid(pid); - eventPublisher.publishEvent(new PIDGeneratedEvent<>(entity, pid)); - } - } -} -``` - -### 5. Transaction Management - -Configure transaction boundaries for event processing: - -```java -@Configuration -public class EventConfig { - @Bean - public TransactionalApplicationListener.Factory transactionalApplicationListenerFactory( - TransactionManager transactionManager) { - return TransactionalApplicationListener.factory(transactionManager); - } -} -``` - -## Use Case Implementation Details - -### PID Record Creation - -The PID Module will listen for entity lifecycle events and use the TypedPIDMakerIDGenerator to create or update PID -records. This decouples PID generation from entity creation and allows for retry mechanisms and better error handling. - -### Versioning - -The Versioning Module will maintain a history of entity changes by listening for EntityUpdatedEvents. It will store -previous versions of entities and provide APIs to retrieve and compare versions. - -### Callbacks for Entity Changes - -The Notification Module will allow clients to subscribe to entity changes and receive callbacks when those entities are -modified. It will maintain a registry of subscriptions and use event listeners to trigger notifications. - -### Importing Entities from DTRs - -The Import Module will provide services to import entities from external Digital Twin Registries. It will publish -EntityImportedEvents when entities are imported, allowing other modules to react accordingly. - -### Schema Generation - -The Schema Module will generate and validate schemas for entities. It will listen for entity lifecycle events and -generate or update schemas as needed. It will publish SchemaGeneratedEvents when schemas are created or updated. - -## Testing Strategy - -1. **Unit Tests**: Test individual components within modules -2. **Module Tests**: Test modules in isolation using Spring Modulith test support -3. **Integration Tests**: Test interactions between modules through events -4. **End-to-End Tests**: Verify complete workflows involving multiple modules - -## Implementation Phases - -1. **Phase 1**: Set up Spring Modulith and implement base event infrastructure -2. **Phase 2**: Implement PID record creation and versioning -3. **Phase 3**: Implement callbacks for entity changes -4. **Phase 4**: Implement entity import from DTRs -5. **Phase 5**: Implement schema generation - -## Conclusion - -This event-driven architecture design provides a solid foundation for implementing the required functionality in IDORIS. -It leverages Spring Modulith to create a modular, loosely coupled system that can evolve into microservices in the -future if needed. The event-based approach allows for asynchronous processing, better error handling, and clearer -separation of concerns. \ No newline at end of file diff --git a/event-architecture-implementation.md b/event-architecture-implementation.md deleted file mode 100644 index fc5243d..0000000 --- a/event-architecture-implementation.md +++ /dev/null @@ -1,112 +0,0 @@ -# IDORIS Event-Driven Architecture Implementation - -## Overview - -This document summarizes the implementation of an event-driven architecture in IDORIS using Spring Modulith. The -implementation follows the design outlined in the `event-architecture-design.md` document and provides a foundation for -the various use cases mentioned in the requirements. - -## Implemented Components - -### Core Event Infrastructure - -1. **DomainEvent Interface**: Base interface for all domain events in the system. - - File: `/src/main/java/edu/kit/datamanager/idoris/core/events/DomainEvent.java` - -2. **AbstractDomainEvent Class**: Abstract base class that implements the DomainEvent interface and provides common - functionality. - - File: `/src/main/java/edu/kit/datamanager/idoris/core/events/AbstractDomainEvent.java` - -3. **Entity Lifecycle Events**: - - `EntityCreatedEvent`: Published when a new entity is created. - - File: `/src/main/java/edu/kit/datamanager/idoris/core/events/EntityCreatedEvent.java` - - `EntityUpdatedEvent`: Published when an entity is updated. - - File: `/src/main/java/edu/kit/datamanager/idoris/core/events/EntityUpdatedEvent.java` - - `PIDGeneratedEvent`: Published when a PID is generated for an entity. - - File: `/src/main/java/edu/kit/datamanager/idoris/core/events/PIDGeneratedEvent.java` - -4. **EventPublisherService**: Service for publishing domain events. - - File: `/src/main/java/edu/kit/datamanager/idoris/core/events/EventPublisherService.java` - -### Event Listeners - -1. **PIDGenerationEventListener**: Listens for EntityCreatedEvent and generates PIDs for entities. - - File: `/src/main/java/edu/kit/datamanager/idoris/pids/PIDGenerationEventListener.java` - -### Spring Modulith Configuration - -1. **ModulithConfig**: Configuration class for Spring Modulith. - - File: `/src/main/java/edu/kit/datamanager/idoris/core/config/ModulithConfig.java` - -2. **Module Definitions**: - - Core Module: `/src/main/java/edu/kit/datamanager/idoris/core/package-info.java` - - Domain Module: `/src/main/java/edu/kit/datamanager/idoris/domain/package-info.java` - - PID Module: `/src/main/java/edu/kit/datamanager/idoris/pids/package-info.java` - -### Dependencies - -Added Spring Modulith dependencies to `build.gradle`: - -```gradle -implementation "org.springframework.experimental:spring-modulith-starter:${springModulithVersion}" -implementation "org.springframework.experimental:spring-modulith-events:${springModulithVersion}" -testImplementation "org.springframework.experimental:spring-modulith-test:${springModulithVersion}" -``` - -## How It Works - -1. **Entity Creation/Update**: - - When an entity is created or updated, the service layer publishes an `EntityCreatedEvent` or `EntityUpdatedEvent` - using the `EventPublisherService`. - -2. **PID Generation**: - - The `PIDGenerationEventListener` listens for `EntityCreatedEvent` and generates a PID for the entity if it doesn't - already have one. - - After generating a PID, it publishes a `PIDGeneratedEvent`. - -3. **Event Propagation**: - - Events are propagated across module boundaries using Spring Modulith's event externalization mechanism. - - Event listeners can be executed in a transaction using the `@Transactional` annotation. - -## Next Steps - -### 1. Implement Additional Event Types - -- `EntityDeletedEvent`: For entity deletion -- `SchemaGeneratedEvent`: For schema generation -- `EntityImportedEvent`: For entity import from DTRs - -### 2. Implement Additional Event Listeners - -- **Versioning Listener**: To maintain a history of entity changes -- **Notification Listener**: To send callbacks for entity changes -- **Schema Generation Listener**: To generate and validate schemas -- **Import Listener**: To handle entity import from DTRs - -### 3. Integrate with Existing Services - -- Modify existing service classes to publish events when entities are created, updated, or deleted. -- Update the TypedPIDMakerIDGenerator to work with the event-driven architecture. - -### 4. Create Module-Specific Services - -- **Versioning Service**: For managing entity versions -- **Notification Service**: For managing subscriptions and sending callbacks -- **Schema Service**: For generating and validating schemas -- **Import Service**: For importing entities from DTRs - -### 5. Testing - -- Write unit tests for event classes and listeners -- Write integration tests for event propagation across modules -- Use Spring Modulith's testing support to verify module boundaries - -## Conclusion - -The implemented event-driven architecture provides a solid foundation for the various use cases mentioned in the -requirements. It leverages Spring Modulith to create a modular, loosely coupled system that can evolve into -microservices in the future if needed. The event-based approach allows for asynchronous processing, better error -handling, and clearer separation of concerns. - -By following the next steps outlined above, the architecture can be extended to support all the required functionality -while maintaining the modularity and loose coupling of the system. \ No newline at end of file diff --git a/event-based-architecture.md b/event-based-architecture.md deleted file mode 100644 index 0dfbd9e..0000000 --- a/event-based-architecture.md +++ /dev/null @@ -1,148 +0,0 @@ -# IDORIS Event-Based Architecture - -## Overview - -This document provides an overview of the event-based architecture implemented in IDORIS using Spring Modulith. The -architecture is designed to support various use cases including PID record creation, versioning, callbacks for entity -changes, importing entities from existing DTRs, and schema generation. - -## Architecture Components - -### 1. Domain Events - -Domain events represent significant occurrences within the system. They are used to communicate between modules in a -loosely coupled way. The following domain events have been implemented: - -- **EntityCreatedEvent**: Published when a new entity is created -- **EntityUpdatedEvent**: Published when an entity is updated -- **EntityDeletedEvent**: Published when an entity is deleted -- **PIDGeneratedEvent**: Published when a PID is generated for an entity -- **SchemaGeneratedEvent**: Published when a schema is generated for an entity -- **EntityImportedEvent**: Published when an entity is imported from an external system -- **VersionCreatedEvent**: Published when a new version of an entity is created - -### 2. Event Publisher - -The `EventPublisherService` provides a centralized way to publish domain events. It wraps Spring's -`ApplicationEventPublisher` and provides a more domain-specific API. - -### 3. Event Listeners - -Event listeners subscribe to domain events and perform actions in response. The following listeners have been -implemented: - -- **PIDGenerationEventListener**: Listens for entity creation events and generates PIDs -- **EntityChangeNotifier**: Listens for entity lifecycle events and notifies subscribers - -### 4. Service Layer - -The service layer encapsulates business logic and publishes domain events when entities are created, updated, or -deleted. The following services have been implemented: - -- **TypeProfileService**: Manages TypeProfile entities -- **AtomicDataTypeService**: Manages AtomicDataType entities -- **OperationService**: Manages Operation entities - -### 5. Notification System - -The notification system allows external systems to subscribe to entity changes. It includes: - -- **EntityChangeSubscriber**: Interface for subscribers that want to be notified of entity changes -- **EntityChangeNotifier**: Component that listens for entity change events and notifies subscribers -- **LoggingEntityChangeSubscriber**: Sample implementation that logs entity changes - -## Module Structure - -The application is organized into the following modules: - -1. **Core Module**: Base abstractions, common interfaces, and event infrastructure -2. **Domain Module**: Entity definitions, domain services, and business logic -3. **Repository Module**: Data access objects and persistence -4. **Notification Module**: Callback mechanism for entity changes -5. **Web Module**: REST controllers and API - -## Event Flow Examples - -### PID Record Creation - -1. An entity is created via a service method -2. The service publishes an `EntityCreatedEvent` -3. The `PIDGenerationEventListener` listens for this event -4. The listener generates a PID using the TypedPID-Maker -5. The listener publishes a `PIDGeneratedEvent` - -### Entity Change Notification - -1. An entity is updated via a service method -2. The service publishes an `EntityUpdatedEvent` -3. The `EntityChangeNotifier` listens for this event -4. The notifier calls all subscribers registered for that entity type or specific entity - -## Implementation Details - -### Publishing Events - -```java -// In a service method -public TypeProfile createTypeProfile(TypeProfile typeProfile) { - TypeProfile saved = typeProfileDao.save(typeProfile); - eventPublisher.publishEntityCreated(saved); - return saved; -} -``` - -### Listening for Events - -```java -@Component -public class PIDGenerationEventListener { - @EventListener - @Transactional - public void handleEntityCreatedEvent(EntityCreatedEvent event) { - GenericIDORISEntity entity = event.getEntity(); - // Generate PID and update entity - eventPublisher.publishPIDGenerated(entity, pid); - } -} -``` - -### Subscribing to Entity Changes - -```java -// Register a subscriber -entityChangeNotifier.subscribeToType("TypeProfile", mySubscriber); - -// Implement the subscriber interface -public class MySubscriber implements EntityChangeSubscriber { - @Override - public void onEntityCreated(GenericIDORISEntity entity) { - // Handle entity creation - } - - @Override - public void onEntityUpdated(GenericIDORISEntity entity, Long previousVersion) { - // Handle entity update - } - - @Override - public void onEntityDeleted(GenericIDORISEntity entity) { - // Handle entity deletion - } -} -``` - -## Benefits - -1. **Loose Coupling**: Modules communicate through events, reducing direct dependencies -2. **Extensibility**: New functionality can be added by implementing new event listeners -3. **Testability**: Components can be tested in isolation -4. **Scalability**: Event-based architecture can be scaled horizontally -5. **Maintainability**: Clear separation of concerns makes the codebase easier to understand and maintain - -## Next Steps - -1. **Implement Additional Event Listeners**: For schema generation, versioning, etc. -2. **Add Event Persistence**: Store events for audit and replay purposes -3. **Implement Event Sourcing**: Rebuild entity state from events -4. **Add Event Monitoring**: Monitor event flow and performance -5. **Implement Event Versioning**: Handle changes to event structure over time \ No newline at end of file diff --git a/modulith-migration-plan.md b/modulith-migration-plan.md deleted file mode 100644 index 4237fdc..0000000 --- a/modulith-migration-plan.md +++ /dev/null @@ -1,202 +0,0 @@ -# IDORIS Spring Modulith Migration Plan - -## Introduction - -This document outlines the plan to transform IDORIS into a Spring Modulith application, replacing Spring Data REST with -Spring MVC and Spring HATEOAS for the API. The goal is to enhance maintainability, lower coupling, and increase -development freedom through a well-structured modular architecture. - -## Goals and Benefits - -- **Improved Maintainability**: Clear module boundaries make the codebase easier to understand and maintain -- **Reduced Coupling**: Explicit dependencies between modules prevent unwanted coupling -- **Enhanced Development Freedom**: Teams can work on separate modules with minimal interference -- **Better Testability**: Modules can be tested in isolation -- **Clearer Architecture**: Module boundaries reflect domain concepts - -## Current Architecture Analysis - -IDORIS currently uses: - -- Spring Boot as the application framework -- Spring Data Neo4j for database access -- Spring Data REST for exposing repositories as REST APIs -- Spring HATEOAS for hypermedia support - -The application is structured around these main components: - -- Domain entities (AtomicDataType, TypeProfile, Operation, etc.) -- Data access objects (DAOs) -- Rules system (validation, processing) -- REST controllers and configuration - -## Module Boundaries - -Based on domain-driven design principles and the current codebase, we propose the following modules: - -1. **Core Module** - - Base abstractions and shared utilities - - Common interfaces and base classes - - Cross-cutting concerns - -2. **Domain Module** - - Entity definitions (DataType, TypeProfile, Operation, etc.) - - Domain services and business logic - - Domain events - -3. **Rules Module** - - Rule definitions and processing - - Validation logic - - Rule execution engine - -4. **Repository Module** - - Data access objects - - Neo4j configuration - - Query definitions - -5. **Web Module** - - REST controllers - - Request/response DTOs - - API documentation - -## Package Structure - -The new package structure will follow Spring Modulith conventions: - -``` -edu.kit.datamanager.idoris -├── core -│ ├── config -│ ├── exception -│ └── util -├── domain -│ ├── entities -│ ├── enums -│ ├── events -│ └── services -├── rules -│ ├── api -│ ├── logic -│ ├── processor -│ └── validation -├── repository -│ ├── config -│ ├── dao -│ └── mapping -└── web - ├── api - ├── controller - ├── dto - └── hateoas -``` - -## Migration from Spring Data REST to Spring MVC with HATEOAS - -### Current Implementation - -Spring Data REST automatically exposes repositories as REST endpoints with hypermedia support. This approach has -limitations: - -- Limited control over API design -- Tight coupling between domain model and API representation -- Challenges with complex business logic - -### New Implementation - -1. **Define Controller Layer** - - Create dedicated controllers for each resource type - - Implement CRUD operations using Spring MVC - - Use Spring HATEOAS for hypermedia support - -2. **Create Resource Representations** - - Define DTOs for request/response - - Implement assemblers to convert between entities and DTOs - - Add hypermedia links using LinkBuilder - -3. **Implement Business Logic** - - Move business logic from repositories to service classes - - Ensure proper separation of concerns - - Implement validation in appropriate layers - -## Required Dependencies - -Add the following dependencies to the build.gradle file: - -```gradle -// Spring Modulith -implementation 'org.springframework.experimental:spring-modulith-starter:1.1.0' -testImplementation 'org.springframework.experimental:spring-modulith-test:1.1.0' - -// Already present, keep these -implementation 'org.springframework.boot:spring-boot-starter-web' -implementation 'org.springframework.boot:spring-boot-starter-hateoas' -implementation 'org.springframework.boot:spring-boot-starter-validation' - -// Remove this dependency -// implementation 'org.springframework.boot:spring-boot-starter-data-rest' -``` - -## Implementation Approach - -### Phase 1: Setup Spring Modulith - -1. Add Spring Modulith dependencies -2. Create the new package structure -3. Configure module boundaries -4. Write module documentation - -### Phase 2: Migrate Domain Model - -1. Reorganize domain entities into the new structure -2. Refactor domain services -3. Implement domain events for cross-module communication - -### Phase 3: Implement Repository Layer - -1. Migrate DAOs to the repository module -2. Refactor Neo4j configuration -3. Implement repository services - -### Phase 4: Develop Web API - -1. Create controllers for each resource type -2. Implement DTOs and assemblers -3. Add hypermedia support using Spring HATEOAS -4. Migrate from Spring Data REST endpoints - -### Phase 5: Rules System Migration - -1. Reorganize rules components -2. Implement clean interfaces between modules -3. Ensure rule execution works across module boundaries - -### Phase 6: Testing and Validation - -1. Write module tests using Spring Modulith test support -2. Verify module boundaries and dependencies -3. Test API endpoints -4. Validate hypermedia functionality - -## Testing Strategy - -1. **Unit Tests**: Test individual components within modules -2. **Module Tests**: Test modules in isolation using Spring Modulith test support -3. **Integration Tests**: Test interactions between modules -4. **API Tests**: Verify REST endpoints and hypermedia functionality - -## Timeline - -- **Week 1-2**: Setup Spring Modulith and restructure packages -- **Week 3-4**: Migrate domain model and repository layer -- **Week 5-6**: Implement web API with Spring MVC and HATEOAS -- **Week 7-8**: Migrate rules system and testing - -## Conclusion - -Transforming IDORIS into a Spring Modulith application with Spring MVC and HATEOAS will provide significant benefits in -terms of maintainability, coupling, and development freedom. The migration can be done incrementally, ensuring that the -application remains functional throughout the process. - -The modular architecture will make it easier to understand the system, develop new features, and maintain the codebase -over time. The explicit module boundaries will prevent unwanted dependencies and ensure that the architecture remains -clean as the application evolves. \ No newline at end of file diff --git a/openapi.json b/openapi.json deleted file mode 100644 index 3b4c1d2..0000000 --- a/openapi.json +++ /dev/null @@ -1,655 +0,0 @@ -{ - "openapi": "3.0.1", - "info": { - "title": "Typed PID Maker - RESTful API", - "description": "The Typed PID Maker is a service for creating, updating, obtaining and validating PID record information using Kernel Information Profiles, as defined by the Research Data Alliance.", - "contact": { - "name": "KIT Data Manager Support", - "url": "https://github.com/kit-data-manager", - "email": "support@datamanager.kit.edu" - }, - "license": { - "name": "Apache 2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0.html" - }, - "version": "2.0.0" - }, - "servers": [ - { - "url": "http://localhost:8090", - "description": "Generated server url" - } - ], - "paths": { - "/api/v1/pit/pid/**": { - "get": { - "tags": [ - "typing-rest-resource-impl" - ], - "summary": "Get the record of the given PID.", - "description": "Get the record to the given PID, if it exists. No validation is performed by default.", - "operationId": "getRecord", - "parameters": [ - { - "name": "validation", - "in": "query", - "description": "If true, validation will be run on the resolved PID. On failure, an error will be returned. On success, the PID will be resolved.", - "required": false, - "schema": { - "type": "boolean", - "default": false - } - } - ], - "responses": { - "400": { - "description": "Validation failed. See body for details.", - "content": { - "application/json": {} - } - }, - "500": { - "description": "Server error. See body for details.", - "content": { - "application/json": {} - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": {} - } - }, - "503": { - "description": "Communication to required external service failed.", - "content": { - "application/json": {} - } - }, - "200": { - "description": "Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PIDRecord" - } - }, - "application/vnd.datamanager.pid.simple+json": { - "schema": { - "$ref": "#/components/schemas/SimplePidRecord" - } - } - } - } - } - }, - "put": { - "tags": [ - "typing-rest-resource-impl" - ], - "summary": "Update an existing PID record", - "description": "Update an existing PID record using the record information from the request body.", - "operationId": "updatePID", - "requestBody": { - "description": "The body containing all PID record values as they should be after the update.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PIDRecord" - } - }, - "application/vnd.datamanager.pid.simple+json": { - "schema": { - "$ref": "#/components/schemas/SimplePidRecord" - } - } - }, - "required": true - }, - "responses": { - "400": { - "description": "Validation failed. See body for details.", - "content": { - "application/json": {} - } - }, - "406": { - "description": "Provided input is invalid with regard to the supported accept header (Not acceptable)", - "content": { - "application/json": {} - } - }, - "500": { - "description": "Server error. See body for details.", - "content": { - "application/json": {} - } - }, - "200": { - "description": "Success.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PIDRecord" - } - }, - "application/vnd.datamanager.pid.simple+json": { - "schema": { - "$ref": "#/components/schemas/SimplePidRecord" - } - } - } - }, - "503": { - "description": "Communication to required external service failed.", - "content": { - "application/json": {} - } - }, - "415": { - "description": "Provided input is invalid with regard to the supported content types. (Unsupported Mediatype)", - "content": { - "application/json": {} - } - }, - "428": { - "description": "No ETag given in If-Match header (Precondition required)", - "content": { - "application/json": {} - } - }, - "412": { - "description": "ETag comparison failed (Precondition failed)", - "content": { - "application/json": {} - } - } - } - } - }, - "/api/v1/search": { - "post": { - "tags": [ - "search-controller" - ], - "summary": "Search for resources.", - "description": "Search for resources using the configured Elastic backend. This endpoint serves as direct proxy to the RESTful endpoint of Elastic. In the body, a query document following the Elastic query format has to be provided. Format errors are returned directly from Elastic. This endpoint also supports authentication and authorization. User information obtained via JWT is applied to the provided query as post filter. If a post filter was already provided with the query it will be replaced. Furthermore, this endpoint supports pagination. 'page' and 'size' query parameters are translated into the Elastic attributes 'from' and 'size' automatically, if not already provided within the query by the caller.", - "operationId": "search", - "parameters": [ - { - "name": "proxy", - "in": "query", - "required": true, - "schema": { - "$ref": "#/components/schemas/ProxyExchangeByte[]" - } - }, - { - "name": "page", - "in": "query", - "description": "Zero-based page index (0..N)", - "schema": { - "type": "integer", - "default": 0 - } - }, - { - "name": "size", - "in": "query", - "description": "The size of the page to be returned", - "schema": { - "type": "integer", - "default": 20 - } - }, - { - "name": "sort", - "in": "query", - "description": "Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.", - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/JsonNode" - } - } - }, - "required": true - }, - "responses": { - "400": { - "description": "Bad Request", - "content": { - "application/hal+json": { - "schema": { - "type": "object" - } - } - } - }, - "200": { - "description": "OK", - "content": { - "application/hal+json": { - "schema": { - "type": "object" - } - } - } - } - }, - "security": [ - { - "bearer-jwt": [] - } - ] - } - }, - "/api/v1/pit/pid/": { - "post": { - "tags": [ - "typing-rest-resource-impl" - ], - "summary": "Create a new PID record", - "description": "Create a new PID record using the record information from the request body.", - "operationId": "createPID", - "parameters": [ - { - "name": "dryrun", - "in": "query", - "description": "If true, only validation will be done and no PID will be created. No data will be changed and no services will be notified.", - "required": false, - "schema": { - "type": "boolean", - "default": false - } - } - ], - "requestBody": { - "description": "The body containing all PID record values as they should be in the new PIDs record.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PIDRecord" - } - }, - "application/vnd.datamanager.pid.simple+json": { - "schema": { - "$ref": "#/components/schemas/SimplePidRecord" - } - } - }, - "required": true - }, - "responses": { - "400": { - "description": "Validation failed. See body for details. Contains also the validated record.", - "content": { - "application/json": {} - } - }, - "406": { - "description": "Provided input is invalid with regard to the supported accept header (Not acceptable)", - "content": { - "application/json": {} - } - }, - "500": { - "description": "Server error. See body for details.", - "content": { - "application/json": {} - } - }, - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PIDRecord" - } - }, - "application/vnd.datamanager.pid.simple+json": { - "schema": { - "$ref": "#/components/schemas/SimplePidRecord" - } - } - } - }, - "503": { - "description": "Communication to required external service failed.", - "content": { - "application/json": {} - } - }, - "409": { - "description": "If providing an own PID is enabled 409 indicates, that the PID already exists.", - "content": { - "application/json": {} - } - }, - "415": { - "description": "Provided input is invalid with regard to the supported content types. (Unsupported Mediatype)", - "content": { - "application/json": {} - } - } - } - } - }, - "/api/v1/pit/known-pid": { - "get": { - "tags": [ - "typing-rest-resource-impl" - ], - "summary": "Returns all known PIDs. Supports paging, filtering criteria, and different formats.", - "description": "Returns all known PIDs, limited by the given page size and number. Several filtering criteria are also available. Known PIDs are defined as being stored in a local store. This store is not a cache! Instead, the service remembers every PID which it created (and resolved, depending on the configuration parameter `pit.storage.strategy` of the service) on request. Use the Accept header to adjust the format.", - "operationId": "findAll_1", - "parameters": [ - { - "name": "created_after", - "in": "query", - "description": "The UTC time of the earliest creation timestamp of a returned PID.", - "required": false, - "schema": { - "type": "string", - "format": "date-time" - } - }, - { - "name": "created_before", - "in": "query", - "description": "The UTC time of the latest creation timestamp of a returned PID.", - "required": false, - "schema": { - "type": "string", - "format": "date-time" - } - }, - { - "name": "modified_after", - "in": "query", - "description": "The UTC time of the earliest modification timestamp of a returned PID.", - "required": false, - "schema": { - "type": "string", - "format": "date-time" - } - }, - { - "name": "modified_before", - "in": "query", - "description": "The UTC time of the latest modification timestamp of a returned PID.", - "required": false, - "schema": { - "type": "string", - "format": "date-time" - } - }, - { - "name": "page", - "in": "query", - "description": "Zero-based page index (0..N)", - "schema": { - "type": "integer", - "default": 0 - } - }, - { - "name": "size", - "in": "query", - "description": "The size of the page to be returned", - "schema": { - "type": "integer", - "default": 20 - } - }, - { - "name": "sort", - "in": "query", - "description": "Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.", - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, - { - "name": "Accept", - "in": "header", - "schema": { - "type": "string", - "enum": [ - "application/tabulator+json" - ] - } - } - ], - "responses": { - "400": { - "description": "Bad Request", - "content": { - "application/hal+json": { - "schema": { - "type": "object" - } - } - } - }, - "200": { - "description": "If the request was valid. May return an empty list.", - "content": { - "application/hal+json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/KnownPid" - } - } - }, - "application/tabulator+json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/TabulatorPaginationFormatKnownPid" - }, - { - "$ref": "#/components/schemas/TabulatorPaginationFormat" - } - ] - } - } - } - }, - "500": { - "description": "Server error. See body for details.", - "content": { - "application/json": {}, - "application/tabulator+json": { - "schema": { - "$ref": "#/components/schemas/TabulatorPaginationFormatKnownPid" - } - } - } - } - } - } - }, - "/api/v1/pit/known-pid/**": { - "get": { - "tags": [ - "typing-rest-resource-impl" - ], - "summary": "Returns a PID and its timestamps from the local store, if available.", - "description": "Returns a PID from the local store. This store is not a cache! Instead, the service remembers every PID which it created (and resolved, depending on the configuration parameter `pit.storage.strategy` of the service) on request. If this PID is known, it will be returned together with the timestamps of creation and modification executed on this PID by this service.", - "operationId": "findByPid", - "responses": { - "400": { - "description": "Bad Request", - "content": { - "application/hal+json": { - "schema": { - "type": "object" - } - } - } - }, - "200": { - "description": "If the PID is known and its information was returned.", - "content": { - "application/hal+json": { - "schema": { - "$ref": "#/components/schemas/KnownPid" - } - } - } - }, - "404": { - "description": "If the PID is unknown.", - "content": { - "application/json": {} - } - }, - "500": { - "description": "Server error. See body for details.", - "content": { - "application/json": {} - } - } - } - } - } - }, - "components": { - "schemas": { - "PIDRecord": { - "type": "object", - "properties": { - "pid": { - "type": "string" - }, - "entries": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PIDRecordEntry" - } - } - } - } - }, - "PIDRecordEntry": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "name": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "SimplePair": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "SimplePidRecord": { - "type": "object", - "properties": { - "pid": { - "type": "string" - }, - "record": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SimplePair" - } - } - } - }, - "JsonNode": { - "type": "object" - }, - "ProxyExchangeByte[]": { - "type": "object" - }, - "KnownPid": { - "required": [ - "created", - "modified", - "pid" - ], - "type": "object", - "properties": { - "pid": { - "type": "string" - }, - "created": { - "type": "string", - "format": "date-time" - }, - "modified": { - "type": "string", - "format": "date-time" - } - } - }, - "TabulatorPaginationFormat": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "object" - } - }, - "last_page": { - "type": "integer", - "format": "int32" - } - } - }, - "TabulatorPaginationFormatKnownPid": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/KnownPid" - } - }, - "last_page": { - "type": "integer", - "format": "int32" - } - } - } - }, - "securitySchemes": { - "bearer-jwt": { - "type": "http", - "scheme": "bearer", - "bearerFormat": "JWT" - } - } - } -} \ No newline at end of file diff --git a/response.md b/response.md deleted file mode 100644 index 60977fb..0000000 --- a/response.md +++ /dev/null @@ -1,98 +0,0 @@ -# Response to Fine-Grained Modularity Approach - -## Understanding the Approach - -The approach you've seen in other projects involves creating separate modules for each domain class, where: - -- Each domain class gets its own module -- Implementation details are hidden within the module -- External and internal interfaces are provided via Spring @Services -- Some projects even include API endpoints within these modules - -## Comparison with Our Proposed Structure - -Our migration plan proposes a more coarse-grained modular structure with 5 main modules: - -1. **Core Module**: Base abstractions and utilities -2. **Domain Module**: All entity definitions and business logic -3. **Rules Module**: Rule definitions and processing -4. **Repository Module**: Data access objects and persistence -5. **Web Module**: REST controllers and API concerns - -## Analysis of Fine-Grained Approach - -### Potential Benefits - -- **Maximum Encapsulation**: Each domain concept is fully isolated -- **Clear Ownership**: Teams can own specific domain modules -- **Focused Development**: Changes to one domain concept don't affect others -- **Independent Deployment**: Theoretically, modules could be deployed separately - -### Significant Drawbacks - -- **Excessive Fragmentation**: For IDORIS with entities like TypeProfile, AtomicDataType, Operation, etc., this would - create many small modules -- **Increased Complexity**: Managing dependencies between numerous small modules becomes challenging -- **Overhead**: Each module requires its own configuration, build setup, etc. -- **Cross-Cutting Concerns**: Difficult to handle concerns that span multiple domain classes -- **Tight Coupling**: Despite the separation, domain classes often have inherent relationships (e.g., TypeProfile - inherits from DataType) -- **API Endpoint Location**: As you noted, placing API endpoints in domain modules violates separation of concerns - -## Recommendation for IDORIS - -I recommend staying with the more balanced approach outlined in our migration plan for several reasons: - -1. **Domain Cohesion**: The entities in IDORIS are closely related (inheritance relationships, references between - entities). Keeping them in a single domain module maintains this cohesion while still providing clear boundaries. - -2. **Appropriate Separation**: The 5-module structure already provides good separation of concerns without excessive - fragmentation: - - Domain logic is separated from persistence - - Web concerns are isolated from business logic - - Rules system has its own boundary - -3. **Practical Maintainability**: A moderate number of well-defined modules is easier to maintain than dozens of tiny - modules. - -4. **Alignment with Spring Modulith**: The Spring Modulith approach generally favors "right-sized" modules that - represent meaningful business capabilities, not individual entities. - -## Alternative Approach - -If you want more fine-grained structure without the drawbacks of separate modules for each entity, consider: - -1. **Sub-packages within modules**: Within the domain module, create clear sub-packages for related entities: - ``` - domain - ├── datatype - │ ├── AtomicDataType.java - │ ├── DataType.java - │ ├── DataTypeService.java - │ └── internal/ - ├── typeprofile - │ ├── TypeProfile.java - │ ├── TypeProfileService.java - │ └── internal/ - └── operation - ├── Operation.java - ├── OperationService.java - └── internal/ - ``` - -2. **Package-private visibility**: Use package-private methods and classes to hide implementation details while keeping - related code in the same module. - -3. **Clear interfaces**: Define public interfaces for each domain concept that other packages can depend on. - -This approach gives you many of the benefits of fine-grained modularity without the overhead of separate build modules. - -## Conclusion - -While the approach of separate modules for each domain class offers maximum isolation, it introduces complexity that -likely outweighs its benefits for IDORIS. The proposed 5-module structure in our migration plan provides a good balance -between separation of concerns and practical maintainability. - -I agree with your assessment that placing API endpoints in domain modules is not ideal, as it violates the separation -between domain logic and web concerns. Keeping controllers in a dedicated web module, as outlined in our plan, is a -better approach. \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/IAttributeDao.java b/src/main/java/edu/kit/datamanager/idoris/attributes/dao/IAttributeDao.java similarity index 86% rename from src/main/java/edu/kit/datamanager/idoris/dao/IAttributeDao.java rename to src/main/java/edu/kit/datamanager/idoris/attributes/dao/IAttributeDao.java index 8e0627b..9ce8251 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/IAttributeDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/attributes/dao/IAttributeDao.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.dao; +package edu.kit.datamanager.idoris.attributes.dao; -import edu.kit.datamanager.idoris.domain.entities.Attribute; +import edu.kit.datamanager.idoris.attributes.entities.Attribute; +import edu.kit.datamanager.idoris.core.domain.dao.IGenericRepo; import org.springframework.data.neo4j.repository.query.Query; public interface IAttributeDao extends IGenericRepo { @@ -30,4 +31,4 @@ public interface IAttributeDao extends IGenericRepo { " DETACH DELETE n") // @RestResource(exported = false) void deleteOrphanedAttributes(); -} +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/Attribute.java b/src/main/java/edu/kit/datamanager/idoris/attributes/entities/Attribute.java similarity index 87% rename from src/main/java/edu/kit/datamanager/idoris/domain/entities/Attribute.java rename to src/main/java/edu/kit/datamanager/idoris/attributes/entities/Attribute.java index 1f23fe2..7be60f1 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/Attribute.java +++ b/src/main/java/edu/kit/datamanager/idoris/attributes/entities/Attribute.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.entities; +package edu.kit.datamanager.idoris.attributes.entities; -import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +import edu.kit.datamanager.idoris.datatypes.entities.DataType; import edu.kit.datamanager.idoris.rules.logic.RuleOutput; import edu.kit.datamanager.idoris.rules.logic.Visitor; import jakarta.validation.constraints.NotNull; @@ -32,7 +33,7 @@ @RequiredArgsConstructor @Getter @Setter -public class Attribute extends GenericIDORISEntity { +public class Attribute extends AdministrativeMetadata { private String defaultValue; private String constantValue; private Integer lowerBoundCardinality = 0; @@ -49,4 +50,4 @@ public class Attribute extends GenericIDORISEntity { protected > T accept(Visitor visitor, Object... args) { return visitor.visit(this, args); } -} +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/package-info.java b/src/main/java/edu/kit/datamanager/idoris/attributes/package-info.java similarity index 67% rename from src/main/java/edu/kit/datamanager/idoris/domain/package-info.java rename to src/main/java/edu/kit/datamanager/idoris/attributes/package-info.java index 635b504..8c28bfa 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/package-info.java +++ b/src/main/java/edu/kit/datamanager/idoris/attributes/package-info.java @@ -15,15 +15,14 @@ */ /** - * Domain module for IDORIS. - * This module contains entity definitions, domain services, and business logic. - * It is responsible for the core domain concepts and their relationships. + * Attributes module for IDORIS. + * This module contains entity definitions, domain services, and business logic related to attributes. + * It is responsible for managing attributes and attribute mappings. * - *

The domain module depends on the core module for base abstractions and interfaces. - * It should not depend on infrastructure concerns like repositories or web controllers.

+ *

The Attributes module depends on the core module for base abstractions and interfaces.

*/ @org.springframework.modulith.ApplicationModule( - displayName = "IDORIS Domain", + displayName = "IDORIS Attributes", allowedDependencies = {"core"} ) -package edu.kit.datamanager.idoris.domain; \ No newline at end of file +package edu.kit.datamanager.idoris.attributes; \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/services/AttributeService.java b/src/main/java/edu/kit/datamanager/idoris/attributes/services/AttributeService.java similarity index 65% rename from src/main/java/edu/kit/datamanager/idoris/services/AttributeService.java rename to src/main/java/edu/kit/datamanager/idoris/attributes/services/AttributeService.java index fda9a77..ecfcb8f 100644 --- a/src/main/java/edu/kit/datamanager/idoris/services/AttributeService.java +++ b/src/main/java/edu/kit/datamanager/idoris/attributes/services/AttributeService.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.services; +package edu.kit.datamanager.idoris.attributes.services; +import edu.kit.datamanager.idoris.attributes.dao.IAttributeDao; +import edu.kit.datamanager.idoris.attributes.entities.Attribute; import edu.kit.datamanager.idoris.core.events.EventPublisherService; -import edu.kit.datamanager.idoris.dao.IAttributeDao; -import edu.kit.datamanager.idoris.domain.entities.Attribute; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -141,4 +141,60 @@ public void deleteOrphanedAttributes() { attributeDao.deleteOrphanedAttributes(); log.info("Deleted orphaned Attributes"); } -} \ No newline at end of file + + /** + * Partially updates an existing Attribute entity. + * + * @param pid the PID of the Attribute to patch + * @param attributePatch the partial Attribute entity with fields to update + * @return the patched Attribute entity + * @throws IllegalArgumentException if the Attribute does not exist + */ + @Transactional + public Attribute patchAttribute(String pid, Attribute attributePatch) { + log.debug("Patching Attribute with PID: {}, patch: {}", pid, attributePatch); + if (pid == null || pid.isEmpty()) { + throw new IllegalArgumentException("Attribute PID cannot be null or empty"); + } + + // Get the current entity + Attribute existing = attributeDao.findById(pid) + .orElseThrow(() -> new IllegalArgumentException("Attribute not found with PID: " + pid)); + Long previousVersion = existing.getVersion(); + + // Apply non-null fields from the patch to the existing entity + if (attributePatch.getName() != null) { + existing.setName(attributePatch.getName()); + } + if (attributePatch.getDescription() != null) { + existing.setDescription(attributePatch.getDescription()); + } + if (attributePatch.getDefaultValue() != null) { + existing.setDefaultValue(attributePatch.getDefaultValue()); + } + if (attributePatch.getConstantValue() != null) { + existing.setConstantValue(attributePatch.getConstantValue()); + } + if (attributePatch.getLowerBoundCardinality() != null) { + existing.setLowerBoundCardinality(attributePatch.getLowerBoundCardinality()); + } + if (attributePatch.getUpperBoundCardinality() != null) { + existing.setUpperBoundCardinality(attributePatch.getUpperBoundCardinality()); + } + if (attributePatch.getDataType() != null) { + existing.setDataType(attributePatch.getDataType()); + } + if (attributePatch.getOverride() != null) { + existing.setOverride(attributePatch.getOverride()); + } + + // Save the updated entity + Attribute saved = attributeDao.save(existing); + + // Publish the patched event + eventPublisher.publishEntityPatched(saved, previousVersion); + + log.info("Patched Attribute with PID: {}", saved.getPid()); + return saved; + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/web/api/IAttributeApi.java b/src/main/java/edu/kit/datamanager/idoris/attributes/web/api/IAttributeApi.java similarity index 83% rename from src/main/java/edu/kit/datamanager/idoris/web/api/IAttributeApi.java rename to src/main/java/edu/kit/datamanager/idoris/attributes/web/api/IAttributeApi.java index ec1bafc..2c51b64 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/api/IAttributeApi.java +++ b/src/main/java/edu/kit/datamanager/idoris/attributes/web/api/IAttributeApi.java @@ -14,10 +14,10 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.web.api; +package edu.kit.datamanager.idoris.attributes.web.api; -import edu.kit.datamanager.idoris.domain.entities.Attribute; -import edu.kit.datamanager.idoris.domain.entities.DataType; +import edu.kit.datamanager.idoris.attributes.entities.Attribute; +import edu.kit.datamanager.idoris.datatypes.entities.DataType; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -176,4 +176,29 @@ ResponseEntity deleteAttribute( } ) ResponseEntity deleteOrphanedAttributes(); -} \ No newline at end of file + + /** + * Partially updates an Attribute entity. + * + * @param pid the PID of the Attribute to patch + * @param attributePatch the partial Attribute entity with fields to update + * @return the patched Attribute entity + */ + @PatchMapping("/{pid}") + @Operation( + summary = "Partially update an Attribute", + description = "Updates specific fields of an existing Attribute entity", + responses = { + @ApiResponse(responseCode = "200", description = "Attribute patched", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = Attribute.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "404", description = "Attribute not found") + } + ) + ResponseEntity> patchAttribute( + @Parameter(description = "PID of the Attribute", required = true) + @PathVariable String pid, + @Parameter(description = "Partial Attribute with fields to update", required = true) + @RequestBody Attribute attributePatch); +} diff --git a/src/main/java/edu/kit/datamanager/idoris/web/hateoas/AttributeModelAssembler.java b/src/main/java/edu/kit/datamanager/idoris/attributes/web/hateoas/AttributeModelAssembler.java similarity index 89% rename from src/main/java/edu/kit/datamanager/idoris/web/hateoas/AttributeModelAssembler.java rename to src/main/java/edu/kit/datamanager/idoris/attributes/web/hateoas/AttributeModelAssembler.java index a80b425..93bc7f3 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/hateoas/AttributeModelAssembler.java +++ b/src/main/java/edu/kit/datamanager/idoris/attributes/web/hateoas/AttributeModelAssembler.java @@ -14,10 +14,11 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.web.hateoas; +package edu.kit.datamanager.idoris.attributes.web.hateoas; -import edu.kit.datamanager.idoris.domain.entities.Attribute; -import edu.kit.datamanager.idoris.web.v1.AttributeController; +import edu.kit.datamanager.idoris.attributes.entities.Attribute; +import edu.kit.datamanager.idoris.attributes.web.v1.AttributeController; +import edu.kit.datamanager.idoris.core.domain.web.hateoas.EntityModelAssembler; import org.springframework.hateoas.EntityModel; import org.springframework.stereotype.Component; diff --git a/src/main/java/edu/kit/datamanager/idoris/web/v1/AttributeController.java b/src/main/java/edu/kit/datamanager/idoris/attributes/web/v1/AttributeController.java similarity index 68% rename from src/main/java/edu/kit/datamanager/idoris/web/v1/AttributeController.java rename to src/main/java/edu/kit/datamanager/idoris/attributes/web/v1/AttributeController.java index bcb6584..5a8d051 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/v1/AttributeController.java +++ b/src/main/java/edu/kit/datamanager/idoris/attributes/web/v1/AttributeController.java @@ -14,15 +14,14 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.web.v1; - -import edu.kit.datamanager.idoris.domain.entities.Attribute; -import edu.kit.datamanager.idoris.domain.entities.DataType; -import edu.kit.datamanager.idoris.services.AttributeService; -import edu.kit.datamanager.idoris.web.api.IAttributeApi; -import edu.kit.datamanager.idoris.web.hateoas.AttributeModelAssembler; -import edu.kit.datamanager.idoris.web.hateoas.DataTypeModelAssembler; -import org.springframework.beans.factory.annotation.Autowired; +package edu.kit.datamanager.idoris.attributes.web.v1; + +import edu.kit.datamanager.idoris.attributes.entities.Attribute; +import edu.kit.datamanager.idoris.attributes.services.AttributeService; +import edu.kit.datamanager.idoris.attributes.web.api.IAttributeApi; +import edu.kit.datamanager.idoris.attributes.web.hateoas.AttributeModelAssembler; +import edu.kit.datamanager.idoris.datatypes.entities.DataType; +import edu.kit.datamanager.idoris.datatypes.web.hateoas.DataTypeModelAssembler; import org.springframework.hateoas.CollectionModel; import org.springframework.hateoas.EntityModel; import org.springframework.http.HttpStatus; @@ -32,7 +31,6 @@ import java.util.List; import java.util.stream.Collectors; -import java.util.stream.StreamSupport; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; @@ -45,21 +43,22 @@ @RequestMapping("/v1/attributes") public class AttributeController implements IAttributeApi { - @Autowired - private AttributeService attributeService; + private final AttributeService attributeService; + private final AttributeModelAssembler attributeModelAssembler; + private final DataTypeModelAssembler dataTypeModelAssembler; - @Autowired - private AttributeModelAssembler attributeModelAssembler; - - @Autowired - private DataTypeModelAssembler dataTypeModelAssembler; + public AttributeController(AttributeService attributeService, AttributeModelAssembler attributeModelAssembler, DataTypeModelAssembler dataTypeModelAssembler) { + this.attributeService = attributeService; + this.attributeModelAssembler = attributeModelAssembler; + this.dataTypeModelAssembler = dataTypeModelAssembler; + } /** * {@inheritDoc} */ @Override public ResponseEntity>> getAllAttributes() { - List> attributes = StreamSupport.stream(attributeService.getAllAttributes().spliterator(), false) + List> attributes = attributeService.getAllAttributes().stream() .map(attributeModelAssembler::toModel) .collect(Collectors.toList()); @@ -88,7 +87,7 @@ public ResponseEntity> getAttribute(String pid) { @Override public ResponseEntity> getDataType(String pid) { return attributeService.getAttribute(pid) - .map(attribute -> attribute.getDataType()) + .map(Attribute::getDataType) .map(dataTypeModelAssembler::toModel) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); @@ -109,7 +108,7 @@ public ResponseEntity> createAttribute(Attribute attribut */ @Override public ResponseEntity> updateAttribute(String pid, Attribute attribute) { - if (!attributeService.getAttribute(pid).isPresent()) { + if (attributeService.getAttribute(pid).isEmpty()) { return ResponseEntity.notFound().build(); } @@ -124,7 +123,7 @@ public ResponseEntity> updateAttribute(String pid, Attrib */ @Override public ResponseEntity deleteAttribute(String pid) { - if (!attributeService.getAttribute(pid).isPresent()) { + if (attributeService.getAttribute(pid).isEmpty()) { return ResponseEntity.notFound().build(); } @@ -140,4 +139,18 @@ public ResponseEntity deleteOrphanedAttributes() { attributeService.deleteOrphanedAttributes(); return ResponseEntity.noContent().build(); } + + /** + * {@inheritDoc} + */ + @Override + public ResponseEntity> patchAttribute(String pid, Attribute attributePatch) { + if (attributeService.getAttribute(pid).isEmpty()) { + return ResponseEntity.notFound().build(); + } + + Attribute patchedAttribute = attributeService.patchAttribute(pid, attributePatch); + EntityModel entityModel = attributeModelAssembler.toModel(patchedAttribute); + return ResponseEntity.ok(entityModel); + } } diff --git a/src/main/java/edu/kit/datamanager/idoris/configuration/ETagConfiguration.java b/src/main/java/edu/kit/datamanager/idoris/configuration/ETagConfiguration.java new file mode 100644 index 0000000..fd4aa0c --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/configuration/ETagConfiguration.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.configuration; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.servlet.HandlerExceptionResolver; + +import java.io.IOException; +import java.util.Set; + +/** + * Configuration for ETag support. + * This configuration adds an ETag filter to the application that will generate ETags for responses + * and validate If-Match headers for non-idempotent operations (PUT, PATCH, DELETE). + */ +@Configuration +public class ETagConfiguration { + + private static final Set NON_IDEMPOTENT_METHODS = Set.of( + HttpMethod.PUT.name(), + HttpMethod.PATCH.name(), + HttpMethod.DELETE.name() + ); + + /** + * Creates an ETag filter bean. + * + * @param handlerExceptionResolver the handler exception resolver + * @return the ETag filter + */ + @Bean + public OncePerRequestFilter etagFilter(HandlerExceptionResolver handlerExceptionResolver) { + return new OncePerRequestFilter() { + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + // Check if the request is for a non-idempotent operation + if (NON_IDEMPOTENT_METHODS.contains(request.getMethod())) { + // Check if the If-Match header is present + String ifMatch = request.getHeader(HttpHeaders.IF_MATCH); + if (ifMatch == null || ifMatch.isEmpty()) { + response.setStatus(HttpStatus.PRECONDITION_REQUIRED.value()); + response.getWriter().write("If-Match header is required for non-idempotent operations"); + return; + } + } + + // Continue with the filter chain + filterChain.doFilter(request, response); + } + }; + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/configuration/ETagControllerAdvice.java b/src/main/java/edu/kit/datamanager/idoris/configuration/ETagControllerAdvice.java new file mode 100644 index 0000000..e2c88e6 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/configuration/ETagControllerAdvice.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.configuration; + +import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.core.MethodParameter; +import org.springframework.hateoas.EntityModel; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +/** + * Controller advice for ETag support. + * This advice adds ETag headers to responses that contain AdministrativeMetadata entities. + */ +@ControllerAdvice +public class ETagControllerAdvice implements ResponseBodyAdvice { + + @Override + public boolean supports(MethodParameter returnType, Class> converterType) { + // Support all response types + return true; + } + + @Override + public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, + Class> selectedConverterType, + ServerHttpRequest request, ServerHttpResponse response) { + + // Extract the entity from the response body + Object entity = body; + if (body instanceof EntityModel) { + entity = ((EntityModel) body).getContent(); + } + + // If the entity is an AdministrativeMetadata, add an ETag header + if (entity instanceof AdministrativeMetadata metadata) { + String etag = "\"" + metadata.getVersion() + "\""; + response.getHeaders().set(HttpHeaders.ETAG, etag); + + // Store the entity in the request attributes for the ETag filter + if (request instanceof ServletServerHttpRequest && response instanceof ServletServerHttpResponse) { + HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest(); + servletRequest.setAttribute("entity", metadata); + } + + // Check if this is a conditional request (If-Match header is present) + String ifMatch = request.getHeaders().getFirst(HttpHeaders.IF_MATCH); + if (ifMatch != null && !ifMatch.isEmpty()) { + // If the ETag doesn't match, return 412 Precondition Failed + if (!ifMatch.equals(etag) && !ifMatch.equals("*")) { + response.setStatusCode(HttpStatus.PRECONDITION_FAILED); + return null; + } + } + } + + return body; + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/configuration/HateoasConfig.java b/src/main/java/edu/kit/datamanager/idoris/configuration/HateoasConfig.java index 3b71795..384a984 100644 --- a/src/main/java/edu/kit/datamanager/idoris/configuration/HateoasConfig.java +++ b/src/main/java/edu/kit/datamanager/idoris/configuration/HateoasConfig.java @@ -16,11 +16,11 @@ package edu.kit.datamanager.idoris.configuration; -import edu.kit.datamanager.idoris.domain.entities.AtomicDataType; -import edu.kit.datamanager.idoris.domain.entities.DataType; -import edu.kit.datamanager.idoris.domain.entities.TypeProfile; -import edu.kit.datamanager.idoris.web.v1.AtomicDataTypeController; -import edu.kit.datamanager.idoris.web.v1.TypeProfileController; +import edu.kit.datamanager.idoris.datatypes.entities.AtomicDataType; +import edu.kit.datamanager.idoris.datatypes.entities.DataType; +import edu.kit.datamanager.idoris.datatypes.entities.TypeProfile; +import edu.kit.datamanager.idoris.datatypes.web.v1.AtomicDataTypeController; +import edu.kit.datamanager.idoris.datatypes.web.v1.TypeProfileController; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.hateoas.EntityModel; @@ -133,4 +133,4 @@ public EntityModel process(EntityModel model) { // return model; // }; // } -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/idoris/configuration/TypedPIDMakerConfig.java b/src/main/java/edu/kit/datamanager/idoris/configuration/TypedPIDMakerConfig.java index 8990149..e5986a5 100644 --- a/src/main/java/edu/kit/datamanager/idoris/configuration/TypedPIDMakerConfig.java +++ b/src/main/java/edu/kit/datamanager/idoris/configuration/TypedPIDMakerConfig.java @@ -38,14 +38,14 @@ @Setter public class TypedPIDMakerConfig { /** - * Put metadata of the GenericIDORISEntity into the PID record. + * Put metadata of the AdministrativeMetadata into the PID record. * * @see edu.kit.datamanager.idoris.domain.GenericIDORISEntity */ private boolean meaningfulPIDRecords = true; /** - * Update existing PID records with the latest metadata from the GenericIDORISEntity. + * Update existing PID records with the latest metadata from the AdministrativeMetadata. * If set to false, existing PID records will not be updated, * but new records will still be created with the latest metadata. */ diff --git a/src/main/java/edu/kit/datamanager/idoris/configuration/ValidationConfig.java b/src/main/java/edu/kit/datamanager/idoris/configuration/ValidationConfig.java index e3e91c7..47d1c74 100644 --- a/src/main/java/edu/kit/datamanager/idoris/configuration/ValidationConfig.java +++ b/src/main/java/edu/kit/datamanager/idoris/configuration/ValidationConfig.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.configuration; -import edu.kit.datamanager.idoris.domain.VisitableElement; +import edu.kit.datamanager.idoris.core.domain.VisitableElement; import edu.kit.datamanager.idoris.rules.logic.OutputMessage; import edu.kit.datamanager.idoris.rules.logic.RuleService; import edu.kit.datamanager.idoris.rules.logic.RuleTask; @@ -119,4 +119,4 @@ private void convertToSpringErrors(ValidationResult result, org.springframework. }); } } -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/GenericIDORISEntity.java b/src/main/java/edu/kit/datamanager/idoris/core/domain/AdministrativeMetadata.java similarity index 83% rename from src/main/java/edu/kit/datamanager/idoris/domain/GenericIDORISEntity.java rename to src/main/java/edu/kit/datamanager/idoris/core/domain/AdministrativeMetadata.java index ec29207..8475d25 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/GenericIDORISEntity.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/domain/AdministrativeMetadata.java @@ -14,16 +14,17 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain; +package edu.kit.datamanager.idoris.core.domain; -import edu.kit.datamanager.idoris.domain.entities.Reference; -import edu.kit.datamanager.idoris.domain.entities.User; +import edu.kit.datamanager.idoris.core.domain.entities.Reference; import edu.kit.datamanager.idoris.pids.ConfigurablePIDGenerator; +import edu.kit.datamanager.idoris.users.entities.User; import lombok.*; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.annotation.Version; import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Relationship; import java.io.Serializable; @@ -35,7 +36,8 @@ @EqualsAndHashCode(callSuper = true) @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PROTECTED) -public abstract class GenericIDORISEntity extends VisitableElement implements Serializable { +@Node("IDORIS") +public abstract class AdministrativeMetadata extends VisitableElement implements Serializable { @GeneratedValue(ConfigurablePIDGenerator.class) String pid; @@ -58,4 +60,4 @@ public abstract class GenericIDORISEntity extends VisitableElement implements Se Set contributors; Set references; -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/VisitableElement.java b/src/main/java/edu/kit/datamanager/idoris/core/domain/VisitableElement.java similarity index 98% rename from src/main/java/edu/kit/datamanager/idoris/domain/VisitableElement.java rename to src/main/java/edu/kit/datamanager/idoris/core/domain/VisitableElement.java index e898802..ef3e0fc 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/VisitableElement.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/domain/VisitableElement.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain; +package edu.kit.datamanager.idoris.core.domain; import com.fasterxml.jackson.annotation.JsonIgnore; import edu.kit.datamanager.idoris.rules.logic.RuleOutput; @@ -75,4 +75,4 @@ public void clearVisitedBy(Visitor visitor) { public String getId() { return internalId; } -} +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/IGenericRepo.java b/src/main/java/edu/kit/datamanager/idoris/core/domain/dao/IGenericRepo.java similarity index 78% rename from src/main/java/edu/kit/datamanager/idoris/dao/IGenericRepo.java rename to src/main/java/edu/kit/datamanager/idoris/core/domain/dao/IGenericRepo.java index e195d5b..7f339ed 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/IGenericRepo.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/domain/dao/IGenericRepo.java @@ -14,17 +14,17 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.dao; +package edu.kit.datamanager.idoris.core.domain.dao; -import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.repository.ListCrudRepository; import org.springframework.data.repository.PagingAndSortingRepository; -public interface IGenericRepo extends Neo4jRepository, ListCrudRepository, PagingAndSortingRepository { +public interface IGenericRepo extends Neo4jRepository, ListCrudRepository, PagingAndSortingRepository { // This interface serves as a marker for generic repositories. // It can be extended by specific repositories to inherit common methods. // Additional methods can be defined here if needed. T findByPid(String pid); -} +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/Reference.java b/src/main/java/edu/kit/datamanager/idoris/core/domain/entities/Reference.java similarity index 92% rename from src/main/java/edu/kit/datamanager/idoris/domain/entities/Reference.java rename to src/main/java/edu/kit/datamanager/idoris/core/domain/entities/Reference.java index b16cef5..20d9fd1 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/Reference.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/domain/entities/Reference.java @@ -14,8 +14,8 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.entities; +package edu.kit.datamanager.idoris.core.domain.entities; public record Reference(String relationType, String targetPID) { -} +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/enums/CombinationOptions.java b/src/main/java/edu/kit/datamanager/idoris/core/domain/enums/CombinationOptions.java similarity index 93% rename from src/main/java/edu/kit/datamanager/idoris/domain/enums/CombinationOptions.java rename to src/main/java/edu/kit/datamanager/idoris/core/domain/enums/CombinationOptions.java index c79ffdd..3e42555 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/enums/CombinationOptions.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/domain/enums/CombinationOptions.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.enums; +package edu.kit.datamanager.idoris.core.domain.enums; import lombok.AllArgsConstructor; import lombok.Getter; @@ -27,4 +27,4 @@ public enum CombinationOptions { ANY("any"), ALL("all"); private final String jsonName; -} +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/web/ValidationException.java b/src/main/java/edu/kit/datamanager/idoris/core/domain/exceptions/ValidationException.java similarity index 95% rename from src/main/java/edu/kit/datamanager/idoris/web/ValidationException.java rename to src/main/java/edu/kit/datamanager/idoris/core/domain/exceptions/ValidationException.java index d49e78b..a513269 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/ValidationException.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/domain/exceptions/ValidationException.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.web; +package edu.kit.datamanager.idoris.core.domain.exceptions; import edu.kit.datamanager.idoris.rules.validation.ValidationResult; import lombok.Getter; @@ -30,5 +30,4 @@ public ValidationException(String message, ValidationResult validationResult) { super(message); this.validationResult = validationResult; } - -} +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/web/ValidationExceptionHandler.java b/src/main/java/edu/kit/datamanager/idoris/core/domain/exceptions/ValidationExceptionHandler.java similarity index 67% rename from src/main/java/edu/kit/datamanager/idoris/web/ValidationExceptionHandler.java rename to src/main/java/edu/kit/datamanager/idoris/core/domain/exceptions/ValidationExceptionHandler.java index 58e2fdb..3213b08 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/ValidationExceptionHandler.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/domain/exceptions/ValidationExceptionHandler.java @@ -14,21 +14,7 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.web;/* - * Copyright (c) 2025 Karlsruhe Institute of Technology - * - * 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 edu.kit.datamanager.idoris.core.domain.exceptions; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; @@ -49,4 +35,4 @@ public ResponseEntity> handleValidationException(ValidationE return ResponseEntity.badRequest().body(response); } -} +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/services/package-info.java b/src/main/java/edu/kit/datamanager/idoris/core/domain/package-info.java similarity index 66% rename from src/main/java/edu/kit/datamanager/idoris/services/package-info.java rename to src/main/java/edu/kit/datamanager/idoris/core/domain/package-info.java index bcf921c..0c38a02 100644 --- a/src/main/java/edu/kit/datamanager/idoris/services/package-info.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/domain/package-info.java @@ -15,8 +15,11 @@ */ /** - * Service layer for the domain module. - * This package contains service classes that implement business logic for domain entities. - * These services use repositories for data access and publish domain events when entities change. + * Core Domain module for IDORIS. + * This module contains base domain classes and interfaces used by multiple domain aggregates. + * It provides the foundation for the domain model. */ -package edu.kit.datamanager.idoris.services; \ No newline at end of file +@org.springframework.modulith.ApplicationModule( + displayName = "IDORIS Core Domain" +) +package edu.kit.datamanager.idoris.core.domain; \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/web/hateoas/EntityModelAssembler.java b/src/main/java/edu/kit/datamanager/idoris/core/domain/web/hateoas/EntityModelAssembler.java similarity index 83% rename from src/main/java/edu/kit/datamanager/idoris/web/hateoas/EntityModelAssembler.java rename to src/main/java/edu/kit/datamanager/idoris/core/domain/web/hateoas/EntityModelAssembler.java index 5842a8b..2cba073 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/hateoas/EntityModelAssembler.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/domain/web/hateoas/EntityModelAssembler.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.web.hateoas; +package edu.kit.datamanager.idoris.core.domain.web.hateoas; -import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.server.RepresentationModelAssembler; @@ -24,9 +24,9 @@ * Base interface for entity model assemblers. * This interface defines the contract for assemblers that convert entities to EntityModel objects with HATEOAS links. * - * @param the entity type, must extend GenericIDORISEntity + * @param the entity type, must extend AdministrativeMetadata */ -public interface EntityModelAssembler extends RepresentationModelAssembler> { +public interface EntityModelAssembler extends RepresentationModelAssembler> { /** * Converts an entity to an EntityModel with HATEOAS links. diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/EntityCreatedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityCreatedEvent.java index 04587e6..009255f 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/events/EntityCreatedEvent.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityCreatedEvent.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.core.events; -import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; import lombok.Getter; import lombok.ToString; @@ -25,11 +25,11 @@ * This event carries the newly created entity and can be used by listeners * to perform additional operations like PID generation, validation, etc. * - * @param the type of entity that was created, must extend GenericIDORISEntity + * @param the type of entity that was created, must extend AdministrativeMetadata */ @Getter @ToString(callSuper = true) -public class EntityCreatedEvent extends AbstractDomainEvent { +public class EntityCreatedEvent extends AbstractDomainEvent { private final T entity; /** @@ -49,4 +49,4 @@ public EntityCreatedEvent(T entity) { public T getEntity() { return entity; } -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/EntityDeletedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityDeletedEvent.java index 4d541a2..d7eb380 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/events/EntityDeletedEvent.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityDeletedEvent.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.core.events; -import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; import lombok.Getter; import lombok.ToString; @@ -25,11 +25,11 @@ * This event carries the deleted entity and can be used by listeners * to perform additional operations like cleanup, notification, etc. * - * @param the type of entity that was deleted, must extend GenericIDORISEntity + * @param the type of entity that was deleted, must extend AdministrativeMetadata */ @Getter @ToString(callSuper = true) -public class EntityDeletedEvent extends AbstractDomainEvent { +public class EntityDeletedEvent extends AbstractDomainEvent { private final T entity; private final String entityType; private final String entityPid; @@ -71,4 +71,4 @@ public String getEntityType() { public String getEntityPid() { return entityPid; } -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/EntityImportedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityImportedEvent.java index cedb106..6bdb74b 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/events/EntityImportedEvent.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityImportedEvent.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.core.events; -import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; import lombok.Getter; import lombok.ToString; @@ -25,11 +25,11 @@ * This event carries the imported entity, the source system, and import metadata. * It can be used by listeners to perform additional operations like validation, enrichment, or notification. * - * @param the type of entity that was imported, must extend GenericIDORISEntity + * @param the type of entity that was imported, must extend AdministrativeMetadata */ @Getter @ToString(callSuper = true) -public class EntityImportedEvent extends AbstractDomainEvent { +public class EntityImportedEvent extends AbstractDomainEvent { private final T entity; private final String sourceSystem; private final String sourceIdentifier; @@ -115,4 +115,4 @@ public enum ImportResult { */ SYSTEM_ERROR } -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/EntityPatchedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityPatchedEvent.java new file mode 100644 index 0000000..5fada1f --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityPatchedEvent.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.core.events; + +import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +import lombok.Getter; + +/** + * Event that is published when an entity is partially updated (patched) in the system. + * This event carries the patched entity and can be used by listeners + * to react to entity patch operations. + * + * @param the type of entity that was patched + */ +@Getter +public class EntityPatchedEvent extends AbstractDomainEvent { + private final T entity; + private final Long previousVersion; + + /** + * Creates a new EntityPatchedEvent for the given entity. + * + * @param entity the patched entity + * @param previousVersion the version of the entity before the patch + */ + public EntityPatchedEvent(T entity, Long previousVersion) { + this.entity = entity; + this.previousVersion = previousVersion; + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/EntityUpdatedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityUpdatedEvent.java index 50c4c8d..96ecc94 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/events/EntityUpdatedEvent.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityUpdatedEvent.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.core.events; -import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; import lombok.Getter; import lombok.ToString; @@ -25,11 +25,11 @@ * This event carries the updated entity and can be used by listeners * to perform additional operations like versioning, validation, etc. * - * @param the type of entity that was updated, must extend GenericIDORISEntity + * @param the type of entity that was updated, must extend AdministrativeMetadata */ @Getter @ToString(callSuper = true) -public class EntityUpdatedEvent extends AbstractDomainEvent { +public class EntityUpdatedEvent extends AbstractDomainEvent { private final T entity; private final Long previousVersion; @@ -61,4 +61,4 @@ public T getEntity() { public Long getPreviousVersion() { return previousVersion; } -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/EventPublisherService.java b/src/main/java/edu/kit/datamanager/idoris/core/events/EventPublisherService.java index 7e9b8af..720d509 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/events/EventPublisherService.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/EventPublisherService.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.core.events; -import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; @@ -45,13 +45,13 @@ public EventPublisherService(ApplicationEventPublisher eventPublisher) { * @param entity the newly created entity * @param the type of entity */ - public void publishEntityCreated(T entity) { + public void publishEntityCreated(T entity) { log.debug("Publishing EntityCreatedEvent for entity: {}", entity); eventPublisher.publishEvent(new EntityCreatedEvent<>(entity)); } /** - * Publishes an entity created event for non-GenericIDORISEntity entities. + * Publishes an entity created event for non-AdministrativeMetadata entities. * * @param entity the newly created entity * @param entityType the type identifier for the entity @@ -68,13 +68,13 @@ public void publishEntityCreated(Object entity, String entityType) { * @param previousVersion the version of the entity before the update * @param the type of entity */ - public void publishEntityUpdated(T entity, Long previousVersion) { + public void publishEntityUpdated(T entity, Long previousVersion) { log.debug("Publishing EntityUpdatedEvent for entity: {}, previous version: {}", entity, previousVersion); eventPublisher.publishEvent(new EntityUpdatedEvent<>(entity, previousVersion)); } /** - * Publishes an entity updated event for non-GenericIDORISEntity entities. + * Publishes an entity updated event for non-AdministrativeMetadata entities. * * @param entity the updated entity * @param entityType the type identifier for the entity @@ -90,13 +90,13 @@ public void publishEntityUpdated(Object entity, String entityType) { * @param entity the deleted entity * @param the type of entity */ - public void publishEntityDeleted(T entity) { + public void publishEntityDeleted(T entity) { log.debug("Publishing EntityDeletedEvent for entity: {}", entity); eventPublisher.publishEvent(new EntityDeletedEvent<>(entity)); } /** - * Publishes an entity deleted event for non-GenericIDORISEntity entities. + * Publishes an entity deleted event for non-AdministrativeMetadata entities. * * @param entity the deleted entity * @param entityType the type identifier for the entity @@ -114,7 +114,7 @@ public void publishEntityDeleted(Object entity, String entityType) { * @param isNewPID indicates whether this is a newly generated PID or an existing one * @param the type of entity */ - public void publishPIDGenerated(T entity, String pid, boolean isNewPID) { + public void publishPIDGenerated(T entity, String pid, boolean isNewPID) { log.debug("Publishing PIDGeneratedEvent for entity: {}, PID: {}, isNewPID: {}", entity, pid, isNewPID); eventPublisher.publishEvent(new PIDGeneratedEvent<>(entity, pid, isNewPID)); } @@ -127,10 +127,33 @@ public void publishPIDGenerated(T entity, String * @param pid the generated PID * @param the type of entity */ - public void publishPIDGenerated(T entity, String pid) { + public void publishPIDGenerated(T entity, String pid) { publishPIDGenerated(entity, pid, true); } + /** + * Publishes an entity patched event. + * + * @param entity the patched entity + * @param previousVersion the version of the entity before the patch + * @param the type of entity + */ + public void publishEntityPatched(T entity, Long previousVersion) { + log.debug("Publishing EntityPatchedEvent for entity: {}, previous version: {}", entity, previousVersion); + eventPublisher.publishEvent(new EntityPatchedEvent<>(entity, previousVersion)); + } + + /** + * Publishes an entity patched event for non-AdministrativeMetadata entities. + * + * @param entity the patched entity + * @param entityType the type identifier for the entity + */ + public void publishEntityPatched(Object entity, String entityType) { + log.debug("Publishing EntityPatchedEvent for entity: {}, type: {}", entity, entityType); + eventPublisher.publishEvent(new GenericEntityPatchedEvent(entity, entityType)); + } + /** * Publishes a generic domain event. * @@ -140,4 +163,4 @@ public void publishEvent(DomainEvent event) { log.debug("Publishing event: {}", event); eventPublisher.publishEvent(event); } -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityCreatedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityCreatedEvent.java index 5c1d95a..cef0614 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityCreatedEvent.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityCreatedEvent.java @@ -21,7 +21,7 @@ /** * Event that is published when a new entity is created in the system. - * This event is for entities that don't extend GenericIDORISEntity. + * This event is for entities that don't extend AdministrativeMetadata. */ @Getter @ToString(callSuper = true) diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityDeletedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityDeletedEvent.java index 03d7a7b..e1f8097 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityDeletedEvent.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityDeletedEvent.java @@ -21,7 +21,7 @@ /** * Event that is published when an entity is deleted from the system. - * This event is for entities that don't extend GenericIDORISEntity. + * This event is for entities that don't extend AdministrativeMetadata. */ @Getter @ToString(callSuper = true) diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityPatchedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityPatchedEvent.java new file mode 100644 index 0000000..a164140 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityPatchedEvent.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.core.events; + +import lombok.Getter; + +/** + * Event that is published when a non-AdministrativeMetadata is partially updated (patched) in the system. + * This event carries the patched entity and can be used by listeners + * to react to entity patch operations. + */ +@Getter +public class GenericEntityPatchedEvent extends AbstractDomainEvent { + private final Object entity; + private final String entityType; + + /** + * Creates a new GenericEntityPatchedEvent for the given entity. + * + * @param entity the patched entity + * @param entityType the type identifier for the entity + */ + public GenericEntityPatchedEvent(Object entity, String entityType) { + this.entity = entity; + this.entityType = entityType; + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityUpdatedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityUpdatedEvent.java index f81ddb3..973b1e0 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityUpdatedEvent.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/GenericEntityUpdatedEvent.java @@ -21,7 +21,7 @@ /** * Event that is published when an entity is updated in the system. - * This event is for entities that don't extend GenericIDORISEntity. + * This event is for entities that don't extend AdministrativeMetadata. */ @Getter @ToString(callSuper = true) diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/PIDGeneratedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/PIDGeneratedEvent.java index 55a6a9c..d5f1682 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/events/PIDGeneratedEvent.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/PIDGeneratedEvent.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.core.events; -import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; import lombok.Getter; import lombok.ToString; @@ -25,11 +25,11 @@ * This event carries the entity and the generated PID, and can be used by listeners * to perform additional operations like PID record creation, indexing, etc. * - * @param the type of entity for which the PID was generated, must extend GenericIDORISEntity + * @param the type of entity for which the PID was generated, must extend AdministrativeMetadata */ @Getter @ToString(callSuper = true) -public class PIDGeneratedEvent extends AbstractDomainEvent { +public class PIDGeneratedEvent extends AbstractDomainEvent { private final T entity; private final String pid; private final boolean isNewPID; @@ -84,4 +84,4 @@ public String getPid() { public boolean isNewPID() { return isNewPID; } -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/SchemaGeneratedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/SchemaGeneratedEvent.java index a9cb691..1259ce6 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/events/SchemaGeneratedEvent.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/SchemaGeneratedEvent.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.core.events; -import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; import lombok.Getter; import lombok.ToString; @@ -28,7 +28,7 @@ @Getter @ToString(callSuper = true) public class SchemaGeneratedEvent extends AbstractDomainEvent { - private final GenericIDORISEntity entity; + private final AdministrativeMetadata entity; private final String schemaFormat; private final String schemaContent; private final boolean isValid; @@ -41,7 +41,7 @@ public class SchemaGeneratedEvent extends AbstractDomainEvent { * @param schemaContent the content of the schema * @param isValid indicates whether the schema is valid */ - public SchemaGeneratedEvent(GenericIDORISEntity entity, String schemaFormat, String schemaContent, boolean isValid) { + public SchemaGeneratedEvent(AdministrativeMetadata entity, String schemaFormat, String schemaContent, boolean isValid) { this.entity = entity; this.schemaFormat = schemaFormat; this.schemaContent = schemaContent; @@ -53,7 +53,7 @@ public SchemaGeneratedEvent(GenericIDORISEntity entity, String schemaFormat, Str * * @return the entity */ - public GenericIDORISEntity getEntity() { + public AdministrativeMetadata getEntity() { return entity; } @@ -83,4 +83,4 @@ public String getSchemaContent() { public boolean isValid() { return isValid; } -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/VersionCreatedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/VersionCreatedEvent.java index 9d3144a..5aee872 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/events/VersionCreatedEvent.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/VersionCreatedEvent.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.core.events; -import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; import lombok.Getter; import lombok.ToString; @@ -27,11 +27,11 @@ * This event carries the current entity, the previous version, and change information. * It can be used by listeners to perform additional operations like version tracking, notification, or audit logging. * - * @param the type of entity that was versioned, must extend GenericIDORISEntity + * @param the type of entity that was versioned, must extend AdministrativeMetadata */ @Getter @ToString(callSuper = true) -public class VersionCreatedEvent extends AbstractDomainEvent { +public class VersionCreatedEvent extends AbstractDomainEvent { private final T currentEntity; private final T previousEntity; private final Long previousVersion; @@ -98,4 +98,4 @@ public Long getCurrentVersion() { public Map getChanges() { return changes; } -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/IAtomicDataTypeDao.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/dao/IAtomicDataTypeDao.java similarity index 87% rename from src/main/java/edu/kit/datamanager/idoris/dao/IAtomicDataTypeDao.java rename to src/main/java/edu/kit/datamanager/idoris/datatypes/dao/IAtomicDataTypeDao.java index 3937ec7..3588dd4 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/IAtomicDataTypeDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/dao/IAtomicDataTypeDao.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.dao; +package edu.kit.datamanager.idoris.datatypes.dao; -import edu.kit.datamanager.idoris.domain.entities.AtomicDataType; +import edu.kit.datamanager.idoris.core.domain.dao.IGenericRepo; +import edu.kit.datamanager.idoris.datatypes.entities.AtomicDataType; import org.springframework.data.neo4j.repository.query.Query; /** @@ -31,4 +32,4 @@ public interface IAtomicDataTypeDao extends IGenericRepo { */ @Query("MATCH (d:AtomicDataType {pid: $pid})-[:inheritsFrom*]->(d2:AtomicDataType) RETURN d2") Iterable findAllInInheritanceChain(String pid); -} +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/IDataTypeDao.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/dao/IDataTypeDao.java similarity index 87% rename from src/main/java/edu/kit/datamanager/idoris/dao/IDataTypeDao.java rename to src/main/java/edu/kit/datamanager/idoris/datatypes/dao/IDataTypeDao.java index 7ad0f29..56ae83d 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/IDataTypeDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/dao/IDataTypeDao.java @@ -14,10 +14,11 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.dao; +package edu.kit.datamanager.idoris.datatypes.dao; -import edu.kit.datamanager.idoris.domain.entities.DataType; -import edu.kit.datamanager.idoris.domain.entities.Operation; +import edu.kit.datamanager.idoris.core.domain.dao.IGenericRepo; +import edu.kit.datamanager.idoris.datatypes.entities.DataType; +import edu.kit.datamanager.idoris.operations.entities.Operation; import org.springframework.data.neo4j.repository.query.Query; /** diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/ITypeProfileDao.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/dao/ITypeProfileDao.java similarity index 87% rename from src/main/java/edu/kit/datamanager/idoris/dao/ITypeProfileDao.java rename to src/main/java/edu/kit/datamanager/idoris/datatypes/dao/ITypeProfileDao.java index 452f8ec..79e71dc 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/ITypeProfileDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/dao/ITypeProfileDao.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.dao; +package edu.kit.datamanager.idoris.datatypes.dao; -import edu.kit.datamanager.idoris.domain.entities.TypeProfile; +import edu.kit.datamanager.idoris.core.domain.dao.IGenericRepo; +import edu.kit.datamanager.idoris.datatypes.entities.TypeProfile; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import org.springframework.data.neo4j.repository.query.Query; @@ -27,4 +28,4 @@ public interface ITypeProfileDao extends IGenericRepo { @Query("MATCH (d:TypeProfile {pid: $pid})-[:inheritsFrom*]->(typeProfile:TypeProfile) return typeProfile") Iterable findAllTypeProfilesInInheritanceChain(String pid); -} +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/AtomicDataType.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/entities/AtomicDataType.java similarity index 91% rename from src/main/java/edu/kit/datamanager/idoris/domain/entities/AtomicDataType.java rename to src/main/java/edu/kit/datamanager/idoris/datatypes/entities/AtomicDataType.java index 9a23a6a..c7749ac 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/AtomicDataType.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/entities/AtomicDataType.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.entities; +package edu.kit.datamanager.idoris.datatypes.entities; -import edu.kit.datamanager.idoris.domain.enums.PrimitiveDataTypes; +import edu.kit.datamanager.idoris.datatypes.enums.PrimitiveDataTypes; import edu.kit.datamanager.idoris.rules.logic.RuleOutput; import edu.kit.datamanager.idoris.rules.logic.Visitor; import lombok.AllArgsConstructor; @@ -33,7 +33,7 @@ @Setter @AllArgsConstructor @NoArgsConstructor -public final class AtomicDataType extends DataType { +public class AtomicDataType extends DataType { @Relationship(value = "inheritsFrom", direction = Relationship.Direction.OUTGOING) private AtomicDataType inheritsFrom; @@ -60,5 +60,4 @@ public boolean inheritsFrom(DataType dataType) { } return false; } - } diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/DataType.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/entities/DataType.java similarity index 86% rename from src/main/java/edu/kit/datamanager/idoris/domain/entities/DataType.java rename to src/main/java/edu/kit/datamanager/idoris/datatypes/entities/DataType.java index 10c0616..78cbb2b 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/DataType.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/entities/DataType.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.entities; +package edu.kit.datamanager.idoris.datatypes.entities; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; -import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -35,7 +35,7 @@ @JsonSubTypes.Type(value = AtomicDataType.class, name = "AtomicDataType"), @JsonSubTypes.Type(value = TypeProfile.class, name = "TypeProfile"), }) -public abstract sealed class DataType extends GenericIDORISEntity permits AtomicDataType, TypeProfile { +public abstract class DataType extends AdministrativeMetadata { private TYPES type; private String defaultValue; diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/TypeProfile.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/entities/TypeProfile.java similarity index 89% rename from src/main/java/edu/kit/datamanager/idoris/domain/entities/TypeProfile.java rename to src/main/java/edu/kit/datamanager/idoris/datatypes/entities/TypeProfile.java index f2870f2..a9dfa59 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/TypeProfile.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/entities/TypeProfile.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.entities; +package edu.kit.datamanager.idoris.datatypes.entities; -import edu.kit.datamanager.idoris.domain.enums.CombinationOptions; +import edu.kit.datamanager.idoris.attributes.entities.Attribute; +import edu.kit.datamanager.idoris.core.domain.enums.CombinationOptions; import edu.kit.datamanager.idoris.rules.logic.RuleOutput; import edu.kit.datamanager.idoris.rules.logic.Visitor; import lombok.AllArgsConstructor; @@ -33,7 +34,7 @@ @Setter @AllArgsConstructor @NoArgsConstructor -public final class TypeProfile extends DataType { +public class TypeProfile extends DataType { @Relationship(value = "inheritsFrom", direction = Relationship.Direction.OUTGOING) private Set inheritsFrom; diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/enums/PrimitiveDataTypes.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/enums/PrimitiveDataTypes.java similarity index 97% rename from src/main/java/edu/kit/datamanager/idoris/domain/enums/PrimitiveDataTypes.java rename to src/main/java/edu/kit/datamanager/idoris/datatypes/enums/PrimitiveDataTypes.java index e3e2bcf..cfbeec8 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/enums/PrimitiveDataTypes.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/enums/PrimitiveDataTypes.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.enums; +package edu.kit.datamanager.idoris.datatypes.enums; import lombok.AllArgsConstructor; import lombok.Getter; @@ -52,4 +52,4 @@ public boolean isValueValid(Object value) { value instanceof Boolean || (value instanceof String string && ("true".equalsIgnoreCase(string) || "false".equalsIgnoreCase(string))); }; } -} +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/web/package-info.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/package-info.java similarity index 55% rename from src/main/java/edu/kit/datamanager/idoris/web/package-info.java rename to src/main/java/edu/kit/datamanager/idoris/datatypes/package-info.java index 05f8e2d..f4ce493 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/package-info.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/package-info.java @@ -15,15 +15,14 @@ */ /** - * Web module for IDORIS. - * This module is responsible for the web API and controllers. - * It provides REST endpoints for accessing and manipulating entities. + * DataTypes module for IDORIS. + * This module contains entity definitions, domain services, and business logic related to data types. + * It is responsible for managing atomic data types and their relationships. * - *

The web module depends on the core module for base abstractions, the domain module for entity definitions, - * and the domain.services package for business logic. It should not depend on repository or other infrastructure concerns directly.

+ *

The DataTypes module depends on the core module for base abstractions and interfaces.

*/ @org.springframework.modulith.ApplicationModule( - displayName = "IDORIS Web", - allowedDependencies = {"core", "domain", "domain.services"} + displayName = "IDORIS DataTypes", + allowedDependencies = {"core"} ) -package edu.kit.datamanager.idoris.web; \ No newline at end of file +package edu.kit.datamanager.idoris.datatypes; \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/services/AtomicDataTypeService.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/services/AtomicDataTypeService.java similarity index 61% rename from src/main/java/edu/kit/datamanager/idoris/services/AtomicDataTypeService.java rename to src/main/java/edu/kit/datamanager/idoris/datatypes/services/AtomicDataTypeService.java index ab9102c..a14c1b5 100644 --- a/src/main/java/edu/kit/datamanager/idoris/services/AtomicDataTypeService.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/services/AtomicDataTypeService.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.services; +package edu.kit.datamanager.idoris.datatypes.services; import edu.kit.datamanager.idoris.core.events.EventPublisherService; -import edu.kit.datamanager.idoris.dao.IAtomicDataTypeDao; -import edu.kit.datamanager.idoris.domain.entities.AtomicDataType; +import edu.kit.datamanager.idoris.datatypes.dao.IAtomicDataTypeDao; +import edu.kit.datamanager.idoris.datatypes.entities.AtomicDataType; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -130,4 +130,66 @@ public List getAllAtomicDataTypes() { log.debug("Retrieving all AtomicDataTypes"); return atomicDataTypeDao.findAll(); } + + /** + * Partially updates an existing AtomicDataType entity. + * + * @param pid the PID of the AtomicDataType to patch + * @param atomicDataTypePatch the partial AtomicDataType entity with fields to update + * @return the patched AtomicDataType entity + * @throws IllegalArgumentException if the AtomicDataType does not exist + */ + @Transactional + public AtomicDataType patchAtomicDataType(String pid, AtomicDataType atomicDataTypePatch) { + log.debug("Patching AtomicDataType with PID: {}, patch: {}", pid, atomicDataTypePatch); + if (pid == null || pid.isEmpty()) { + throw new IllegalArgumentException("AtomicDataType PID cannot be null or empty"); + } + + // Get the current entity + AtomicDataType existing = atomicDataTypeDao.findById(pid) + .orElseThrow(() -> new IllegalArgumentException("AtomicDataType not found with PID: " + pid)); + Long previousVersion = existing.getVersion(); + + // Apply non-null fields from the patch to the existing entity + if (atomicDataTypePatch.getName() != null) { + existing.setName(atomicDataTypePatch.getName()); + } + if (atomicDataTypePatch.getDescription() != null) { + existing.setDescription(atomicDataTypePatch.getDescription()); + } + if (atomicDataTypePatch.getDefaultValue() != null) { + existing.setDefaultValue(atomicDataTypePatch.getDefaultValue()); + } + if (atomicDataTypePatch.getPrimitiveDataType() != null) { + existing.setPrimitiveDataType(atomicDataTypePatch.getPrimitiveDataType()); + } + if (atomicDataTypePatch.getRegularExpression() != null) { + existing.setRegularExpression(atomicDataTypePatch.getRegularExpression()); + } + if (atomicDataTypePatch.getPermittedValues() != null) { + existing.setPermittedValues(atomicDataTypePatch.getPermittedValues()); + } + if (atomicDataTypePatch.getForbiddenValues() != null) { + existing.setForbiddenValues(atomicDataTypePatch.getForbiddenValues()); + } + if (atomicDataTypePatch.getMinimum() != null) { + existing.setMinimum(atomicDataTypePatch.getMinimum()); + } + if (atomicDataTypePatch.getMaximum() != null) { + existing.setMaximum(atomicDataTypePatch.getMaximum()); + } + if (atomicDataTypePatch.getInheritsFrom() != null) { + existing.setInheritsFrom(atomicDataTypePatch.getInheritsFrom()); + } + + // Save the updated entity + AtomicDataType saved = atomicDataTypeDao.save(existing); + + // Publish the patched event + eventPublisher.publishEntityPatched(saved, previousVersion); + + log.info("Patched AtomicDataType with PID: {}", saved.getPid()); + return saved; + } } diff --git a/src/main/java/edu/kit/datamanager/idoris/services/TypeProfileService.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/services/TypeProfileService.java similarity index 74% rename from src/main/java/edu/kit/datamanager/idoris/services/TypeProfileService.java rename to src/main/java/edu/kit/datamanager/idoris/datatypes/services/TypeProfileService.java index ad28f00..59796f8 100644 --- a/src/main/java/edu/kit/datamanager/idoris/services/TypeProfileService.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/services/TypeProfileService.java @@ -13,12 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -package edu.kit.datamanager.idoris.services; +package edu.kit.datamanager.idoris.datatypes.services; import edu.kit.datamanager.idoris.core.events.EventPublisherService; -import edu.kit.datamanager.idoris.dao.ITypeProfileDao; -import edu.kit.datamanager.idoris.domain.entities.TypeProfile; +import edu.kit.datamanager.idoris.datatypes.dao.ITypeProfileDao; +import edu.kit.datamanager.idoris.datatypes.entities.TypeProfile; import edu.kit.datamanager.idoris.rules.validation.ValidationPolicyValidator; import edu.kit.datamanager.idoris.rules.validation.ValidationResult; import lombok.extern.slf4j.Slf4j; @@ -75,17 +74,13 @@ public TypeProfile createTypeProfile(TypeProfile typeProfile) { @Transactional public TypeProfile updateTypeProfile(TypeProfile typeProfile) { log.debug("Updating TypeProfile: {}", typeProfile); - if (typeProfile.getPid() == null || typeProfile.getPid().isEmpty()) { throw new IllegalArgumentException("TypeProfile must have a PID to be updated"); } - // Get the current version before updating TypeProfile existing = typeProfileDao.findById(typeProfile.getPid()) .orElseThrow(() -> new IllegalArgumentException("TypeProfile not found with PID: " + typeProfile.getPid())); - Long previousVersion = existing.getVersion(); - TypeProfile saved = typeProfileDao.save(typeProfile); eventPublisher.publishEntityUpdated(saved, previousVersion); log.info("Updated TypeProfile with PID: {}", saved.getPid()); @@ -101,10 +96,8 @@ public TypeProfile updateTypeProfile(TypeProfile typeProfile) { @Transactional public void deleteTypeProfile(String pid) { log.debug("Deleting TypeProfile with PID: {}", pid); - TypeProfile typeProfile = typeProfileDao.findById(pid) .orElseThrow(() -> new IllegalArgumentException("TypeProfile not found with PID: " + pid)); - typeProfileDao.delete(typeProfile); eventPublisher.publishEntityDeleted(typeProfile); log.info("Deleted TypeProfile with PID: {}", pid); @@ -143,10 +136,8 @@ public List getAllTypeProfiles() { @Transactional(readOnly = true) public ValidationResult validateTypeProfile(String pid) { log.debug("Validating TypeProfile with PID: {}", pid); - TypeProfile typeProfile = typeProfileDao.findById(pid) .orElseThrow(() -> new IllegalArgumentException("TypeProfile not found with PID: " + pid)); - ValidationPolicyValidator validator = new ValidationPolicyValidator(); return typeProfile.execute(validator); } @@ -161,12 +152,54 @@ public ValidationResult validateTypeProfile(String pid) { @Transactional(readOnly = true) public Iterable getInheritanceChain(String pid) { log.debug("Retrieving inheritance chain for TypeProfile with PID: {}", pid); - // Check if the TypeProfile exists if (!typeProfileDao.existsById(pid)) { throw new IllegalArgumentException("TypeProfile not found with PID: " + pid); } - return typeProfileDao.findAllTypeProfilesInInheritanceChain(pid); } + + /** + * Partially updates an existing TypeProfile entity. + * + * @param pid the PID of the TypeProfile to patch + * @param typeProfilePatch the partial TypeProfile entity with fields to update + * @return the patched TypeProfile entity + * @throws IllegalArgumentException if the TypeProfile does not exist + */ + @Transactional + public TypeProfile patchTypeProfile(String pid, TypeProfile typeProfilePatch) { + log.debug("Patching TypeProfile with PID: {}, patch: {}", pid, typeProfilePatch); + if (pid == null || pid.isEmpty()) { + throw new IllegalArgumentException("TypeProfile PID cannot be null or empty"); + } + + // Get the current entity + TypeProfile existing = typeProfileDao.findById(pid) + .orElseThrow(() -> new IllegalArgumentException("TypeProfile not found with PID: " + pid)); + Long previousVersion = existing.getVersion(); + + // Apply non-null fields from the patch to the existing entity + if (typeProfilePatch.getName() != null) { + existing.setName(typeProfilePatch.getName()); + } + if (typeProfilePatch.getDescription() != null) { + existing.setDescription(typeProfilePatch.getDescription()); + } + if (typeProfilePatch.getAttributes() != null && !typeProfilePatch.getAttributes().isEmpty()) { + existing.setAttributes(typeProfilePatch.getAttributes()); + } + if (typeProfilePatch.getInheritsFrom() != null && !typeProfilePatch.getInheritsFrom().isEmpty()) { + existing.setInheritsFrom(typeProfilePatch.getInheritsFrom()); + } + + // Save the updated entity + TypeProfile saved = typeProfileDao.save(existing); + + // Publish the patched event + eventPublisher.publishEntityPatched(saved, previousVersion); + + log.info("Patched TypeProfile with PID: {}", saved.getPid()); + return saved; + } } diff --git a/src/main/java/edu/kit/datamanager/idoris/web/api/IAtomicDataTypeApi.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/api/IAtomicDataTypeApi.java similarity index 81% rename from src/main/java/edu/kit/datamanager/idoris/web/api/IAtomicDataTypeApi.java rename to src/main/java/edu/kit/datamanager/idoris/datatypes/web/api/IAtomicDataTypeApi.java index 19dee97..e81599d 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/api/IAtomicDataTypeApi.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/api/IAtomicDataTypeApi.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.web.api; +package edu.kit.datamanager.idoris.datatypes.web.api; -import edu.kit.datamanager.idoris.domain.entities.AtomicDataType; +import edu.kit.datamanager.idoris.datatypes.entities.AtomicDataType; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -153,11 +153,36 @@ ResponseEntity deleteAtomicDataType( responses = { @ApiResponse(responseCode = "200", description = "Operations found", content = @Content(mediaType = "application/hal+json", - schema = @Schema(implementation = edu.kit.datamanager.idoris.domain.entities.Operation.class))), + schema = @Schema(implementation = edu.kit.datamanager.idoris.operations.entities.Operation.class))), @ApiResponse(responseCode = "404", description = "AtomicDataType not found") } ) - ResponseEntity>> getOperationsForAtomicDataType( + ResponseEntity>> getOperationsForAtomicDataType( @Parameter(description = "PID of the AtomicDataType", required = true) @PathVariable String pid); + + /** + * Partially updates an AtomicDataType entity. + * + * @param pid the PID of the AtomicDataType to patch + * @param atomicDataTypePatch the partial AtomicDataType entity with fields to update + * @return the patched AtomicDataType entity + */ + @PatchMapping("/{pid}") + @io.swagger.v3.oas.annotations.Operation( + summary = "Partially update an AtomicDataType", + description = "Updates specific fields of an existing AtomicDataType entity", + responses = { + @ApiResponse(responseCode = "200", description = "AtomicDataType patched", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = AtomicDataType.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "404", description = "AtomicDataType not found") + } + ) + ResponseEntity> patchAtomicDataType( + @Parameter(description = "PID of the AtomicDataType", required = true) + @PathVariable String pid, + @Parameter(description = "Partial AtomicDataType with fields to update", required = true) + @RequestBody AtomicDataType atomicDataTypePatch); } diff --git a/src/main/java/edu/kit/datamanager/idoris/web/api/ITypeProfileApi.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/api/ITypeProfileApi.java similarity index 85% rename from src/main/java/edu/kit/datamanager/idoris/web/api/ITypeProfileApi.java rename to src/main/java/edu/kit/datamanager/idoris/datatypes/web/api/ITypeProfileApi.java index 5813fcc..8e0749c 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/api/ITypeProfileApi.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/api/ITypeProfileApi.java @@ -14,12 +14,12 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.web.api; +package edu.kit.datamanager.idoris.datatypes.web.api; -import edu.kit.datamanager.idoris.domain.entities.Attribute; -import edu.kit.datamanager.idoris.domain.entities.Operation; -import edu.kit.datamanager.idoris.domain.entities.TypeProfile; -import edu.kit.datamanager.idoris.web.v1.TypeProfileController.TypeProfileInheritance; +import edu.kit.datamanager.idoris.attributes.entities.Attribute; +import edu.kit.datamanager.idoris.datatypes.entities.TypeProfile; +import edu.kit.datamanager.idoris.datatypes.web.v1.TypeProfileController.TypeProfileInheritance; +import edu.kit.datamanager.idoris.operations.entities.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -225,4 +225,29 @@ ResponseEntity deleteTypeProfile( ResponseEntity> getInheritanceTree( @Parameter(description = "PID of the TypeProfile", required = true) @NotNull @PathVariable String pid); -} \ No newline at end of file + + /** + * Partially updates a TypeProfile entity. + * + * @param pid the PID of the TypeProfile to patch + * @param typeProfilePatch the partial TypeProfile entity with fields to update + * @return the patched TypeProfile entity + */ + @PatchMapping("/{pid}") + @io.swagger.v3.oas.annotations.Operation( + summary = "Partially update a TypeProfile", + description = "Updates specific fields of an existing TypeProfile entity", + responses = { + @ApiResponse(responseCode = "200", description = "TypeProfile patched", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = TypeProfile.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "404", description = "TypeProfile not found") + } + ) + ResponseEntity> patchTypeProfile( + @Parameter(description = "PID of the TypeProfile", required = true) + @PathVariable String pid, + @Parameter(description = "Partial TypeProfile with fields to update", required = true) + @RequestBody TypeProfile typeProfilePatch); +} diff --git a/src/main/java/edu/kit/datamanager/idoris/web/hateoas/AtomicDataTypeModelAssembler.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/AtomicDataTypeModelAssembler.java similarity index 89% rename from src/main/java/edu/kit/datamanager/idoris/web/hateoas/AtomicDataTypeModelAssembler.java rename to src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/AtomicDataTypeModelAssembler.java index a8dd898..1121840 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/hateoas/AtomicDataTypeModelAssembler.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/AtomicDataTypeModelAssembler.java @@ -14,10 +14,11 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.web.hateoas; +package edu.kit.datamanager.idoris.datatypes.web.hateoas; -import edu.kit.datamanager.idoris.domain.entities.AtomicDataType; -import edu.kit.datamanager.idoris.web.v1.AtomicDataTypeController; +import edu.kit.datamanager.idoris.core.domain.web.hateoas.EntityModelAssembler; +import edu.kit.datamanager.idoris.datatypes.entities.AtomicDataType; +import edu.kit.datamanager.idoris.datatypes.web.v1.AtomicDataTypeController; import org.springframework.hateoas.EntityModel; import org.springframework.stereotype.Component; diff --git a/src/main/java/edu/kit/datamanager/idoris/web/hateoas/DataTypeModelAssembler.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/DataTypeModelAssembler.java similarity index 84% rename from src/main/java/edu/kit/datamanager/idoris/web/hateoas/DataTypeModelAssembler.java rename to src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/DataTypeModelAssembler.java index 21a164c..4d981c1 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/hateoas/DataTypeModelAssembler.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/DataTypeModelAssembler.java @@ -14,13 +14,14 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.web.hateoas; +package edu.kit.datamanager.idoris.datatypes.web.hateoas; -import edu.kit.datamanager.idoris.domain.entities.AtomicDataType; -import edu.kit.datamanager.idoris.domain.entities.DataType; -import edu.kit.datamanager.idoris.domain.entities.TypeProfile; -import edu.kit.datamanager.idoris.web.v1.AtomicDataTypeController; -import edu.kit.datamanager.idoris.web.v1.TypeProfileController; +import edu.kit.datamanager.idoris.core.domain.web.hateoas.EntityModelAssembler; +import edu.kit.datamanager.idoris.datatypes.entities.AtomicDataType; +import edu.kit.datamanager.idoris.datatypes.entities.DataType; +import edu.kit.datamanager.idoris.datatypes.entities.TypeProfile; +import edu.kit.datamanager.idoris.datatypes.web.v1.AtomicDataTypeController; +import edu.kit.datamanager.idoris.datatypes.web.v1.TypeProfileController; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.hateoas.EntityModel; import org.springframework.stereotype.Component; @@ -68,4 +69,4 @@ public EntityModel toModel(DataType dataType) { return entityModel; } } -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/idoris/web/hateoas/TypeProfileModelAssembler.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/TypeProfileModelAssembler.java similarity index 90% rename from src/main/java/edu/kit/datamanager/idoris/web/hateoas/TypeProfileModelAssembler.java rename to src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/TypeProfileModelAssembler.java index 90267d7..bad6be7 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/hateoas/TypeProfileModelAssembler.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/TypeProfileModelAssembler.java @@ -14,10 +14,11 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.web.hateoas; +package edu.kit.datamanager.idoris.datatypes.web.hateoas; -import edu.kit.datamanager.idoris.domain.entities.TypeProfile; -import edu.kit.datamanager.idoris.web.v1.TypeProfileController; +import edu.kit.datamanager.idoris.core.domain.web.hateoas.EntityModelAssembler; +import edu.kit.datamanager.idoris.datatypes.entities.TypeProfile; +import edu.kit.datamanager.idoris.datatypes.web.v1.TypeProfileController; import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder; import org.springframework.stereotype.Component; diff --git a/src/main/java/edu/kit/datamanager/idoris/web/v1/AtomicDataTypeController.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/v1/AtomicDataTypeController.java similarity index 71% rename from src/main/java/edu/kit/datamanager/idoris/web/v1/AtomicDataTypeController.java rename to src/main/java/edu/kit/datamanager/idoris/datatypes/web/v1/AtomicDataTypeController.java index 62073bb..670de63 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/v1/AtomicDataTypeController.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/v1/AtomicDataTypeController.java @@ -14,19 +14,19 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.web.v1; +package edu.kit.datamanager.idoris.datatypes.web.v1; import edu.kit.datamanager.idoris.configuration.ApplicationProperties; -import edu.kit.datamanager.idoris.domain.entities.AtomicDataType; -import edu.kit.datamanager.idoris.domain.entities.Operation; +import edu.kit.datamanager.idoris.core.domain.exceptions.ValidationException; +import edu.kit.datamanager.idoris.datatypes.entities.AtomicDataType; +import edu.kit.datamanager.idoris.datatypes.services.AtomicDataTypeService; +import edu.kit.datamanager.idoris.datatypes.web.api.IAtomicDataTypeApi; +import edu.kit.datamanager.idoris.datatypes.web.hateoas.AtomicDataTypeModelAssembler; +import edu.kit.datamanager.idoris.operations.entities.Operation; +import edu.kit.datamanager.idoris.operations.services.OperationService; import edu.kit.datamanager.idoris.rules.logic.RuleService; import edu.kit.datamanager.idoris.rules.logic.RuleTask; import edu.kit.datamanager.idoris.rules.validation.ValidationResult; -import edu.kit.datamanager.idoris.services.AtomicDataTypeService; -import edu.kit.datamanager.idoris.services.OperationService; -import edu.kit.datamanager.idoris.web.ValidationException; -import edu.kit.datamanager.idoris.web.api.IAtomicDataTypeApi; -import edu.kit.datamanager.idoris.web.hateoas.AtomicDataTypeModelAssembler; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -230,6 +230,75 @@ public ResponseEntity deleteAtomicDataType( return ResponseEntity.noContent().build(); } + /** + * {@inheritDoc} + */ + @Override + @PatchMapping("/{pid}") + @io.swagger.v3.oas.annotations.Operation( + summary = "Partially update an AtomicDataType", + description = "Updates specific fields of an existing AtomicDataType entity", + responses = { + @ApiResponse(responseCode = "200", description = "AtomicDataType patched", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = AtomicDataType.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "404", description = "AtomicDataType not found") + } + ) + public ResponseEntity> patchAtomicDataType( + @Parameter(description = "PID of the AtomicDataType", required = true) + @PathVariable String pid, + @Parameter(description = "Partial AtomicDataType with fields to update", required = true) + @RequestBody AtomicDataType atomicDataTypePatch) { + if (!atomicDataTypeService.getAtomicDataType(pid).isPresent()) { + return ResponseEntity.notFound().build(); + } + + // Validate the patch if it contains fields that need validation + if (atomicDataTypePatch.getPrimitiveDataType() != null || + atomicDataTypePatch.getRegularExpression() != null || + atomicDataTypePatch.getPermittedValues() != null || + atomicDataTypePatch.getForbiddenValues() != null || + atomicDataTypePatch.getMinimum() != null || + atomicDataTypePatch.getMaximum() != null || + atomicDataTypePatch.getInheritsFrom() != null) { + + // Get the current entity + AtomicDataType existing = atomicDataTypeService.getAtomicDataType(pid).get(); + + // Create a merged entity for validation + AtomicDataType merged = new AtomicDataType(); + merged.setPid(existing.getPid()); + merged.setName(atomicDataTypePatch.getName() != null ? atomicDataTypePatch.getName() : existing.getName()); + merged.setDescription(atomicDataTypePatch.getDescription() != null ? atomicDataTypePatch.getDescription() : existing.getDescription()); + merged.setDefaultValue(atomicDataTypePatch.getDefaultValue() != null ? atomicDataTypePatch.getDefaultValue() : existing.getDefaultValue()); + merged.setPrimitiveDataType(atomicDataTypePatch.getPrimitiveDataType() != null ? atomicDataTypePatch.getPrimitiveDataType() : existing.getPrimitiveDataType()); + merged.setRegularExpression(atomicDataTypePatch.getRegularExpression() != null ? atomicDataTypePatch.getRegularExpression() : existing.getRegularExpression()); + merged.setPermittedValues(atomicDataTypePatch.getPermittedValues() != null ? atomicDataTypePatch.getPermittedValues() : existing.getPermittedValues()); + merged.setForbiddenValues(atomicDataTypePatch.getForbiddenValues() != null ? atomicDataTypePatch.getForbiddenValues() : existing.getForbiddenValues()); + merged.setMinimum(atomicDataTypePatch.getMinimum() != null ? atomicDataTypePatch.getMinimum() : existing.getMinimum()); + merged.setMaximum(atomicDataTypePatch.getMaximum() != null ? atomicDataTypePatch.getMaximum() : existing.getMaximum()); + merged.setInheritsFrom(atomicDataTypePatch.getInheritsFrom() != null ? atomicDataTypePatch.getInheritsFrom() : existing.getInheritsFrom()); + + // Validate the merged entity + ValidationResult validationResult = ruleService.executeRules( + RuleTask.VALIDATE, + merged, + ValidationResult::new + ); + + // Check if validation failed + if (hasValidationErrors(validationResult)) { + throw new ValidationException("Entity validation failed", validationResult); + } + } + + AtomicDataType patchedAtomicDataType = atomicDataTypeService.patchAtomicDataType(pid, atomicDataTypePatch); + EntityModel entityModel = atomicDataTypeModelAssembler.toModel(patchedAtomicDataType); + return ResponseEntity.ok(entityModel); + } + /** * {@inheritDoc} */ diff --git a/src/main/java/edu/kit/datamanager/idoris/web/v1/TypeProfileController.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/v1/TypeProfileController.java similarity index 83% rename from src/main/java/edu/kit/datamanager/idoris/web/v1/TypeProfileController.java rename to src/main/java/edu/kit/datamanager/idoris/datatypes/web/v1/TypeProfileController.java index c42e046..4de8c81 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/v1/TypeProfileController.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/v1/TypeProfileController.java @@ -14,20 +14,20 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.web.v1; +package edu.kit.datamanager.idoris.datatypes.web.v1; +import edu.kit.datamanager.idoris.attributes.entities.Attribute; import edu.kit.datamanager.idoris.configuration.ApplicationProperties; -import edu.kit.datamanager.idoris.domain.entities.Attribute; -import edu.kit.datamanager.idoris.domain.entities.Operation; -import edu.kit.datamanager.idoris.domain.entities.TypeProfile; +import edu.kit.datamanager.idoris.core.domain.exceptions.ValidationException; +import edu.kit.datamanager.idoris.datatypes.entities.TypeProfile; +import edu.kit.datamanager.idoris.datatypes.services.TypeProfileService; +import edu.kit.datamanager.idoris.datatypes.web.api.ITypeProfileApi; +import edu.kit.datamanager.idoris.datatypes.web.hateoas.TypeProfileModelAssembler; +import edu.kit.datamanager.idoris.operations.entities.Operation; +import edu.kit.datamanager.idoris.operations.services.OperationService; import edu.kit.datamanager.idoris.rules.logic.RuleService; import edu.kit.datamanager.idoris.rules.logic.RuleTask; import edu.kit.datamanager.idoris.rules.validation.ValidationResult; -import edu.kit.datamanager.idoris.services.OperationService; -import edu.kit.datamanager.idoris.services.TypeProfileService; -import edu.kit.datamanager.idoris.web.ValidationException; -import edu.kit.datamanager.idoris.web.api.ITypeProfileApi; -import edu.kit.datamanager.idoris.web.hateoas.TypeProfileModelAssembler; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -329,6 +329,62 @@ public ResponseEntity deleteTypeProfile( return ResponseEntity.noContent().build(); } + /** + * {@inheritDoc} + */ + @Override + @PatchMapping("/{pid}") + @io.swagger.v3.oas.annotations.Operation( + summary = "Partially update a TypeProfile", + description = "Updates specific fields of an existing TypeProfile entity", + responses = { + @ApiResponse(responseCode = "200", description = "TypeProfile patched", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = TypeProfile.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "404", description = "TypeProfile not found") + } + ) + public ResponseEntity> patchTypeProfile( + @Parameter(description = "PID of the TypeProfile", required = true) + @PathVariable String pid, + @Parameter(description = "Partial TypeProfile with fields to update", required = true) + @RequestBody TypeProfile typeProfilePatch) { + if (!typeProfileService.getTypeProfile(pid).isPresent()) { + return ResponseEntity.notFound().build(); + } + + // Validate the patch if it contains fields that need validation + if (typeProfilePatch.getAttributes() != null || typeProfilePatch.getInheritsFrom() != null) { + // Get the current entity + TypeProfile existing = typeProfileService.getTypeProfile(pid).get(); + + // Create a merged entity for validation + TypeProfile merged = new TypeProfile(); + merged.setPid(existing.getPid()); + merged.setName(typeProfilePatch.getName() != null ? typeProfilePatch.getName() : existing.getName()); + merged.setDescription(typeProfilePatch.getDescription() != null ? typeProfilePatch.getDescription() : existing.getDescription()); + merged.setAttributes(typeProfilePatch.getAttributes() != null ? typeProfilePatch.getAttributes() : existing.getAttributes()); + merged.setInheritsFrom(typeProfilePatch.getInheritsFrom() != null ? typeProfilePatch.getInheritsFrom() : existing.getInheritsFrom()); + + // Validate the merged entity + ValidationResult validationResult = ruleService.executeRules( + RuleTask.VALIDATE, + merged, + ValidationResult::new + ); + + // Check if validation failed + if (hasValidationErrors(validationResult)) { + throw new ValidationException("Entity validation failed", validationResult); + } + } + + TypeProfile patchedTypeProfile = typeProfileService.patchTypeProfile(pid, typeProfilePatch); + EntityModel entityModel = typeProfileModelAssembler.toModel(patchedTypeProfile); + return ResponseEntity.ok(entityModel); + } + /** * {@inheritDoc} */ diff --git a/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeNotifier.java b/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeNotifier.java index dca91a3..590ce73 100644 --- a/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeNotifier.java +++ b/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeNotifier.java @@ -16,10 +16,10 @@ package edu.kit.datamanager.idoris.notification; +import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; import edu.kit.datamanager.idoris.core.events.EntityCreatedEvent; import edu.kit.datamanager.idoris.core.events.EntityDeletedEvent; import edu.kit.datamanager.idoris.core.events.EntityUpdatedEvent; -import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @@ -100,8 +100,8 @@ public void unsubscribeFromEntity(String entityPid, EntityChangeSubscriber subsc * @param event the entity created event */ @EventListener - public void handleEntityCreated(EntityCreatedEvent event) { - GenericIDORISEntity entity = event.getEntity(); + public void handleEntityCreated(EntityCreatedEvent event) { + AdministrativeMetadata entity = event.getEntity(); String entityType = entity.getClass().getSimpleName(); String entityPid = entity.getPid(); @@ -141,8 +141,8 @@ public void handleEntityCreated(EntityCreatedEvent event) { * @param event the entity updated event */ @EventListener - public void handleEntityUpdated(EntityUpdatedEvent event) { - GenericIDORISEntity entity = event.getEntity(); + public void handleEntityUpdated(EntityUpdatedEvent event) { + AdministrativeMetadata entity = event.getEntity(); String entityType = entity.getClass().getSimpleName(); String entityPid = entity.getPid(); @@ -182,8 +182,8 @@ public void handleEntityUpdated(EntityUpdatedEvent event) { * @param event the entity deleted event */ @EventListener - public void handleEntityDeleted(EntityDeletedEvent event) { - GenericIDORISEntity entity = event.getEntity(); + public void handleEntityDeleted(EntityDeletedEvent event) { + AdministrativeMetadata entity = event.getEntity(); String entityType = event.getEntityType(); String entityPid = event.getEntityPid(); diff --git a/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeSubscriber.java b/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeSubscriber.java index dc97846..fd0bc1e 100644 --- a/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeSubscriber.java +++ b/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeSubscriber.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.notification; -import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; /** * Interface for subscribers that want to be notified of entity changes. @@ -30,7 +30,7 @@ public interface EntityChangeSubscriber { * * @param entity the created entity */ - void onEntityCreated(GenericIDORISEntity entity); + void onEntityCreated(AdministrativeMetadata entity); /** * Called when an entity is updated. @@ -38,12 +38,12 @@ public interface EntityChangeSubscriber { * @param entity the updated entity * @param previousVersion the version of the entity before the update */ - void onEntityUpdated(GenericIDORISEntity entity, Long previousVersion); + void onEntityUpdated(AdministrativeMetadata entity, Long previousVersion); /** * Called when an entity is deleted. * * @param entity the deleted entity */ - void onEntityDeleted(GenericIDORISEntity entity); -} \ No newline at end of file + void onEntityDeleted(AdministrativeMetadata entity); +} diff --git a/src/main/java/edu/kit/datamanager/idoris/notification/LoggingEntityChangeSubscriber.java b/src/main/java/edu/kit/datamanager/idoris/notification/LoggingEntityChangeSubscriber.java index 3b60dc7..59ebcbb 100644 --- a/src/main/java/edu/kit/datamanager/idoris/notification/LoggingEntityChangeSubscriber.java +++ b/src/main/java/edu/kit/datamanager/idoris/notification/LoggingEntityChangeSubscriber.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.notification; -import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -36,7 +36,7 @@ public class LoggingEntityChangeSubscriber implements EntityChangeSubscriber { * @param entity the created entity */ @Override - public void onEntityCreated(GenericIDORISEntity entity) { + public void onEntityCreated(AdministrativeMetadata entity) { log.info("Entity created: type={}, pid={}, name={}", entity.getClass().getSimpleName(), entity.getPid(), @@ -51,7 +51,7 @@ public void onEntityCreated(GenericIDORISEntity entity) { * @param previousVersion the version of the entity before the update */ @Override - public void onEntityUpdated(GenericIDORISEntity entity, Long previousVersion) { + public void onEntityUpdated(AdministrativeMetadata entity, Long previousVersion) { log.info("Entity updated: type={}, pid={}, name={}, previousVersion={}, newVersion={}", entity.getClass().getSimpleName(), entity.getPid(), @@ -67,10 +67,10 @@ public void onEntityUpdated(GenericIDORISEntity entity, Long previousVersion) { * @param entity the deleted entity */ @Override - public void onEntityDeleted(GenericIDORISEntity entity) { + public void onEntityDeleted(AdministrativeMetadata entity) { log.info("Entity deleted: type={}, pid={}, name={}", entity.getClass().getSimpleName(), entity.getPid(), entity.getName()); } -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/IAttributeMappingDao.java b/src/main/java/edu/kit/datamanager/idoris/operations/dao/IAttributeMappingDao.java similarity index 92% rename from src/main/java/edu/kit/datamanager/idoris/dao/IAttributeMappingDao.java rename to src/main/java/edu/kit/datamanager/idoris/operations/dao/IAttributeMappingDao.java index c7a5343..c7fd4a4 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/IAttributeMappingDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/operations/dao/IAttributeMappingDao.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.dao; +package edu.kit.datamanager.idoris.operations.dao; -import edu.kit.datamanager.idoris.domain.entities.AttributeMapping; +import edu.kit.datamanager.idoris.operations.entities.AttributeMapping; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.query.Query; @@ -42,4 +42,4 @@ public interface IAttributeMappingDao extends Neo4jRepository findByOutputAttributePid(String pid); -} +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/IOperationDao.java b/src/main/java/edu/kit/datamanager/idoris/operations/dao/IOperationDao.java similarity index 92% rename from src/main/java/edu/kit/datamanager/idoris/dao/IOperationDao.java rename to src/main/java/edu/kit/datamanager/idoris/operations/dao/IOperationDao.java index ab6eb9c..4b283e9 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/IOperationDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/operations/dao/IOperationDao.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.dao; +package edu.kit.datamanager.idoris.operations.dao; -import edu.kit.datamanager.idoris.domain.entities.Operation; +import edu.kit.datamanager.idoris.core.domain.dao.IGenericRepo; +import edu.kit.datamanager.idoris.operations.entities.Operation; import org.springframework.data.neo4j.repository.query.Query; /** @@ -42,4 +43,4 @@ public interface IOperationDao extends IGenericRepo { UNION MATCH (d:DataType {pid: $pid})-[:inheritsFrom*]->(:DataType)-[:attributes]->(:Attribute)-[:dataType]->(:DataType)-[:inheritsFrom*]->(:DataType)<-[:dataType]-(:Attribute)<-[:executableOn]-(o:Operation) RETURN o""") Iterable getOperationsForDataType(String pid); -} +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/AttributeMapping.java b/src/main/java/edu/kit/datamanager/idoris/operations/entities/AttributeMapping.java similarity index 90% rename from src/main/java/edu/kit/datamanager/idoris/domain/entities/AttributeMapping.java rename to src/main/java/edu/kit/datamanager/idoris/operations/entities/AttributeMapping.java index 75ba923..eba8b99 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/AttributeMapping.java +++ b/src/main/java/edu/kit/datamanager/idoris/operations/entities/AttributeMapping.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.entities; +package edu.kit.datamanager.idoris.operations.entities; -import edu.kit.datamanager.idoris.domain.VisitableElement; +import edu.kit.datamanager.idoris.attributes.entities.Attribute; +import edu.kit.datamanager.idoris.core.domain.VisitableElement; import edu.kit.datamanager.idoris.rules.logic.RuleOutput; import edu.kit.datamanager.idoris.rules.logic.Visitor; import lombok.AllArgsConstructor; @@ -50,4 +51,4 @@ public class AttributeMapping extends VisitableElement implements Serializable { protected > T accept(Visitor visitor, Object... args) { return visitor.visit(this, args); } -} +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/Operation.java b/src/main/java/edu/kit/datamanager/idoris/operations/entities/Operation.java similarity index 87% rename from src/main/java/edu/kit/datamanager/idoris/domain/entities/Operation.java rename to src/main/java/edu/kit/datamanager/idoris/operations/entities/Operation.java index 6afc8ec..a3c7cc2 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/Operation.java +++ b/src/main/java/edu/kit/datamanager/idoris/operations/entities/Operation.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.entities; +package edu.kit.datamanager.idoris.operations.entities; -import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import edu.kit.datamanager.idoris.attributes.entities.Attribute; +import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; import edu.kit.datamanager.idoris.rules.logic.RuleOutput; import edu.kit.datamanager.idoris.rules.logic.Visitor; import lombok.AllArgsConstructor; @@ -34,7 +35,7 @@ @Setter @AllArgsConstructor @RequiredArgsConstructor -public class Operation extends GenericIDORISEntity { +public class Operation extends AdministrativeMetadata { @Relationship(value = "executableOn", direction = Relationship.Direction.OUTGOING) private Attribute executableOn; @Relationship(value = "returns", direction = Relationship.Direction.INCOMING) diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/OperationStep.java b/src/main/java/edu/kit/datamanager/idoris/operations/entities/OperationStep.java similarity index 88% rename from src/main/java/edu/kit/datamanager/idoris/domain/entities/OperationStep.java rename to src/main/java/edu/kit/datamanager/idoris/operations/entities/OperationStep.java index 3b141a4..d1dfa2d 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/OperationStep.java +++ b/src/main/java/edu/kit/datamanager/idoris/operations/entities/OperationStep.java @@ -14,12 +14,13 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.entities; +package edu.kit.datamanager.idoris.operations.entities; -import edu.kit.datamanager.idoris.domain.VisitableElement; -import edu.kit.datamanager.idoris.domain.enums.ExecutionMode; +import edu.kit.datamanager.idoris.core.domain.VisitableElement; +import edu.kit.datamanager.idoris.operations.entities.enums.ExecutionMode; import edu.kit.datamanager.idoris.rules.logic.RuleOutput; import edu.kit.datamanager.idoris.rules.logic.Visitor; +import edu.kit.datamanager.idoris.technologyinterfaces.entities.TechnologyInterface; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -61,4 +62,4 @@ public class OperationStep extends VisitableElement implements Serializable { protected > T accept(Visitor visitor, Object... args) { return visitor.visit(this, args); } -} +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/enums/ExecutionMode.java b/src/main/java/edu/kit/datamanager/idoris/operations/entities/enums/ExecutionMode.java similarity index 91% rename from src/main/java/edu/kit/datamanager/idoris/domain/enums/ExecutionMode.java rename to src/main/java/edu/kit/datamanager/idoris/operations/entities/enums/ExecutionMode.java index cc91fdd..9096a04 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/enums/ExecutionMode.java +++ b/src/main/java/edu/kit/datamanager/idoris/operations/entities/enums/ExecutionMode.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.enums; +package edu.kit.datamanager.idoris.operations.entities.enums; public enum ExecutionMode { sync, async -} +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/operations/package-info.java b/src/main/java/edu/kit/datamanager/idoris/operations/package-info.java new file mode 100644 index 0000000..edee39e --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/operations/package-info.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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. + */ + +/** + * Operations module for IDORIS. + * This module contains entity definitions, domain services, and business logic related to operations. + * It is responsible for managing operations and operation steps. + * + *

The Operations module depends on the core module for base abstractions and interfaces.

+ */ +@org.springframework.modulith.ApplicationModule( + displayName = "IDORIS Operations", + allowedDependencies = {"core"} +) +package edu.kit.datamanager.idoris.operations; \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/services/AttributeMappingService.java b/src/main/java/edu/kit/datamanager/idoris/operations/services/AttributeMappingService.java similarity index 96% rename from src/main/java/edu/kit/datamanager/idoris/services/AttributeMappingService.java rename to src/main/java/edu/kit/datamanager/idoris/operations/services/AttributeMappingService.java index cef8089..165f1ed 100644 --- a/src/main/java/edu/kit/datamanager/idoris/services/AttributeMappingService.java +++ b/src/main/java/edu/kit/datamanager/idoris/operations/services/AttributeMappingService.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.services; +package edu.kit.datamanager.idoris.operations.services; import edu.kit.datamanager.idoris.core.events.EventPublisherService; -import edu.kit.datamanager.idoris.dao.IAttributeMappingDao; -import edu.kit.datamanager.idoris.domain.entities.AttributeMapping; +import edu.kit.datamanager.idoris.operations.dao.IAttributeMappingDao; +import edu.kit.datamanager.idoris.operations.entities.AttributeMapping; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/edu/kit/datamanager/idoris/services/OperationService.java b/src/main/java/edu/kit/datamanager/idoris/operations/services/OperationService.java similarity index 69% rename from src/main/java/edu/kit/datamanager/idoris/services/OperationService.java rename to src/main/java/edu/kit/datamanager/idoris/operations/services/OperationService.java index b97285d..e642436 100644 --- a/src/main/java/edu/kit/datamanager/idoris/services/OperationService.java +++ b/src/main/java/edu/kit/datamanager/idoris/operations/services/OperationService.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.services; +package edu.kit.datamanager.idoris.operations.services; import edu.kit.datamanager.idoris.core.events.EventPublisherService; -import edu.kit.datamanager.idoris.dao.IOperationDao; -import edu.kit.datamanager.idoris.domain.entities.Operation; +import edu.kit.datamanager.idoris.operations.dao.IOperationDao; +import edu.kit.datamanager.idoris.operations.entities.Operation; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -142,4 +142,54 @@ public Iterable getOperationsForDataType(String dataTypePid) { log.debug("Retrieving Operations for DataType with PID: {}", dataTypePid); return operationDao.getOperationsForDataType(dataTypePid); } + + /** + * Partially updates an existing Operation entity. + * + * @param pid the PID of the Operation to patch + * @param operationPatch the partial Operation entity with fields to update + * @return the patched Operation entity + * @throws IllegalArgumentException if the Operation does not exist + */ + @Transactional + public Operation patchOperation(String pid, Operation operationPatch) { + log.debug("Patching Operation with PID: {}, patch: {}", pid, operationPatch); + if (pid == null || pid.isEmpty()) { + throw new IllegalArgumentException("Operation PID cannot be null or empty"); + } + + // Get the current entity + Operation existing = operationDao.findById(pid) + .orElseThrow(() -> new IllegalArgumentException("Operation not found with PID: " + pid)); + Long previousVersion = existing.getVersion(); + + // Apply non-null fields from the patch to the existing entity + if (operationPatch.getName() != null) { + existing.setName(operationPatch.getName()); + } + if (operationPatch.getDescription() != null) { + existing.setDescription(operationPatch.getDescription()); + } + if (operationPatch.getExecutableOn() != null) { + existing.setExecutableOn(operationPatch.getExecutableOn()); + } + if (operationPatch.getReturns() != null) { + existing.setReturns(operationPatch.getReturns()); + } + if (operationPatch.getEnvironment() != null) { + existing.setEnvironment(operationPatch.getEnvironment()); + } + if (operationPatch.getExecution() != null) { + existing.setExecution(operationPatch.getExecution()); + } + + // Save the updated entity + Operation saved = operationDao.save(existing); + + // Publish the patched event + eventPublisher.publishEntityPatched(saved, previousVersion); + + log.info("Patched Operation with PID: {}", saved.getPid()); + return saved; + } } diff --git a/src/main/java/edu/kit/datamanager/idoris/web/api/IOperationApi.java b/src/main/java/edu/kit/datamanager/idoris/operations/web/api/IOperationApi.java similarity index 84% rename from src/main/java/edu/kit/datamanager/idoris/web/api/IOperationApi.java rename to src/main/java/edu/kit/datamanager/idoris/operations/web/api/IOperationApi.java index 572cbbe..3d0cd11 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/api/IOperationApi.java +++ b/src/main/java/edu/kit/datamanager/idoris/operations/web/api/IOperationApi.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.web.api; +package edu.kit.datamanager.idoris.operations.web.api; -import edu.kit.datamanager.idoris.domain.entities.Operation; +import edu.kit.datamanager.idoris.operations.entities.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -179,4 +179,29 @@ ResponseEntity validate( ResponseEntity>> getOperationsForDataType( @Parameter(description = "PID of the data type", required = true) @RequestParam String pid); -} \ No newline at end of file + + /** + * Partially updates an Operation entity. + * + * @param pid the PID of the Operation to patch + * @param operationPatch the partial Operation entity with fields to update + * @return the patched Operation entity + */ + @PatchMapping("/{pid}") + @io.swagger.v3.oas.annotations.Operation( + summary = "Partially update an Operation", + description = "Updates specific fields of an existing Operation entity", + responses = { + @ApiResponse(responseCode = "200", description = "Operation patched", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = Operation.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "404", description = "Operation not found") + } + ) + ResponseEntity> patchOperation( + @Parameter(description = "PID of the Operation", required = true) + @PathVariable String pid, + @Parameter(description = "Partial Operation with fields to update", required = true) + @RequestBody Operation operationPatch); +} diff --git a/src/main/java/edu/kit/datamanager/idoris/web/hateoas/OperationModelAssembler.java b/src/main/java/edu/kit/datamanager/idoris/operations/web/hateoas/OperationModelAssembler.java similarity index 88% rename from src/main/java/edu/kit/datamanager/idoris/web/hateoas/OperationModelAssembler.java rename to src/main/java/edu/kit/datamanager/idoris/operations/web/hateoas/OperationModelAssembler.java index 3c48f00..fc29d21 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/hateoas/OperationModelAssembler.java +++ b/src/main/java/edu/kit/datamanager/idoris/operations/web/hateoas/OperationModelAssembler.java @@ -14,10 +14,11 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.web.hateoas; +package edu.kit.datamanager.idoris.operations.web.hateoas; -import edu.kit.datamanager.idoris.domain.entities.Operation; -import edu.kit.datamanager.idoris.web.v1.OperationController; +import edu.kit.datamanager.idoris.core.domain.web.hateoas.EntityModelAssembler; +import edu.kit.datamanager.idoris.operations.entities.Operation; +import edu.kit.datamanager.idoris.operations.web.v1.OperationController; import org.springframework.hateoas.EntityModel; import org.springframework.stereotype.Component; diff --git a/src/main/java/edu/kit/datamanager/idoris/web/v1/OperationController.java b/src/main/java/edu/kit/datamanager/idoris/operations/web/v1/OperationController.java similarity index 75% rename from src/main/java/edu/kit/datamanager/idoris/web/v1/OperationController.java rename to src/main/java/edu/kit/datamanager/idoris/operations/web/v1/OperationController.java index f810312..f5c89d5 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/v1/OperationController.java +++ b/src/main/java/edu/kit/datamanager/idoris/operations/web/v1/OperationController.java @@ -14,15 +14,15 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.web.v1; +package edu.kit.datamanager.idoris.operations.web.v1; -import edu.kit.datamanager.idoris.domain.entities.Operation; +import edu.kit.datamanager.idoris.core.domain.exceptions.ValidationException; +import edu.kit.datamanager.idoris.operations.entities.Operation; +import edu.kit.datamanager.idoris.operations.services.OperationService; +import edu.kit.datamanager.idoris.operations.web.api.IOperationApi; +import edu.kit.datamanager.idoris.operations.web.hateoas.OperationModelAssembler; import edu.kit.datamanager.idoris.rules.validation.ValidationPolicyValidator; import edu.kit.datamanager.idoris.rules.validation.ValidationResult; -import edu.kit.datamanager.idoris.services.OperationService; -import edu.kit.datamanager.idoris.web.ValidationException; -import edu.kit.datamanager.idoris.web.api.IOperationApi; -import edu.kit.datamanager.idoris.web.hateoas.OperationModelAssembler; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -268,4 +268,63 @@ public ResponseEntity>> getOperationsForD return ResponseEntity.ok(collectionModel); } + + /** + * {@inheritDoc} + */ + @Override + @PatchMapping("/{pid}") + @io.swagger.v3.oas.annotations.Operation( + summary = "Partially update an Operation", + description = "Updates specific fields of an existing Operation entity", + responses = { + @ApiResponse(responseCode = "200", description = "Operation patched", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = Operation.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "404", description = "Operation not found") + } + ) + public ResponseEntity> patchOperation( + @Parameter(description = "PID of the Operation", required = true) + @PathVariable String pid, + @Parameter(description = "Partial Operation with fields to update", required = true) + @RequestBody Operation operationPatch) { + if (!operationService.getOperation(pid).isPresent()) { + return ResponseEntity.notFound().build(); + } + + // Validate the patch if it contains fields that need validation + if (operationPatch.getExecutableOn() != null || + operationPatch.getReturns() != null || + operationPatch.getEnvironment() != null || + operationPatch.getExecution() != null) { + + // Get the current entity + Operation existing = operationService.getOperation(pid).get(); + + // Create a merged entity for validation + Operation merged = new Operation(); + merged.setPid(existing.getPid()); + merged.setName(operationPatch.getName() != null ? operationPatch.getName() : existing.getName()); + merged.setDescription(operationPatch.getDescription() != null ? operationPatch.getDescription() : existing.getDescription()); + merged.setExecutableOn(operationPatch.getExecutableOn() != null ? operationPatch.getExecutableOn() : existing.getExecutableOn()); + merged.setReturns(operationPatch.getReturns() != null ? operationPatch.getReturns() : existing.getReturns()); + merged.setEnvironment(operationPatch.getEnvironment() != null ? operationPatch.getEnvironment() : existing.getEnvironment()); + merged.setExecution(operationPatch.getExecution() != null ? operationPatch.getExecution() : existing.getExecution()); + + // Validate the merged entity + ValidationPolicyValidator validator = new ValidationPolicyValidator(); + ValidationResult validationResult = merged.execute(validator); + + // Check if validation failed + if (!validationResult.isValid()) { + throw new ValidationException("Operation validation failed", validationResult); + } + } + + Operation patchedOperation = operationService.patchOperation(pid, operationPatch); + EntityModel entityModel = operationModelAssembler.toModel(patchedOperation); + return ResponseEntity.ok(entityModel); + } } diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/ConfigurablePIDGenerator.java b/src/main/java/edu/kit/datamanager/idoris/pids/ConfigurablePIDGenerator.java index 11ce46e..49666df 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/ConfigurablePIDGenerator.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/ConfigurablePIDGenerator.java @@ -17,7 +17,7 @@ package edu.kit.datamanager.idoris.pids; import edu.kit.datamanager.idoris.configuration.ApplicationProperties; -import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.ObjectProvider; @@ -50,7 +50,7 @@ public ConfigurablePIDGenerator(ApplicationProperties applicationProperties, * @param primaryLabel The primary label of the entity. * @param entity The entity for which the PID is generated. * @return A generated PID as a String. - * @throws IllegalArgumentException if the primary label is null or empty, or if the entity is not an instance of GenericIDORISEntity. + * @throws IllegalArgumentException if the primary label is null or empty, or if the entity is not an instance of AdministrativeMetadata. * @throws IllegalStateException if the configured PID generation strategy is not available. */ @Override @@ -68,9 +68,9 @@ public String generateId(String primaryLabel, Object entity) { log.error("Primary label is null or empty"); throw new IllegalArgumentException("Primary label must not be null or empty."); } - if (!(entity instanceof GenericIDORISEntity)) { - log.error("Entity is null or not an instance of GenericIDORISEntity"); - throw new IllegalArgumentException("Entity must be a non-null instance of GenericIDORISEntity."); + if (!(entity instanceof AdministrativeMetadata)) { + log.error("Entity is null or not an instance of AdministrativeMetadata"); + throw new IllegalArgumentException("Entity must be a non-null instance of AdministrativeMetadata."); } switch (strategy) { @@ -97,4 +97,4 @@ public String generateId(String primaryLabel, Object entity) { } } } -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/PIDGenerationEventListener.java b/src/main/java/edu/kit/datamanager/idoris/pids/PIDGenerationEventListener.java index d313efb..82ac7eb 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/PIDGenerationEventListener.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/PIDGenerationEventListener.java @@ -16,9 +16,9 @@ package edu.kit.datamanager.idoris.pids; +import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; import edu.kit.datamanager.idoris.core.events.EntityCreatedEvent; import edu.kit.datamanager.idoris.core.events.EventPublisherService; -import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @@ -55,8 +55,8 @@ public PIDGenerationEventListener(TypedPIDMakerIDGenerator pidGenerator, EventPu */ @EventListener @Transactional - public void handleEntityCreatedEvent(EntityCreatedEvent event) { - GenericIDORISEntity entity = event.getEntity(); + public void handleEntityCreatedEvent(EntityCreatedEvent event) { + AdministrativeMetadata entity = event.getEntity(); log.debug("Handling EntityCreatedEvent for entity: {}", entity); if (entity.getPid() == null || entity.getPid().isEmpty()) { @@ -71,4 +71,4 @@ public void handleEntityCreatedEvent(EntityCreatedEvent eve log.debug("Entity already has a PID: {}", entity.getPid()); } } -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/TypedPIDMakerIDGenerator.java b/src/main/java/edu/kit/datamanager/idoris/pids/TypedPIDMakerIDGenerator.java index 9f4b67c..7af8ef5 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/TypedPIDMakerIDGenerator.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/TypedPIDMakerIDGenerator.java @@ -19,11 +19,11 @@ import com.google.common.base.Ascii; import edu.kit.datamanager.idoris.configuration.ApplicationProperties; import edu.kit.datamanager.idoris.configuration.TypedPIDMakerConfig; -import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; -import edu.kit.datamanager.idoris.domain.entities.ORCiDUser; +import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; import edu.kit.datamanager.idoris.pids.client.TypedPIDMakerClient; import edu.kit.datamanager.idoris.pids.client.model.PIDRecord; import edu.kit.datamanager.idoris.pids.client.model.PIDRecordEntry; +import edu.kit.datamanager.idoris.users.entities.ORCiDUser; import jakarta.annotation.Nonnull; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -38,7 +38,7 @@ /** * ID generator that uses the Typed PID Maker service to generate PIDs. - * It also creates PID records with metadata from the GenericIDORISEntity. + * It also creates PID records with metadata from the AdministrativeMetadata. */ @Component @Slf4j @@ -83,8 +83,8 @@ public TypedPIDMakerIDGenerator(ApplicationProperties applicationProperties, Typ @Override @Nonnull public String generateId(String primaryLabel, Object entity) { - if (!(entity instanceof GenericIDORISEntity idorisEntity)) { - log.warn("Entity is not a GenericIDORISEntity, falling back to UUID generation"); + if (!(entity instanceof AdministrativeMetadata idorisEntity)) { + log.warn("Entity is not a AdministrativeMetadata, falling back to UUID generation"); return java.util.UUID.randomUUID().toString(); } @@ -123,13 +123,13 @@ public String generateId(String primaryLabel, Object entity) { } /** - * Creates a PID record with metadata from the GenericIDORISEntity. + * Creates a PID record with metadata from the AdministrativeMetadata. * Note: This method only adds metadata if the Helmholtz Kernel Information Profile allows it. * - * @param entity The GenericIDORISEntity + * @param entity The AdministrativeMetadata * @return The PID record */ - private PIDRecord createPIDRecord(GenericIDORISEntity entity) { + private PIDRecord createPIDRecord(AdministrativeMetadata entity) { List recordEntries = new ArrayList<>(); // Only add metadata if configured to do so diff --git a/src/main/java/edu/kit/datamanager/idoris/web/v1/PidRedirectController.java b/src/main/java/edu/kit/datamanager/idoris/pids/web/v1/PidRedirectController.java similarity index 91% rename from src/main/java/edu/kit/datamanager/idoris/web/v1/PidRedirectController.java rename to src/main/java/edu/kit/datamanager/idoris/pids/web/v1/PidRedirectController.java index bb9cc61..3a62d2b 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/v1/PidRedirectController.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/web/v1/PidRedirectController.java @@ -13,10 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package edu.kit.datamanager.idoris.web.v1; +package edu.kit.datamanager.idoris.pids.web.v1; import com.google.common.base.Ascii; -import edu.kit.datamanager.idoris.dao.*; +import edu.kit.datamanager.idoris.attributes.dao.IAttributeDao; +import edu.kit.datamanager.idoris.core.domain.dao.IGenericRepo; +import edu.kit.datamanager.idoris.datatypes.dao.IAtomicDataTypeDao; +import edu.kit.datamanager.idoris.datatypes.dao.ITypeProfileDao; +import edu.kit.datamanager.idoris.operations.dao.IOperationDao; +import edu.kit.datamanager.idoris.technologyinterfaces.dao.ITechnologyInterfaceDao; import lombok.extern.java.Log; import org.neo4j.driver.Value; import org.springframework.data.neo4j.core.Neo4jClient; @@ -34,9 +39,7 @@ @RequestMapping("/v1/pid") @Log public class PidRedirectController { - private static final String FIND_ENTITY_BY_PID_QUERY = "MATCH (n {pid: $pid}) RETURN n.pid AS pid, labels(n) AS nodeLabels LIMIT 1"; - private final Neo4jClient neo4jClient; private final Map> labelToDaoMap; @@ -47,7 +50,6 @@ public PidRedirectController(Neo4jClient neo4jClient, ITechnologyInterfaceDao technologyInterfaceDao, ITypeProfileDao typeProfileDao) { this.neo4jClient = neo4jClient; - Map> mapBuilder = new HashMap<>(); mapBuilder.put("AtomicDataType", atomicDataTypeDao); mapBuilder.put("Attribute", attributeDao); @@ -68,29 +70,22 @@ public PidRedirectController(Neo4jClient neo4jClient, @GetMapping("/{pidValue}") public ResponseEntity redirectToEntity(@PathVariable("pidValue") String pidValue) { Optional> entityDataOptional = fetchNeo4jData(pidValue); - if (entityDataOptional.isEmpty()) { log.warning("No entity data found in Neo4j for PID: " + pidValue); return ResponseEntity.notFound().build(); } - Map entityData = entityDataOptional.get(); String entityPid = entityData.containsKey("pid") ? (String) entityData.get("pid") : null; - if (entityPid == null || entityPid.isEmpty()) { log.warning("Extracted PID is null or empty for input: " + pidValue); return ResponseEntity.notFound().build(); } - List nodeLabels = extractAndFilterLabels(entityData); - if (nodeLabels.isEmpty()) { log.warning("No suitable labels found for PID: " + pidValue + " after filtering."); return ResponseEntity.notFound().build(); } - Optional redirectPathBaseOptional = determineRedirectPathBase(entityPid, nodeLabels); - if (redirectPathBaseOptional.isPresent()) { String redirectUrl = String.format("/%s/%s", redirectPathBaseOptional.get(), entityPid); return ResponseEntity.status(HttpStatus.FOUND) @@ -116,7 +111,7 @@ private Optional> fetchNeo4jData(String pidValue) { /** * Extracts and filters labels from the entity data. - * Filters out null, empty, "GenericIDORISEntity", and labels starting with "_". + * Filters out null, empty, "AdministrativeMetadata", and labels starting with "_". * * @param entityData The entity data map containing labels. * @return A list of filtered labels. @@ -127,12 +122,11 @@ private List extractAndFilterLabels(Map entityData) { log.fine("nodeLabels field is missing or not of type Value for entity data: " + entityData); return Collections.emptyList(); } - return nodeLabelsValue.asList(Value::asString) .stream() .filter(label -> label != null && !label.isEmpty()) .map(String::trim) - .filter(label -> !label.equals("GenericIDORISEntity") && !label.startsWith("_")) + .filter(label -> !label.equals("AdministrativeMetadata") && !label.startsWith("_")) .toList(); } diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/logic/Visitor.java b/src/main/java/edu/kit/datamanager/idoris/rules/logic/Visitor.java index 37b5da0..f2f449b 100644 --- a/src/main/java/edu/kit/datamanager/idoris/rules/logic/Visitor.java +++ b/src/main/java/edu/kit/datamanager/idoris/rules/logic/Visitor.java @@ -16,8 +16,13 @@ package edu.kit.datamanager.idoris.rules.logic; -import edu.kit.datamanager.idoris.domain.VisitableElement; -import edu.kit.datamanager.idoris.domain.entities.*; +import edu.kit.datamanager.idoris.attributes.entities.Attribute; +import edu.kit.datamanager.idoris.datatypes.entities.AtomicDataType; +import edu.kit.datamanager.idoris.datatypes.entities.TypeProfile; +import edu.kit.datamanager.idoris.operations.entities.AttributeMapping; +import edu.kit.datamanager.idoris.operations.entities.Operation; +import edu.kit.datamanager.idoris.operations.entities.OperationStep; +import edu.kit.datamanager.idoris.technologyinterfaces.entities.TechnologyInterface; import jakarta.validation.constraints.NotNull; import lombok.extern.slf4j.Slf4j; @@ -82,6 +87,7 @@ public T visit(Attribute attribute, Object... args) { return notAllowed(attribute); } + /** * Visits an AttributeMapping element and processes it. * Default implementation treats this as a not allowed element type. @@ -220,4 +226,4 @@ protected final T save(String id, T result) { cache.put(id, result); return result; } -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/validation/InheritanceValidator.java b/src/main/java/edu/kit/datamanager/idoris/rules/validation/InheritanceValidator.java index eaa6004..fa17ec3 100644 --- a/src/main/java/edu/kit/datamanager/idoris/rules/validation/InheritanceValidator.java +++ b/src/main/java/edu/kit/datamanager/idoris/rules/validation/InheritanceValidator.java @@ -16,9 +16,9 @@ package edu.kit.datamanager.idoris.rules.validation; -import edu.kit.datamanager.idoris.domain.entities.AtomicDataType; -import edu.kit.datamanager.idoris.domain.entities.Attribute; -import edu.kit.datamanager.idoris.domain.entities.TypeProfile; +import edu.kit.datamanager.idoris.attributes.entities.Attribute; +import edu.kit.datamanager.idoris.datatypes.entities.AtomicDataType; +import edu.kit.datamanager.idoris.datatypes.entities.TypeProfile; import edu.kit.datamanager.idoris.rules.logic.Rule; import edu.kit.datamanager.idoris.rules.logic.RuleTask; import lombok.extern.slf4j.Slf4j; @@ -144,4 +144,4 @@ public ValidationResult visit(TypeProfile typeProfile, Object... args) { return result; } -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/validation/SyntaxValidator.java b/src/main/java/edu/kit/datamanager/idoris/rules/validation/SyntaxValidator.java index fc26390..2422f17 100644 --- a/src/main/java/edu/kit/datamanager/idoris/rules/validation/SyntaxValidator.java +++ b/src/main/java/edu/kit/datamanager/idoris/rules/validation/SyntaxValidator.java @@ -16,12 +16,19 @@ package edu.kit.datamanager.idoris.rules.validation; -import edu.kit.datamanager.idoris.domain.entities.*; -import edu.kit.datamanager.idoris.domain.enums.CombinationOptions; -import edu.kit.datamanager.idoris.domain.enums.ExecutionMode; -import edu.kit.datamanager.idoris.domain.enums.PrimitiveDataTypes; +import edu.kit.datamanager.idoris.attributes.entities.Attribute; +import edu.kit.datamanager.idoris.core.domain.enums.CombinationOptions; +import edu.kit.datamanager.idoris.datatypes.entities.AtomicDataType; +import edu.kit.datamanager.idoris.datatypes.entities.DataType; +import edu.kit.datamanager.idoris.datatypes.entities.TypeProfile; +import edu.kit.datamanager.idoris.datatypes.enums.PrimitiveDataTypes; +import edu.kit.datamanager.idoris.operations.entities.AttributeMapping; +import edu.kit.datamanager.idoris.operations.entities.Operation; +import edu.kit.datamanager.idoris.operations.entities.OperationStep; +import edu.kit.datamanager.idoris.operations.entities.enums.ExecutionMode; import edu.kit.datamanager.idoris.rules.logic.Rule; import edu.kit.datamanager.idoris.rules.logic.RuleTask; +import edu.kit.datamanager.idoris.technologyinterfaces.entities.TechnologyInterface; import lombok.extern.slf4j.Slf4j; import java.util.Arrays; @@ -343,4 +350,4 @@ private void validateDataType(DataType dataType, ValidationResult result) { dataType, WARNING); } } -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationPolicyValidator.java b/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationPolicyValidator.java index 25d5935..f551f23 100644 --- a/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationPolicyValidator.java +++ b/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationPolicyValidator.java @@ -16,9 +16,9 @@ package edu.kit.datamanager.idoris.rules.validation; -import edu.kit.datamanager.idoris.domain.entities.Attribute; -import edu.kit.datamanager.idoris.domain.entities.TypeProfile; -import edu.kit.datamanager.idoris.domain.enums.CombinationOptions; +import edu.kit.datamanager.idoris.attributes.entities.Attribute; +import edu.kit.datamanager.idoris.core.domain.enums.CombinationOptions; +import edu.kit.datamanager.idoris.datatypes.entities.TypeProfile; import edu.kit.datamanager.idoris.rules.logic.Rule; import edu.kit.datamanager.idoris.rules.logic.RuleTask; import lombok.extern.slf4j.Slf4j; @@ -147,4 +147,4 @@ private Object getTypeProfileAndParentElementaryInformation(TypeProfile typeProf */ private record ElementaryInformation(String pid, String name, CombinationOptions validationPolicy) { } -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationVisitor.java b/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationVisitor.java index 13ac031..851ab59 100644 --- a/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationVisitor.java +++ b/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationVisitor.java @@ -30,7 +30,7 @@ * need validation capabilities.

*/ -import edu.kit.datamanager.idoris.domain.VisitableElement; +import edu.kit.datamanager.idoris.core.domain.VisitableElement; import edu.kit.datamanager.idoris.rules.logic.IRule; import edu.kit.datamanager.idoris.rules.logic.Visitor; diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/ITechnologyInterfaceDao.java b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/dao/ITechnologyInterfaceDao.java similarity index 77% rename from src/main/java/edu/kit/datamanager/idoris/dao/ITechnologyInterfaceDao.java rename to src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/dao/ITechnologyInterfaceDao.java index e69779e..f140639 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/ITechnologyInterfaceDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/dao/ITechnologyInterfaceDao.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.dao; +package edu.kit.datamanager.idoris.technologyinterfaces.dao; -import edu.kit.datamanager.idoris.domain.entities.TechnologyInterface; +import edu.kit.datamanager.idoris.core.domain.dao.IGenericRepo; +import edu.kit.datamanager.idoris.technologyinterfaces.entities.TechnologyInterface; public interface ITechnologyInterfaceDao extends IGenericRepo { -} +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/TechnologyInterface.java b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/entities/TechnologyInterface.java similarity index 85% rename from src/main/java/edu/kit/datamanager/idoris/domain/entities/TechnologyInterface.java rename to src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/entities/TechnologyInterface.java index e311354..004b5e2 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/TechnologyInterface.java +++ b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/entities/TechnologyInterface.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.entities; +package edu.kit.datamanager.idoris.technologyinterfaces.entities; -import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import edu.kit.datamanager.idoris.attributes.entities.Attribute; +import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; import edu.kit.datamanager.idoris.rules.logic.RuleOutput; import edu.kit.datamanager.idoris.rules.logic.Visitor; import lombok.AllArgsConstructor; @@ -33,7 +34,7 @@ @Setter @RequiredArgsConstructor @AllArgsConstructor -public class TechnologyInterface extends GenericIDORISEntity { +public class TechnologyInterface extends AdministrativeMetadata { @Relationship(value = "attributes", direction = Relationship.Direction.INCOMING) private Set attributes; @@ -47,4 +48,4 @@ public class TechnologyInterface extends GenericIDORISEntity { protected > T accept(Visitor visitor, Object... args) { return visitor.visit(this, args); } -} +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/package-info.java b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/package-info.java new file mode 100644 index 0000000..0882aa6 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/package-info.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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. + */ + +/** + * Technology Interfaces module for IDORIS. + * This module contains entity definitions, domain services, and business logic related to technology interfaces. + * It is responsible for managing technology interfaces and their relationships. + * + *

The Technology Interfaces module depends on the core module for base abstractions and interfaces.

+ */ +@org.springframework.modulith.ApplicationModule( + displayName = "IDORIS Technology Interfaces", + allowedDependencies = {"core"} +) +package edu.kit.datamanager.idoris.technologyinterfaces; \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/services/TechnologyInterfaceService.java b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/services/TechnologyInterfaceService.java similarity index 68% rename from src/main/java/edu/kit/datamanager/idoris/services/TechnologyInterfaceService.java rename to src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/services/TechnologyInterfaceService.java index 3ea80ba..354eb1f 100644 --- a/src/main/java/edu/kit/datamanager/idoris/services/TechnologyInterfaceService.java +++ b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/services/TechnologyInterfaceService.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.services; +package edu.kit.datamanager.idoris.technologyinterfaces.services; import edu.kit.datamanager.idoris.core.events.EventPublisherService; -import edu.kit.datamanager.idoris.dao.ITechnologyInterfaceDao; -import edu.kit.datamanager.idoris.domain.entities.TechnologyInterface; +import edu.kit.datamanager.idoris.technologyinterfaces.dao.ITechnologyInterfaceDao; +import edu.kit.datamanager.idoris.technologyinterfaces.entities.TechnologyInterface; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -130,4 +130,51 @@ public List getAllTechnologyInterfaces() { log.debug("Retrieving all TechnologyInterfaces"); return technologyInterfaceDao.findAll(); } -} \ No newline at end of file + + /** + * Partially updates an existing TechnologyInterface entity. + * + * @param pid the PID of the TechnologyInterface to patch + * @param technologyInterfacePatch the partial TechnologyInterface entity with fields to update + * @return the patched TechnologyInterface entity + * @throws IllegalArgumentException if the TechnologyInterface does not exist + */ + @Transactional + public TechnologyInterface patchTechnologyInterface(String pid, TechnologyInterface technologyInterfacePatch) { + log.debug("Patching TechnologyInterface with PID: {}, patch: {}", pid, technologyInterfacePatch); + if (pid == null || pid.isEmpty()) { + throw new IllegalArgumentException("TechnologyInterface PID cannot be null or empty"); + } + + // Get the current entity + TechnologyInterface existing = technologyInterfaceDao.findById(pid) + .orElseThrow(() -> new IllegalArgumentException("TechnologyInterface not found with PID: " + pid)); + Long previousVersion = existing.getVersion(); + + // Apply non-null fields from the patch to the existing entity + if (technologyInterfacePatch.getName() != null) { + existing.setName(technologyInterfacePatch.getName()); + } + if (technologyInterfacePatch.getDescription() != null) { + existing.setDescription(technologyInterfacePatch.getDescription()); + } + if (technologyInterfacePatch.getAttributes() != null) { + existing.setAttributes(technologyInterfacePatch.getAttributes()); + } + if (technologyInterfacePatch.getOutputs() != null) { + existing.setOutputs(technologyInterfacePatch.getOutputs()); + } + if (technologyInterfacePatch.getAdapters() != null) { + existing.setAdapters(technologyInterfacePatch.getAdapters()); + } + + // Save the updated entity + TechnologyInterface saved = technologyInterfaceDao.save(existing); + + // Publish the patched event + eventPublisher.publishEntityPatched(saved, previousVersion); + + log.info("Patched TechnologyInterface with PID: {}", saved.getPid()); + return saved; + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/web/api/ITechnologyInterfaceApi.java b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/web/api/ITechnologyInterfaceApi.java similarity index 83% rename from src/main/java/edu/kit/datamanager/idoris/web/api/ITechnologyInterfaceApi.java rename to src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/web/api/ITechnologyInterfaceApi.java index d4eaedd..64627a4 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/api/ITechnologyInterfaceApi.java +++ b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/web/api/ITechnologyInterfaceApi.java @@ -14,10 +14,10 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.web.api; +package edu.kit.datamanager.idoris.technologyinterfaces.web.api; -import edu.kit.datamanager.idoris.domain.entities.Attribute; -import edu.kit.datamanager.idoris.domain.entities.TechnologyInterface; +import edu.kit.datamanager.idoris.attributes.entities.Attribute; +import edu.kit.datamanager.idoris.technologyinterfaces.entities.TechnologyInterface; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -181,4 +181,29 @@ ResponseEntity> updateTechnologyInterface( ResponseEntity deleteTechnologyInterface( @Parameter(description = "PID of the TechnologyInterface", required = true) @PathVariable String pid); -} \ No newline at end of file + + /** + * Partially updates a TechnologyInterface entity. + * + * @param pid the PID of the TechnologyInterface to patch + * @param technologyInterfacePatch the partial TechnologyInterface entity with fields to update + * @return the patched TechnologyInterface entity + */ + @PatchMapping("/{pid}") + @Operation( + summary = "Partially update a TechnologyInterface", + description = "Updates specific fields of an existing TechnologyInterface entity", + responses = { + @ApiResponse(responseCode = "200", description = "TechnologyInterface patched", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = TechnologyInterface.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "404", description = "TechnologyInterface not found") + } + ) + ResponseEntity> patchTechnologyInterface( + @Parameter(description = "PID of the TechnologyInterface", required = true) + @PathVariable String pid, + @Parameter(description = "Partial TechnologyInterface with fields to update", required = true) + @RequestBody TechnologyInterface technologyInterfacePatch); +} diff --git a/src/main/java/edu/kit/datamanager/idoris/web/hateoas/TechnologyInterfaceModelAssembler.java b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/web/hateoas/TechnologyInterfaceModelAssembler.java similarity index 87% rename from src/main/java/edu/kit/datamanager/idoris/web/hateoas/TechnologyInterfaceModelAssembler.java rename to src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/web/hateoas/TechnologyInterfaceModelAssembler.java index 1b964ec..c26b9b3 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/hateoas/TechnologyInterfaceModelAssembler.java +++ b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/web/hateoas/TechnologyInterfaceModelAssembler.java @@ -14,10 +14,11 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.web.hateoas; +package edu.kit.datamanager.idoris.technologyinterfaces.web.hateoas; -import edu.kit.datamanager.idoris.domain.entities.TechnologyInterface; -import edu.kit.datamanager.idoris.web.v1.TechnologyInterfaceController; +import edu.kit.datamanager.idoris.core.domain.web.hateoas.EntityModelAssembler; +import edu.kit.datamanager.idoris.technologyinterfaces.entities.TechnologyInterface; +import edu.kit.datamanager.idoris.technologyinterfaces.web.v1.TechnologyInterfaceController; import org.springframework.hateoas.EntityModel; import org.springframework.stereotype.Component; diff --git a/src/main/java/edu/kit/datamanager/idoris/web/v1/TechnologyInterfaceController.java b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/web/v1/TechnologyInterfaceController.java similarity index 84% rename from src/main/java/edu/kit/datamanager/idoris/web/v1/TechnologyInterfaceController.java rename to src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/web/v1/TechnologyInterfaceController.java index c945499..7b9ef84 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/v1/TechnologyInterfaceController.java +++ b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/web/v1/TechnologyInterfaceController.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.web.v1; - -import edu.kit.datamanager.idoris.domain.entities.Attribute; -import edu.kit.datamanager.idoris.domain.entities.TechnologyInterface; -import edu.kit.datamanager.idoris.services.TechnologyInterfaceService; -import edu.kit.datamanager.idoris.web.api.ITechnologyInterfaceApi; -import edu.kit.datamanager.idoris.web.hateoas.AttributeModelAssembler; -import edu.kit.datamanager.idoris.web.hateoas.TechnologyInterfaceModelAssembler; +package edu.kit.datamanager.idoris.technologyinterfaces.web.v1; + +import edu.kit.datamanager.idoris.attributes.entities.Attribute; +import edu.kit.datamanager.idoris.attributes.web.hateoas.AttributeModelAssembler; +import edu.kit.datamanager.idoris.technologyinterfaces.entities.TechnologyInterface; +import edu.kit.datamanager.idoris.technologyinterfaces.services.TechnologyInterfaceService; +import edu.kit.datamanager.idoris.technologyinterfaces.web.api.ITechnologyInterfaceApi; +import edu.kit.datamanager.idoris.technologyinterfaces.web.hateoas.TechnologyInterfaceModelAssembler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.hateoas.CollectionModel; import org.springframework.hateoas.EntityModel; @@ -163,4 +163,18 @@ public ResponseEntity deleteTechnologyInterface(String pid) { technologyInterfaceService.deleteTechnologyInterface(pid); return ResponseEntity.noContent().build(); } -} \ No newline at end of file + + /** + * {@inheritDoc} + */ + @Override + public ResponseEntity> patchTechnologyInterface(String pid, TechnologyInterface technologyInterfacePatch) { + if (!technologyInterfaceService.getTechnologyInterface(pid).isPresent()) { + return ResponseEntity.notFound().build(); + } + + TechnologyInterface patchedTechnologyInterface = technologyInterfaceService.patchTechnologyInterface(pid, technologyInterfacePatch); + EntityModel entityModel = technologyInterfaceModelAssembler.toModel(patchedTechnologyInterface); + return ResponseEntity.ok(entityModel); + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/IUserDao.java b/src/main/java/edu/kit/datamanager/idoris/users/dao/IUserDao.java similarity index 93% rename from src/main/java/edu/kit/datamanager/idoris/dao/IUserDao.java rename to src/main/java/edu/kit/datamanager/idoris/users/dao/IUserDao.java index 3dbd3ff..cf50b31 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/IUserDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/users/dao/IUserDao.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.dao; +package edu.kit.datamanager.idoris.users.dao; -import edu.kit.datamanager.idoris.domain.entities.User; +import edu.kit.datamanager.idoris.users.entities.User; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.query.Query; import org.springframework.data.repository.ListCrudRepository; @@ -34,4 +34,4 @@ public interface IUserDao extends Neo4jRepository, ListCrudReposit @Query("MATCH (u:TextUser) WHERE u.email = $email RETURN u") User findTextUserByEmail(String email); -} +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/ORCiDUser.java b/src/main/java/edu/kit/datamanager/idoris/users/entities/ORCiDUser.java similarity index 94% rename from src/main/java/edu/kit/datamanager/idoris/domain/entities/ORCiDUser.java rename to src/main/java/edu/kit/datamanager/idoris/users/entities/ORCiDUser.java index 44e58dc..b270df0 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/ORCiDUser.java +++ b/src/main/java/edu/kit/datamanager/idoris/users/entities/ORCiDUser.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.entities; +package edu.kit.datamanager.idoris.users.entities; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; @@ -31,4 +31,4 @@ public final class ORCiDUser extends User { @JsonProperty("orcid") private URL orcid; -} +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/TextUser.java b/src/main/java/edu/kit/datamanager/idoris/users/entities/TextUser.java similarity index 88% rename from src/main/java/edu/kit/datamanager/idoris/domain/entities/TextUser.java rename to src/main/java/edu/kit/datamanager/idoris/users/entities/TextUser.java index 016c301..9a1c152 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/TextUser.java +++ b/src/main/java/edu/kit/datamanager/idoris/users/entities/TextUser.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Karlsruhe Institute of Technology + * Copyright (c) 2024-2025 Karlsruhe Institute of Technology * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.entities; +package edu.kit.datamanager.idoris.users.entities; import lombok.AllArgsConstructor; import lombok.Getter; @@ -29,4 +29,4 @@ public final class TextUser extends User { private String name; private String email; private String details; -} +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/User.java b/src/main/java/edu/kit/datamanager/idoris/users/entities/User.java similarity index 80% rename from src/main/java/edu/kit/datamanager/idoris/domain/entities/User.java rename to src/main/java/edu/kit/datamanager/idoris/users/entities/User.java index a3c69ea..97cb58e 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/User.java +++ b/src/main/java/edu/kit/datamanager/idoris/users/entities/User.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.entities; +package edu.kit.datamanager.idoris.users.entities; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; @@ -38,10 +38,10 @@ @RequiredArgsConstructor @JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION, property = "type") @JsonSubTypes({ - @JsonSubTypes.Type(value = ORCiDUser.class, name = "orcid"), - @JsonSubTypes.Type(value = TextUser.class, name = "text") + @JsonSubTypes.Type(value = edu.kit.datamanager.idoris.users.entities.ORCiDUser.class, name = "orcid"), + @JsonSubTypes.Type(value = edu.kit.datamanager.idoris.users.entities.TextUser.class, name = "text") }) -public abstract sealed class User implements Serializable permits ORCiDUser, TextUser { +public abstract sealed class User implements Serializable permits edu.kit.datamanager.idoris.users.entities.ORCiDUser, edu.kit.datamanager.idoris.users.entities.TextUser { @CreatedDate Instant createdAt; @Id @@ -49,4 +49,3 @@ public abstract sealed class User implements Serializable permits ORCiDUser, Tex private String internalId; private String type; } - diff --git a/src/main/java/edu/kit/datamanager/idoris/users/package-info.java b/src/main/java/edu/kit/datamanager/idoris/users/package-info.java new file mode 100644 index 0000000..2c5c0a1 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/users/package-info.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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. + */ + +/** + * Users module for IDORIS. + * This module contains entity definitions, domain services, and business logic related to users. + * It is responsible for managing users and their relationships. + * + *

The Users module depends on the core module for base abstractions and interfaces.

+ */ +@org.springframework.modulith.ApplicationModule( + displayName = "IDORIS Users", + allowedDependencies = {"core"} +) +package edu.kit.datamanager.idoris.users; \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3a5b36a..614942a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -20,7 +20,6 @@ logging.level.edu.kit.datamanager=DEBUG spring.neo4j.uri=bolt://localhost:7687 spring.neo4j.authentication.username=neo4j spring.neo4j.authentication.password=superSecret -management.endpoints.access.default=unrestricted management.endpoints.web.exposure.include=* # Spring Doc Settings for OpenAPI documentation springdoc.show-actuator=true From 1da2ae1ef5e95a67b434bc8bb3979d806a6db30b Mon Sep 17 00:00:00 2001 From: Maximilian Inckmann Date: Thu, 17 Jul 2025 10:38:52 +0200 Subject: [PATCH 04/19] added user endpoint Signed-off-by: Maximilian Inckmann --- catalog-info.yaml | 13 +- .../idoris/attributes/entities/Attribute.java | 2 +- .../configuration/ETagControllerAdvice.java | 2 +- .../idoris/configuration/OpenAPIConfig.java | 18 +- .../configuration/TypedPIDMakerConfig.java | 8 +- .../idoris/core/domain/dao/IGenericRepo.java | 2 +- .../AdministrativeMetadata.java | 4 +- .../web/hateoas/EntityModelAssembler.java | 2 +- .../core/events/EntityCreatedEvent.java | 2 +- .../core/events/EntityDeletedEvent.java | 2 +- .../core/events/EntityImportedEvent.java | 2 +- .../core/events/EntityPatchedEvent.java | 2 +- .../core/events/EntityUpdatedEvent.java | 2 +- .../core/events/EventPublisherService.java | 2 +- .../idoris/core/events/PIDGeneratedEvent.java | 2 +- .../core/events/SchemaGeneratedEvent.java | 2 +- .../core/events/VersionCreatedEvent.java | 2 +- .../idoris/datatypes/entities/DataType.java | 2 +- .../notification/EntityChangeNotifier.java | 2 +- .../notification/EntityChangeSubscriber.java | 2 +- .../LoggingEntityChangeSubscriber.java | 2 +- .../idoris/operations/entities/Operation.java | 2 +- .../idoris/pids/ConfigurablePIDGenerator.java | 2 +- .../pids/PIDGenerationEventListener.java | 2 +- .../idoris/pids/TypedPIDMakerIDGenerator.java | 2 +- .../entities/TechnologyInterface.java | 2 +- .../idoris/users/services/UserService.java | 111 +++++++ .../users/services/UserServiceImpl.java | 121 ++++++++ .../idoris/users/web/api/IUserApi.java | 236 +++++++++++++++ .../users/web/hateoas/UserModelAssembler.java | 63 ++++ .../idoris/users/web/v1/UserController.java | 189 ++++++++++++ .../users/services/UserServiceImplTest.java | 189 ++++++++++++ .../users/web/v1/UserControllerTest.java | 271 ++++++++++++++++++ 33 files changed, 1223 insertions(+), 44 deletions(-) rename src/main/java/edu/kit/datamanager/idoris/core/domain/{ => entities}/AdministrativeMetadata.java (93%) create mode 100644 src/main/java/edu/kit/datamanager/idoris/users/services/UserService.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/users/services/UserServiceImpl.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/users/web/api/IUserApi.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/users/web/hateoas/UserModelAssembler.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/users/web/v1/UserController.java create mode 100644 src/test/java/edu/kit/datamanager/idoris/users/services/UserServiceImplTest.java create mode 100644 src/test/java/edu/kit/datamanager/idoris/users/web/v1/UserControllerTest.java diff --git a/catalog-info.yaml b/catalog-info.yaml index d0a6c11..f175bb3 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -28,11 +28,10 @@ apiVersion: backstage.io/v1alpha1 kind: API metadata: name: idoris-rest-api - description: A placeholder for the HATEOAS compliant REST API of IDORIS + description: The HATEOAS compliant REST API of IDORIS tags: - rest - hateoas - - alps - openapi spec: type: openapi @@ -40,15 +39,7 @@ spec: owner: user:maximiliani system: idoris definition: | - openapi: "3.0.0" - info: - version: 0.0.1 - title: IDORIS API - license: - name: Apache 2.0 - servers: - - url: http://localhost:8095/api - paths: + {"openapi":"3.1.0","info":{"title":"IDORIS API","description":"API for the Integrated Data Type and Operations Registry with Inheritance System (IDORIS). This API provides endpoints for managing data types, operations, and their relationships within IDORIS.","contact":{"name":"KIT Data Manager Team","url":"https://kit-data-manager.github.io/webpage","email":"webmaster@datamanager.kit.edu"},"version":"0.2.0"},"externalDocs":{"description":"IDORIS GitHub Repository","url":"https://github.com/maximiliani/idoris"},"servers":[{"url":"http://localhost:8095/api","description":"Generated server url"}],"tags":[{"name":"TypeProfile","description":"API for managing TypeProfiles"},{"name":"TechnologyInterface","description":"API for managing TechnologyInterfaces"},{"name":"Operation","description":"API for managing Operations"},{"name":"AtomicDataType","description":"API for managing AtomicDataTypes"},{"name":"Actuator","description":"Monitor and interact","externalDocs":{"description":"Spring Boot Actuator Web API Documentation","url":"https://docs.spring.io/spring-boot/docs/current/actuator-api/html/"}},{"name":"Attribute","description":"API for managing Attributes"}],"paths":{"/v1/typeProfiles/{pid}":{"get":{"tags":["TypeProfile"],"summary":"Get a TypeProfile by PID","description":"Returns a TypeProfile entity by its PID","operationId":"getTypeProfile","parameters":[{"name":"pid","in":"path","description":"PID of the TypeProfile","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"TypeProfile found","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/TypeProfile"}}}},"404":{"description":"TypeProfile not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelTypeProfile"}}}}}},"put":{"tags":["TypeProfile"],"summary":"Update a TypeProfile","description":"Updates an existing TypeProfile entity after validating it","operationId":"updateTypeProfile","parameters":[{"name":"pid","in":"path","description":"PID of the TypeProfile","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TypeProfile"}}},"required":true},"responses":{"200":{"description":"TypeProfile updated","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/TypeProfile"}}}},"400":{"description":"Invalid input or validation failed","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelTypeProfile"}}}},"404":{"description":"TypeProfile not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelTypeProfile"}}}}}},"delete":{"tags":["TypeProfile"],"summary":"Delete a TypeProfile","description":"Deletes a TypeProfile entity","operationId":"deleteTypeProfile","parameters":[{"name":"pid","in":"path","description":"PID of the TypeProfile","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"TypeProfile deleted"},"404":{"description":"TypeProfile not found"}}},"patch":{"tags":["TypeProfile"],"summary":"Partially update a TypeProfile","description":"Updates specific fields of an existing TypeProfile entity","operationId":"patchTypeProfile","parameters":[{"name":"pid","in":"path","description":"PID of the TypeProfile","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TypeProfile"}}},"required":true},"responses":{"200":{"description":"TypeProfile patched","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/TypeProfile"}}}},"400":{"description":"Invalid input","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelTypeProfile"}}}},"404":{"description":"TypeProfile not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelTypeProfile"}}}}}}},"/v1/technologyInterfaces/{pid}":{"get":{"tags":["TechnologyInterface"],"summary":"Get a TechnologyInterface by PID","description":"Returns a TechnologyInterface entity by its PID","operationId":"getTechnologyInterface","parameters":[{"name":"pid","in":"path","description":"PID of the TechnologyInterface","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"TechnologyInterface found","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/TechnologyInterface"}}}},"404":{"description":"TechnologyInterface not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelTechnologyInterface"}}}}}},"put":{"tags":["TechnologyInterface"],"summary":"Update a TechnologyInterface","description":"Updates an existing TechnologyInterface entity","operationId":"updateTechnologyInterface","parameters":[{"name":"pid","in":"path","description":"PID of the TechnologyInterface","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TechnologyInterface"}}},"required":true},"responses":{"200":{"description":"TechnologyInterface updated","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/TechnologyInterface"}}}},"400":{"description":"Invalid input","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelTechnologyInterface"}}}},"404":{"description":"TechnologyInterface not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelTechnologyInterface"}}}}}},"delete":{"tags":["TechnologyInterface"],"summary":"Delete a TechnologyInterface","description":"Deletes a TechnologyInterface entity","operationId":"deleteTechnologyInterface","parameters":[{"name":"pid","in":"path","description":"PID of the TechnologyInterface","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"TechnologyInterface deleted"},"404":{"description":"TechnologyInterface not found"}}},"patch":{"tags":["TechnologyInterface"],"summary":"Partially update a TechnologyInterface","description":"Updates specific fields of an existing TechnologyInterface entity","operationId":"patchTechnologyInterface","parameters":[{"name":"pid","in":"path","description":"PID of the TechnologyInterface","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TechnologyInterface"}}},"required":true},"responses":{"200":{"description":"TechnologyInterface patched","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/TechnologyInterface"}}}},"400":{"description":"Invalid input","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelTechnologyInterface"}}}},"404":{"description":"TechnologyInterface not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelTechnologyInterface"}}}}}}},"/v1/operations/{pid}":{"get":{"tags":["Operation"],"summary":"Get an Operation by PID","description":"Returns an Operation entity by its PID","operationId":"getOperation","parameters":[{"name":"pid","in":"path","description":"PID of the Operation","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Operation found","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/Operation"}}}},"404":{"description":"Operation not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelOperation"}}}}}},"put":{"tags":["Operation"],"summary":"Update an Operation","description":"Updates an existing Operation entity after validating it","operationId":"updateOperation","parameters":[{"name":"pid","in":"path","description":"PID of the Operation","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Operation"}}},"required":true},"responses":{"200":{"description":"Operation updated","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/Operation"}}}},"400":{"description":"Invalid input or validation failed","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelOperation"}}}},"404":{"description":"Operation not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelOperation"}}}}}},"delete":{"tags":["Operation"],"summary":"Delete an Operation","description":"Deletes an Operation entity","operationId":"deleteOperation","parameters":[{"name":"pid","in":"path","description":"PID of the Operation","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Operation deleted"},"404":{"description":"Operation not found"}}},"patch":{"tags":["Operation"],"summary":"Partially update an Operation","description":"Updates specific fields of an existing Operation entity","operationId":"patchOperation","parameters":[{"name":"pid","in":"path","description":"PID of the Operation","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Operation"}}},"required":true},"responses":{"200":{"description":"Operation patched","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/Operation"}}}},"400":{"description":"Invalid input","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelOperation"}}}},"404":{"description":"Operation not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelOperation"}}}}}}},"/v1/attributes/{pid}":{"get":{"tags":["Attribute"],"summary":"Get an Attribute by PID","description":"Returns an Attribute entity by its PID","operationId":"getAttribute","parameters":[{"name":"pid","in":"path","description":"PID of the Attribute","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Attribute found","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/Attribute"}}}},"404":{"description":"Attribute not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelAttribute"}}}}}},"put":{"tags":["Attribute"],"summary":"Update an Attribute","description":"Updates an existing Attribute entity","operationId":"updateAttribute","parameters":[{"name":"pid","in":"path","description":"PID of the Attribute","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Attribute"}}},"required":true},"responses":{"200":{"description":"Attribute updated","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/Attribute"}}}},"400":{"description":"Invalid input","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelAttribute"}}}},"404":{"description":"Attribute not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelAttribute"}}}}}},"delete":{"tags":["Attribute"],"summary":"Delete an Attribute","description":"Deletes an Attribute entity","operationId":"deleteAttribute","parameters":[{"name":"pid","in":"path","description":"PID of the Attribute","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Attribute deleted"},"404":{"description":"Attribute not found"}}},"patch":{"tags":["Attribute"],"summary":"Partially update an Attribute","description":"Updates specific fields of an existing Attribute entity","operationId":"patchAttribute","parameters":[{"name":"pid","in":"path","description":"PID of the Attribute","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Attribute"}}},"required":true},"responses":{"200":{"description":"Attribute patched","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/Attribute"}}}},"400":{"description":"Invalid input","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelAttribute"}}}},"404":{"description":"Attribute not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelAttribute"}}}}}}},"/v1/atomicDataTypes/{pid}":{"get":{"tags":["AtomicDataType"],"summary":"Get an AtomicDataType by PID","description":"Returns an AtomicDataType entity by its PID","operationId":"getAtomicDataType","parameters":[{"name":"pid","in":"path","description":"PID of the AtomicDataType","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"AtomicDataType found","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/AtomicDataType"}}}},"404":{"description":"AtomicDataType not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelAtomicDataType"}}}}}},"put":{"tags":["AtomicDataType"],"summary":"Update an AtomicDataType","description":"Updates an existing AtomicDataType entity after validating it","operationId":"updateAtomicDataType","parameters":[{"name":"pid","in":"path","description":"PID of the AtomicDataType","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AtomicDataType"}}},"required":true},"responses":{"200":{"description":"AtomicDataType updated","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/AtomicDataType"}}}},"400":{"description":"Invalid input or validation failed","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelAtomicDataType"}}}},"404":{"description":"AtomicDataType not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelAtomicDataType"}}}}}},"delete":{"tags":["AtomicDataType"],"summary":"Delete an AtomicDataType","description":"Deletes an AtomicDataType entity","operationId":"deleteAtomicDataType","parameters":[{"name":"pid","in":"path","description":"PID of the AtomicDataType","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"AtomicDataType deleted"},"404":{"description":"AtomicDataType not found"}}},"patch":{"tags":["AtomicDataType"],"summary":"Partially update an AtomicDataType","description":"Updates specific fields of an existing AtomicDataType entity","operationId":"patchAtomicDataType","parameters":[{"name":"pid","in":"path","description":"PID of the AtomicDataType","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AtomicDataType"}}},"required":true},"responses":{"200":{"description":"AtomicDataType patched","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/AtomicDataType"}}}},"400":{"description":"Invalid input","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelAtomicDataType"}}}},"404":{"description":"AtomicDataType not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelAtomicDataType"}}}}}}},"/v1/typeProfiles":{"get":{"tags":["TypeProfile"],"summary":"Get all TypeProfiles","description":"Returns a collection of all TypeProfile entities","operationId":"getAllTypeProfiles","responses":{"200":{"description":"TypeProfiles found","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/TypeProfile"}}}}}},"post":{"tags":["TypeProfile"],"summary":"Create a new TypeProfile","description":"Creates a new TypeProfile entity after validating it","operationId":"createTypeProfile","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TypeProfile"}}},"required":true},"responses":{"201":{"description":"TypeProfile created","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/TypeProfile"}}}},"400":{"description":"Invalid input or validation failed","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelTypeProfile"}}}}}}},"/v1/technologyInterfaces":{"get":{"tags":["TechnologyInterface"],"summary":"Get all TechnologyInterfaces","description":"Returns a collection of all TechnologyInterface entities","operationId":"getAllTechnologyInterfaces","responses":{"200":{"description":"TechnologyInterfaces found","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/TechnologyInterface"}}}}}},"post":{"tags":["TechnologyInterface"],"summary":"Create a new TechnologyInterface","description":"Creates a new TechnologyInterface entity","operationId":"createTechnologyInterface","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TechnologyInterface"}}},"required":true},"responses":{"201":{"description":"TechnologyInterface created","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/TechnologyInterface"}}}},"400":{"description":"Invalid input","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelTechnologyInterface"}}}}}}},"/v1/operations":{"get":{"tags":["Operation"],"summary":"Get all Operations","description":"Returns a collection of all Operation entities","operationId":"getAllOperations","responses":{"200":{"description":"Operations found","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/Operation"}}}}}},"post":{"tags":["Operation"],"summary":"Create a new Operation","description":"Creates a new Operation entity after validating it","operationId":"createOperation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Operation"}}},"required":true},"responses":{"201":{"description":"Operation created","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/Operation"}}}},"400":{"description":"Invalid input or validation failed","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelOperation"}}}}}}},"/v1/attributes":{"get":{"tags":["Attribute"],"summary":"Get all Attributes","description":"Returns a collection of all Attribute entities","operationId":"getAllAttributes","responses":{"200":{"description":"Attributes found","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/Attribute"}}}}}},"post":{"tags":["Attribute"],"summary":"Create a new Attribute","description":"Creates a new Attribute entity","operationId":"createAttribute","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Attribute"}}},"required":true},"responses":{"201":{"description":"Attribute created","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/Attribute"}}}},"400":{"description":"Invalid input","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelAttribute"}}}}}}},"/v1/atomicDataTypes":{"get":{"tags":["AtomicDataType"],"summary":"Get all AtomicDataTypes","description":"Returns a collection of all AtomicDataType entities","operationId":"getAllAtomicDataTypes","responses":{"200":{"description":"AtomicDataTypes found","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/AtomicDataType"}}}}}},"post":{"tags":["AtomicDataType"],"summary":"Create a new AtomicDataType","description":"Creates a new AtomicDataType entity after validating it","operationId":"createAtomicDataType","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AtomicDataType"}}},"required":true},"responses":{"201":{"description":"AtomicDataType created","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/AtomicDataType"}}}},"400":{"description":"Invalid input or validation failed","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelAtomicDataType"}}}}}}},"/actuator/loggers/{name}":{"get":{"tags":["Actuator"],"summary":"Actuator web endpoint 'loggers-name'","operationId":"loggerLevels","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/vnd.spring-boot.actuator.v3+json":{"schema":{"type":"object"}},"application/vnd.spring-boot.actuator.v2+json":{"schema":{"type":"object"}},"application/json":{"schema":{"type":"object"}}}},"404":{"description":"Not Found"}}},"post":{"tags":["Actuator"],"summary":"Actuator web endpoint 'loggers-name'","operationId":"configureLogLevel","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"string","enum":["TRACE","DEBUG","INFO","WARN","ERROR","FATAL","OFF"]}}}},"responses":{"204":{"description":"No Content"},"400":{"description":"Bad Request"}}}},"/v1/typeProfiles/{pid}/validate":{"get":{"tags":["TypeProfile"],"summary":"Validate a TypeProfile","description":"Validates a TypeProfile entity and returns the validation result","operationId":"validate","parameters":[{"name":"pid","in":"path","description":"PID of the TypeProfile","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"TypeProfile is valid","content":{"*/*":{"schema":{"type":"object"}}}},"218":{"description":"TypeProfile is invalid","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"TypeProfile not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/v1/typeProfiles/{pid}/operations":{"get":{"tags":["TypeProfile"],"summary":"Get operations for a TypeProfile","description":"Returns a collection of operations that can be executed on a TypeProfile","operationId":"getOperationsForTypeProfile","parameters":[{"name":"pid","in":"path","description":"PID of the TypeProfile","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Operations found","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/Operation"}}}},"404":{"description":"TypeProfile not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CollectionModelEntityModelOperation"}}}}}}},"/v1/typeProfiles/{pid}/inheritedAttributes":{"get":{"tags":["TypeProfile"],"summary":"Get inherited attributes of a TypeProfile","description":"Returns a collection of attributes inherited by a TypeProfile","operationId":"getInheritedAttributes","parameters":[{"name":"pid","in":"path","description":"PID of the TypeProfile","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Inherited attributes found","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/Attribute"}}}},"404":{"description":"TypeProfile not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CollectionModelEntityModelAttribute"}}}}}}},"/v1/typeProfiles/{pid}/inheritanceTree":{"get":{"tags":["TypeProfile"],"summary":"Get inheritance tree of a TypeProfile","description":"Returns the inheritance tree of a TypeProfile","operationId":"getInheritanceTree","parameters":[{"name":"pid","in":"path","description":"PID of the TypeProfile","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Inheritance tree found","content":{"application/hal+json":{}}},"404":{"description":"TypeProfile not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelTypeProfileInheritance"}}}}}}},"/v1/technologyInterfaces/{pid}/outputs":{"get":{"tags":["TechnologyInterface"],"summary":"Get outputs of a TechnologyInterface","description":"Returns a collection of outputs of a TechnologyInterface","operationId":"getOutputs","parameters":[{"name":"pid","in":"path","description":"PID of the TechnologyInterface","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Outputs found","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/Attribute"}}}},"404":{"description":"TechnologyInterface not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CollectionModelEntityModelAttribute"}}}}}}},"/v1/technologyInterfaces/{pid}/attributes":{"get":{"tags":["TechnologyInterface"],"summary":"Get attributes of a TechnologyInterface","description":"Returns a collection of attributes of a TechnologyInterface","operationId":"getAttributes","parameters":[{"name":"pid","in":"path","description":"PID of the TechnologyInterface","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Attributes found","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/Attribute"}}}},"404":{"description":"TechnologyInterface not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CollectionModelEntityModelAttribute"}}}}}}},"/v1/operations/{pid}/validate":{"get":{"tags":["Operation"],"summary":"Validate an Operation","description":"Validates an Operation entity and returns the validation result","operationId":"validate_1","parameters":[{"name":"pid","in":"path","description":"PID of the Operation","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Operation is valid","content":{"*/*":{"schema":{"type":"object"}}}},"218":{"description":"Operation is invalid","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Operation not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/v1/operations/search/getOperationsForDataType":{"get":{"tags":["Operation"],"summary":"Get operations for a data type","description":"Returns a collection of operations that can be executed on a data type","operationId":"getOperationsForDataType","parameters":[{"name":"pid","in":"query","description":"PID of the data type","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Operations found","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/Operation"}}}}}}},"/v1/attributes/{pid}/dataType":{"get":{"tags":["Attribute"],"summary":"Get the DataType of an Attribute","description":"Returns the DataType of an Attribute","operationId":"getDataType","parameters":[{"name":"pid","in":"path","description":"PID of the Attribute","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"DataType found","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/DataType"}}}},"404":{"description":"Attribute not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntityModelDataType"}}}}}}},"/v1/atomicDataTypes/{pid}/operations":{"get":{"tags":["AtomicDataType"],"summary":"Get operations for an AtomicDataType","description":"Returns a collection of operations that can be executed on an AtomicDataType","operationId":"getOperationsForAtomicDataType","parameters":[{"name":"pid","in":"path","description":"PID of the AtomicDataType","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Operations found","content":{"application/hal+json":{"schema":{"$ref":"#/components/schemas/Operation"}}}},"404":{"description":"AtomicDataType not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CollectionModelEntityModelOperation"}}}}}}},"/actuator":{"get":{"tags":["Actuator"],"summary":"Actuator root web endpoint","operationId":"links","responses":{"200":{"description":"OK","content":{"application/vnd.spring-boot.actuator.v3+json":{"schema":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/Link"}}}},"application/vnd.spring-boot.actuator.v2+json":{"schema":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/Link"}}}},"application/json":{"schema":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/Link"}}}}}}}}},"/actuator/threaddump":{"get":{"tags":["Actuator"],"summary":"Actuator web endpoint 'threaddump'","operationId":"threadDump","responses":{"200":{"description":"OK","content":{"text/plain;charset=UTF-8":{"schema":{"type":"object"}},"application/vnd.spring-boot.actuator.v3+json":{"schema":{"type":"object"}},"application/vnd.spring-boot.actuator.v2+json":{"schema":{"type":"object"}},"application/json":{"schema":{"type":"object"}}}}}}},"/actuator/scheduledtasks":{"get":{"tags":["Actuator"],"summary":"Actuator web endpoint 'scheduledtasks'","operationId":"scheduledTasks","responses":{"200":{"description":"OK","content":{"application/vnd.spring-boot.actuator.v3+json":{"schema":{"type":"object"}},"application/vnd.spring-boot.actuator.v2+json":{"schema":{"type":"object"}},"application/json":{"schema":{"type":"object"}}}}}}},"/actuator/sbom":{"get":{"tags":["Actuator"],"summary":"Actuator web endpoint 'sbom'","operationId":"sboms","responses":{"200":{"description":"OK","content":{"application/vnd.spring-boot.actuator.v3+json":{"schema":{"type":"object"}},"application/vnd.spring-boot.actuator.v2+json":{"schema":{"type":"object"}},"application/json":{"schema":{"type":"object"}}}}}}},"/actuator/sbom/{id}":{"get":{"tags":["Actuator"],"summary":"Actuator web endpoint 'sbom-id'","operationId":"sbom","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/octet-stream":{"schema":{"type":"object"}}}},"404":{"description":"Not Found"}}}},"/actuator/modulith":{"get":{"tags":["Actuator"],"summary":"Actuator web endpoint 'modulith'","operationId":"getApplicationModules","responses":{"200":{"description":"OK","content":{"application/vnd.spring-boot.actuator.v3+json":{"schema":{"type":"object"}},"application/vnd.spring-boot.actuator.v2+json":{"schema":{"type":"object"}},"application/json":{"schema":{"type":"object"}}}}}}},"/actuator/metrics":{"get":{"tags":["Actuator"],"summary":"Actuator web endpoint 'metrics'","operationId":"listNames","responses":{"200":{"description":"OK","content":{"application/vnd.spring-boot.actuator.v3+json":{"schema":{"type":"object"}},"application/vnd.spring-boot.actuator.v2+json":{"schema":{"type":"object"}},"application/json":{"schema":{"type":"object"}}}}}}},"/actuator/metrics/{requiredMetricName}":{"get":{"tags":["Actuator"],"summary":"Actuator web endpoint 'metrics-requiredMetricName'","operationId":"metric","parameters":[{"name":"requiredMetricName","in":"path","required":true,"schema":{"type":"string"}},{"name":"tag","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/vnd.spring-boot.actuator.v3+json":{"schema":{"type":"object"}},"application/vnd.spring-boot.actuator.v2+json":{"schema":{"type":"object"}},"application/json":{"schema":{"type":"object"}}}},"404":{"description":"Not Found"}}}},"/actuator/mappings":{"get":{"tags":["Actuator"],"summary":"Actuator web endpoint 'mappings'","operationId":"mappings","responses":{"200":{"description":"OK","content":{"application/vnd.spring-boot.actuator.v3+json":{"schema":{"type":"object"}},"application/vnd.spring-boot.actuator.v2+json":{"schema":{"type":"object"}},"application/json":{"schema":{"type":"object"}}}}}}},"/actuator/loggers":{"get":{"tags":["Actuator"],"summary":"Actuator web endpoint 'loggers'","operationId":"loggers","responses":{"200":{"description":"OK","content":{"application/vnd.spring-boot.actuator.v3+json":{"schema":{"type":"object"}},"application/vnd.spring-boot.actuator.v2+json":{"schema":{"type":"object"}},"application/json":{"schema":{"type":"object"}}}}}}},"/actuator/info":{"get":{"tags":["Actuator"],"summary":"Actuator web endpoint 'info'","operationId":"info","responses":{"200":{"description":"OK","content":{"application/vnd.spring-boot.actuator.v3+json":{"schema":{"type":"object"}},"application/vnd.spring-boot.actuator.v2+json":{"schema":{"type":"object"}},"application/json":{"schema":{"type":"object"}}}}}}},"/actuator/health":{"get":{"tags":["Actuator"],"summary":"Actuator web endpoint 'health'","operationId":"health","responses":{"200":{"description":"OK","content":{"application/vnd.spring-boot.actuator.v3+json":{"schema":{"type":"object"}},"application/vnd.spring-boot.actuator.v2+json":{"schema":{"type":"object"}},"application/json":{"schema":{"type":"object"}}}}}}},"/actuator/env":{"get":{"tags":["Actuator"],"summary":"Actuator web endpoint 'env'","operationId":"environment","parameters":[{"name":"pattern","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/vnd.spring-boot.actuator.v3+json":{"schema":{"type":"object"}},"application/vnd.spring-boot.actuator.v2+json":{"schema":{"type":"object"}},"application/json":{"schema":{"type":"object"}}}}}}},"/actuator/env/{toMatch}":{"get":{"tags":["Actuator"],"summary":"Actuator web endpoint 'env-toMatch'","operationId":"environmentEntry","parameters":[{"name":"toMatch","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/vnd.spring-boot.actuator.v3+json":{"schema":{"type":"object"}},"application/vnd.spring-boot.actuator.v2+json":{"schema":{"type":"object"}},"application/json":{"schema":{"type":"object"}}}},"404":{"description":"Not Found"}}}},"/actuator/configprops":{"get":{"tags":["Actuator"],"summary":"Actuator web endpoint 'configprops'","operationId":"configurationProperties","responses":{"200":{"description":"OK","content":{"application/vnd.spring-boot.actuator.v3+json":{"schema":{"type":"object"}},"application/vnd.spring-boot.actuator.v2+json":{"schema":{"type":"object"}},"application/json":{"schema":{"type":"object"}}}}}}},"/actuator/configprops/{prefix}":{"get":{"tags":["Actuator"],"summary":"Actuator web endpoint 'configprops-prefix'","operationId":"configurationPropertiesWithPrefix","parameters":[{"name":"prefix","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/vnd.spring-boot.actuator.v3+json":{"schema":{"type":"object"}},"application/vnd.spring-boot.actuator.v2+json":{"schema":{"type":"object"}},"application/json":{"schema":{"type":"object"}}}},"404":{"description":"Not Found"}}}},"/actuator/conditions":{"get":{"tags":["Actuator"],"summary":"Actuator web endpoint 'conditions'","operationId":"conditions","responses":{"200":{"description":"OK","content":{"application/vnd.spring-boot.actuator.v3+json":{"schema":{"type":"object"}},"application/vnd.spring-boot.actuator.v2+json":{"schema":{"type":"object"}},"application/json":{"schema":{"type":"object"}}}}}}},"/actuator/caches":{"get":{"tags":["Actuator"],"summary":"Actuator web endpoint 'caches'","operationId":"caches","responses":{"200":{"description":"OK","content":{"application/vnd.spring-boot.actuator.v3+json":{"schema":{"type":"object"}},"application/vnd.spring-boot.actuator.v2+json":{"schema":{"type":"object"}},"application/json":{"schema":{"type":"object"}}}}}},"delete":{"tags":["Actuator"],"summary":"Actuator web endpoint 'caches'","operationId":"clearCaches","responses":{"204":{"description":"No Content"}}}},"/actuator/caches/{cache}":{"get":{"tags":["Actuator"],"summary":"Actuator web endpoint 'caches-cache'","operationId":"cache","parameters":[{"name":"cache","in":"path","required":true,"schema":{"type":"string"}},{"name":"cacheManager","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/vnd.spring-boot.actuator.v3+json":{"schema":{"type":"object"}},"application/vnd.spring-boot.actuator.v2+json":{"schema":{"type":"object"}},"application/json":{"schema":{"type":"object"}}}},"404":{"description":"Not Found"}}},"delete":{"tags":["Actuator"],"summary":"Actuator web endpoint 'caches-cache'","operationId":"clearCache","parameters":[{"name":"cache","in":"path","required":true,"schema":{"type":"string"}},{"name":"cacheManager","in":"query","schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"404":{"description":"Not Found"}}}},"/actuator/beans":{"get":{"tags":["Actuator"],"summary":"Actuator web endpoint 'beans'","operationId":"beans","responses":{"200":{"description":"OK","content":{"application/vnd.spring-boot.actuator.v3+json":{"schema":{"type":"object"}},"application/vnd.spring-boot.actuator.v2+json":{"schema":{"type":"object"}},"application/json":{"schema":{"type":"object"}}}}}}},"/v1/attributes/orphaned":{"delete":{"tags":["Attribute"],"summary":"Delete orphaned Attributes","description":"Deletes Attribute entities that are not referenced by any other node","operationId":"deleteOrphanedAttributes","responses":{"204":{"description":"Orphaned Attributes deleted"}}}}},"components":{"schemas":{"AtomicDataType":{"allOf":[{"$ref":"#/components/schemas/DataType"},{"type":"object","properties":{"inheritsFrom":{"$ref":"#/components/schemas/AtomicDataType"},"primitiveDataType":{"type":"string","enum":["string","integer","number","bool"]},"regularExpression":{"type":"string"},"permittedValues":{"type":"array","items":{"type":"string"},"uniqueItems":true},"forbiddenValues":{"type":"array","items":{"type":"string"},"uniqueItems":true},"minimum":{"type":"integer","format":"int32"},"maximum":{"type":"integer","format":"int32"}}}]},"Attribute":{"type":"object","properties":{"internalId":{"type":"string"},"pid":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"version":{"type":"integer","format":"int64"},"createdAt":{"type":"string","format":"date-time"},"lastModifiedAt":{"type":"string","format":"date-time"},"expectedUseCases":{"type":"array","items":{"type":"string"},"uniqueItems":true},"contributors":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/ORCiDUser"},{"$ref":"#/components/schemas/TextUser"}]},"uniqueItems":true},"references":{"type":"array","items":{"$ref":"#/components/schemas/Reference"},"uniqueItems":true},"defaultValue":{"type":"string"},"constantValue":{"type":"string"},"lowerBoundCardinality":{"type":"integer","format":"int32"},"upperBoundCardinality":{"type":"integer","format":"int32"},"dataType":{"oneOf":[{"$ref":"#/components/schemas/AtomicDataType"},{"$ref":"#/components/schemas/TypeProfile"}]},"override":{"$ref":"#/components/schemas/Attribute"},"id":{"type":"string"}},"required":["dataType"]},"DataType":{"type":"object","discriminator":{"propertyName":"type"},"properties":{"internalId":{"type":"string"},"pid":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"version":{"type":"integer","format":"int64"},"createdAt":{"type":"string","format":"date-time"},"lastModifiedAt":{"type":"string","format":"date-time"},"expectedUseCases":{"type":"array","items":{"type":"string"},"uniqueItems":true},"contributors":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/ORCiDUser"},{"$ref":"#/components/schemas/TextUser"}]},"uniqueItems":true},"references":{"type":"array","items":{"$ref":"#/components/schemas/Reference"},"uniqueItems":true},"type":{"type":"string","enum":["AtomicDataType","TypeProfile"]},"defaultValue":{"type":"string"},"id":{"type":"string"}}},"ORCiDUser":{"allOf":[{"$ref":"#/components/schemas/User"},{"type":"object","properties":{"orcid":{"type":"string","format":"url"}}}]},"Reference":{"type":"object","properties":{"relationType":{"type":"string"},"targetPID":{"type":"string"}}},"TextUser":{"allOf":[{"$ref":"#/components/schemas/User"},{"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string"},"details":{"type":"string"}}}]},"TypeProfile":{"allOf":[{"$ref":"#/components/schemas/DataType"},{"type":"object","properties":{"inheritsFrom":{"type":"array","items":{"$ref":"#/components/schemas/TypeProfile"},"uniqueItems":true},"attributes":{"type":"array","items":{"$ref":"#/components/schemas/Attribute"},"uniqueItems":true},"permitEmbedding":{"type":"boolean"},"allowAdditionalAttributes":{"type":"boolean"},"validationPolicy":{"type":"string","enum":["NONE","ONE","ANY","ALL"]},"abstract":{"type":"boolean"}}}]},"User":{"type":"object","discriminator":{"propertyName":"type"},"properties":{"createdAt":{"type":"string","format":"date-time"},"internalId":{"type":"string"},"type":{"type":"string"}}},"EntityModelTypeProfile":{"type":"object","properties":{"internalId":{"type":"string"},"pid":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"version":{"type":"integer","format":"int64"},"createdAt":{"type":"string","format":"date-time"},"lastModifiedAt":{"type":"string","format":"date-time"},"expectedUseCases":{"type":"array","items":{"type":"string"},"uniqueItems":true},"contributors":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/ORCiDUser"},{"$ref":"#/components/schemas/TextUser"}]},"uniqueItems":true},"references":{"type":"array","items":{"$ref":"#/components/schemas/Reference"},"uniqueItems":true},"type":{"type":"string","enum":["AtomicDataType","TypeProfile"]},"defaultValue":{"type":"string"},"inheritsFrom":{"type":"array","items":{"$ref":"#/components/schemas/TypeProfile"},"uniqueItems":true},"attributes":{"type":"array","items":{"$ref":"#/components/schemas/Attribute"},"uniqueItems":true},"permitEmbedding":{"type":"boolean"},"allowAdditionalAttributes":{"type":"boolean"},"validationPolicy":{"type":"string","enum":["NONE","ONE","ANY","ALL"]},"abstract":{"type":"boolean"},"id":{"type":"string"},"_links":{"$ref":"#/components/schemas/Links"}}},"Link":{"type":"object","properties":{"href":{"type":"string"},"hreflang":{"type":"string"},"title":{"type":"string"},"type":{"type":"string"},"deprecation":{"type":"string"},"profile":{"type":"string"},"name":{"type":"string"},"templated":{"type":"boolean"}}},"TechnologyInterface":{"type":"object","properties":{"internalId":{"type":"string"},"pid":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"version":{"type":"integer","format":"int64"},"createdAt":{"type":"string","format":"date-time"},"lastModifiedAt":{"type":"string","format":"date-time"},"expectedUseCases":{"type":"array","items":{"type":"string"},"uniqueItems":true},"contributors":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/ORCiDUser"},{"$ref":"#/components/schemas/TextUser"}]},"uniqueItems":true},"references":{"type":"array","items":{"$ref":"#/components/schemas/Reference"},"uniqueItems":true},"attributes":{"type":"array","items":{"$ref":"#/components/schemas/Attribute"},"uniqueItems":true},"outputs":{"type":"array","items":{"$ref":"#/components/schemas/Attribute"},"uniqueItems":true},"adapters":{"type":"array","items":{"type":"string"},"uniqueItems":true},"id":{"type":"string"}}},"EntityModelTechnologyInterface":{"type":"object","properties":{"internalId":{"type":"string"},"pid":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"version":{"type":"integer","format":"int64"},"createdAt":{"type":"string","format":"date-time"},"lastModifiedAt":{"type":"string","format":"date-time"},"expectedUseCases":{"type":"array","items":{"type":"string"},"uniqueItems":true},"contributors":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/ORCiDUser"},{"$ref":"#/components/schemas/TextUser"}]},"uniqueItems":true},"references":{"type":"array","items":{"$ref":"#/components/schemas/Reference"},"uniqueItems":true},"attributes":{"type":"array","items":{"$ref":"#/components/schemas/Attribute"},"uniqueItems":true},"outputs":{"type":"array","items":{"$ref":"#/components/schemas/Attribute"},"uniqueItems":true},"adapters":{"type":"array","items":{"type":"string"},"uniqueItems":true},"id":{"type":"string"},"_links":{"$ref":"#/components/schemas/Links"}}},"AttributeMapping":{"type":"object","properties":{"internalId":{"type":"string"},"name":{"type":"string"},"input":{"$ref":"#/components/schemas/Attribute"},"replaceCharactersInValueWithInput":{"type":"string"},"value":{"type":"string"},"index":{"type":"integer","format":"int32"},"output":{"$ref":"#/components/schemas/Attribute"},"id":{"type":"string"}}},"Operation":{"type":"object","properties":{"internalId":{"type":"string"},"pid":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"version":{"type":"integer","format":"int64"},"createdAt":{"type":"string","format":"date-time"},"lastModifiedAt":{"type":"string","format":"date-time"},"expectedUseCases":{"type":"array","items":{"type":"string"},"uniqueItems":true},"contributors":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/ORCiDUser"},{"$ref":"#/components/schemas/TextUser"}]},"uniqueItems":true},"references":{"type":"array","items":{"$ref":"#/components/schemas/Reference"},"uniqueItems":true},"executableOn":{"$ref":"#/components/schemas/Attribute"},"returns":{"type":"array","items":{"$ref":"#/components/schemas/Attribute"},"uniqueItems":true},"environment":{"type":"array","items":{"$ref":"#/components/schemas/Attribute"},"uniqueItems":true},"execution":{"type":"array","items":{"$ref":"#/components/schemas/OperationStep"}},"id":{"type":"string"}}},"OperationStep":{"type":"object","properties":{"internalId":{"type":"string"},"index":{"type":"integer","format":"int32"},"name":{"type":"string"},"mode":{"type":"string","enum":["sync","async"]},"subSteps":{"type":"array","items":{"$ref":"#/components/schemas/OperationStep"}},"executeOperation":{"$ref":"#/components/schemas/Operation"},"useTechnology":{"$ref":"#/components/schemas/TechnologyInterface"},"inputMappings":{"type":"array","items":{"$ref":"#/components/schemas/AttributeMapping"}},"outputMappings":{"type":"array","items":{"$ref":"#/components/schemas/AttributeMapping"}},"id":{"type":"string"}}},"EntityModelOperation":{"type":"object","properties":{"internalId":{"type":"string"},"pid":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"version":{"type":"integer","format":"int64"},"createdAt":{"type":"string","format":"date-time"},"lastModifiedAt":{"type":"string","format":"date-time"},"expectedUseCases":{"type":"array","items":{"type":"string"},"uniqueItems":true},"contributors":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/ORCiDUser"},{"$ref":"#/components/schemas/TextUser"}]},"uniqueItems":true},"references":{"type":"array","items":{"$ref":"#/components/schemas/Reference"},"uniqueItems":true},"executableOn":{"$ref":"#/components/schemas/Attribute"},"returns":{"type":"array","items":{"$ref":"#/components/schemas/Attribute"},"uniqueItems":true},"environment":{"type":"array","items":{"$ref":"#/components/schemas/Attribute"},"uniqueItems":true},"execution":{"type":"array","items":{"$ref":"#/components/schemas/OperationStep"}},"id":{"type":"string"},"_links":{"$ref":"#/components/schemas/Links"}}},"EntityModelAttribute":{"type":"object","properties":{"internalId":{"type":"string"},"pid":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"version":{"type":"integer","format":"int64"},"createdAt":{"type":"string","format":"date-time"},"lastModifiedAt":{"type":"string","format":"date-time"},"expectedUseCases":{"type":"array","items":{"type":"string"},"uniqueItems":true},"contributors":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/ORCiDUser"},{"$ref":"#/components/schemas/TextUser"}]},"uniqueItems":true},"references":{"type":"array","items":{"$ref":"#/components/schemas/Reference"},"uniqueItems":true},"defaultValue":{"type":"string"},"constantValue":{"type":"string"},"lowerBoundCardinality":{"type":"integer","format":"int32"},"upperBoundCardinality":{"type":"integer","format":"int32"},"dataType":{"oneOf":[{"$ref":"#/components/schemas/AtomicDataType"},{"$ref":"#/components/schemas/TypeProfile"}]},"override":{"$ref":"#/components/schemas/Attribute"},"id":{"type":"string"},"_links":{"$ref":"#/components/schemas/Links"}},"required":["dataType"]},"EntityModelAtomicDataType":{"type":"object","properties":{"internalId":{"type":"string"},"pid":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"version":{"type":"integer","format":"int64"},"createdAt":{"type":"string","format":"date-time"},"lastModifiedAt":{"type":"string","format":"date-time"},"expectedUseCases":{"type":"array","items":{"type":"string"},"uniqueItems":true},"contributors":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/ORCiDUser"},{"$ref":"#/components/schemas/TextUser"}]},"uniqueItems":true},"references":{"type":"array","items":{"$ref":"#/components/schemas/Reference"},"uniqueItems":true},"type":{"type":"string","enum":["AtomicDataType","TypeProfile"]},"defaultValue":{"type":"string"},"inheritsFrom":{"$ref":"#/components/schemas/AtomicDataType"},"primitiveDataType":{"type":"string","enum":["string","integer","number","bool"]},"regularExpression":{"type":"string"},"permittedValues":{"type":"array","items":{"type":"string"},"uniqueItems":true},"forbiddenValues":{"type":"array","items":{"type":"string"},"uniqueItems":true},"minimum":{"type":"integer","format":"int32"},"maximum":{"type":"integer","format":"int32"},"id":{"type":"string"},"_links":{"$ref":"#/components/schemas/Links"}}},"CollectionModelEntityModelOperation":{"type":"object","properties":{"_embedded":{"type":"object","properties":{"operationList":{"type":"array","items":{"$ref":"#/components/schemas/EntityModelOperation"}}}},"_links":{"$ref":"#/components/schemas/Links"}}},"CollectionModelEntityModelAttribute":{"type":"object","properties":{"_embedded":{"type":"object","properties":{"attributeList":{"type":"array","items":{"$ref":"#/components/schemas/EntityModelAttribute"}}}},"_links":{"$ref":"#/components/schemas/Links"}}},"CollectionModelEntityModelTypeProfileInheritance":{"type":"object","properties":{"_embedded":{"type":"object","properties":{"typeProfileInheritanceList":{"type":"array","items":{"$ref":"#/components/schemas/EntityModelTypeProfileInheritance"}}}},"_links":{"$ref":"#/components/schemas/Links"}}},"EntityModelTypeProfileInheritance":{"type":"object","properties":{"pid":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"attributes":{"$ref":"#/components/schemas/CollectionModelEntityModelAttribute"},"inheritsFrom":{"$ref":"#/components/schemas/CollectionModelEntityModelTypeProfileInheritance"},"_links":{"$ref":"#/components/schemas/Links"}}},"EntityModelDataType":{"type":"object","properties":{"internalId":{"type":"string"},"pid":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"version":{"type":"integer","format":"int64"},"createdAt":{"type":"string","format":"date-time"},"lastModifiedAt":{"type":"string","format":"date-time"},"expectedUseCases":{"type":"array","items":{"type":"string"},"uniqueItems":true},"contributors":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/ORCiDUser"},{"$ref":"#/components/schemas/TextUser"}]},"uniqueItems":true},"references":{"type":"array","items":{"$ref":"#/components/schemas/Reference"},"uniqueItems":true},"type":{"type":"string","enum":["AtomicDataType","TypeProfile"]},"defaultValue":{"type":"string"},"id":{"type":"string"},"_links":{"$ref":"#/components/schemas/Links"}}},"Links":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/Link"}}}}} --- # https://backstage.io/docs/features/software-catalog/descriptor-format#kind-resource diff --git a/src/main/java/edu/kit/datamanager/idoris/attributes/entities/Attribute.java b/src/main/java/edu/kit/datamanager/idoris/attributes/entities/Attribute.java index 7be60f1..dd96f24 100644 --- a/src/main/java/edu/kit/datamanager/idoris/attributes/entities/Attribute.java +++ b/src/main/java/edu/kit/datamanager/idoris/attributes/entities/Attribute.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.attributes.entities; -import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; import edu.kit.datamanager.idoris.datatypes.entities.DataType; import edu.kit.datamanager.idoris.rules.logic.RuleOutput; import edu.kit.datamanager.idoris.rules.logic.Visitor; diff --git a/src/main/java/edu/kit/datamanager/idoris/configuration/ETagControllerAdvice.java b/src/main/java/edu/kit/datamanager/idoris/configuration/ETagControllerAdvice.java index e2c88e6..25c0b07 100644 --- a/src/main/java/edu/kit/datamanager/idoris/configuration/ETagControllerAdvice.java +++ b/src/main/java/edu/kit/datamanager/idoris/configuration/ETagControllerAdvice.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.configuration; -import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; import jakarta.servlet.http.HttpServletRequest; import org.springframework.core.MethodParameter; import org.springframework.hateoas.EntityModel; diff --git a/src/main/java/edu/kit/datamanager/idoris/configuration/OpenAPIConfig.java b/src/main/java/edu/kit/datamanager/idoris/configuration/OpenAPIConfig.java index a746705..171a9c0 100644 --- a/src/main/java/edu/kit/datamanager/idoris/configuration/OpenAPIConfig.java +++ b/src/main/java/edu/kit/datamanager/idoris/configuration/OpenAPIConfig.java @@ -28,8 +28,13 @@ @OpenAPIDefinition( info = @Info( title = "IDORIS API", - version = "1.0", - description = "API for IDORIS system" + version = "0.2.0", + description = "API for the Integrated Data Type and Operations Registry with Inheritance System (IDORIS). This API provides endpoints for managing data types, operations, and their relationships within IDORIS.", + contact = @io.swagger.v3.oas.annotations.info.Contact( + name = "KIT Data Manager Team", + email = "webmaster@datamanager.kit.edu", + url = "https://kit-data-manager.github.io/webpage" + ) ) ) public class OpenAPIConfig { @@ -38,13 +43,14 @@ public OpenAPI customOpenAPI() { return new OpenAPI() .info(new io.swagger.v3.oas.models.info.Info() .title("IDORIS API") - .version("1.0") + .version("0.2.0") .description("API documentation for IDORIS system") .contact(new Contact() .name("KIT Data Manager Team") - .email("webmaster@datamanager.kit.edu"))) + .email("webmaster@datamanager.kit.edu") + .url("https://kit-data-manager.github.io/webpage"))) .externalDocs(new ExternalDocumentation() - .description("IDORIS Documentation") - .url("https://github.com/kit-data-manager/idoris")); + .description("IDORIS GitHub Repository") + .url("https://github.com/maximiliani/idoris")); } } \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/configuration/TypedPIDMakerConfig.java b/src/main/java/edu/kit/datamanager/idoris/configuration/TypedPIDMakerConfig.java index e5986a5..9f73338 100644 --- a/src/main/java/edu/kit/datamanager/idoris/configuration/TypedPIDMakerConfig.java +++ b/src/main/java/edu/kit/datamanager/idoris/configuration/TypedPIDMakerConfig.java @@ -38,9 +38,11 @@ @Setter public class TypedPIDMakerConfig { /** - * Put metadata of the AdministrativeMetadata into the PID record. - * - * @see edu.kit.datamanager.idoris.domain.GenericIDORISEntity + * Determines whether the PID records should only contain a pointer to the entity in IDORIS + * or if they should contain meaningful metadata. + *

+ * If set to true, the PID records will contain metadata. + * If set to false, the PID records will only contain a pointer to the entity in IDORIS. */ private boolean meaningfulPIDRecords = true; diff --git a/src/main/java/edu/kit/datamanager/idoris/core/domain/dao/IGenericRepo.java b/src/main/java/edu/kit/datamanager/idoris/core/domain/dao/IGenericRepo.java index 7f339ed..1c8830a 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/domain/dao/IGenericRepo.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/domain/dao/IGenericRepo.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.core.domain.dao; -import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.repository.ListCrudRepository; import org.springframework.data.repository.PagingAndSortingRepository; diff --git a/src/main/java/edu/kit/datamanager/idoris/core/domain/AdministrativeMetadata.java b/src/main/java/edu/kit/datamanager/idoris/core/domain/entities/AdministrativeMetadata.java similarity index 93% rename from src/main/java/edu/kit/datamanager/idoris/core/domain/AdministrativeMetadata.java rename to src/main/java/edu/kit/datamanager/idoris/core/domain/entities/AdministrativeMetadata.java index 8475d25..d5d9312 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/domain/AdministrativeMetadata.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/domain/entities/AdministrativeMetadata.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.core.domain; +package edu.kit.datamanager.idoris.core.domain.entities; -import edu.kit.datamanager.idoris.core.domain.entities.Reference; +import edu.kit.datamanager.idoris.core.domain.VisitableElement; import edu.kit.datamanager.idoris.pids.ConfigurablePIDGenerator; import edu.kit.datamanager.idoris.users.entities.User; import lombok.*; diff --git a/src/main/java/edu/kit/datamanager/idoris/core/domain/web/hateoas/EntityModelAssembler.java b/src/main/java/edu/kit/datamanager/idoris/core/domain/web/hateoas/EntityModelAssembler.java index 2cba073..de582e0 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/domain/web/hateoas/EntityModelAssembler.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/domain/web/hateoas/EntityModelAssembler.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.core.domain.web.hateoas; -import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.server.RepresentationModelAssembler; diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/EntityCreatedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityCreatedEvent.java index 009255f..d0d9a04 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/events/EntityCreatedEvent.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityCreatedEvent.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.core.events; -import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; import lombok.Getter; import lombok.ToString; diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/EntityDeletedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityDeletedEvent.java index d7eb380..c57e4c5 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/events/EntityDeletedEvent.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityDeletedEvent.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.core.events; -import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; import lombok.Getter; import lombok.ToString; diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/EntityImportedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityImportedEvent.java index 6bdb74b..2d0e655 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/events/EntityImportedEvent.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityImportedEvent.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.core.events; -import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; import lombok.Getter; import lombok.ToString; diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/EntityPatchedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityPatchedEvent.java index 5fada1f..505de70 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/events/EntityPatchedEvent.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityPatchedEvent.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.core.events; -import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; import lombok.Getter; /** diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/EntityUpdatedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityUpdatedEvent.java index 96ecc94..a9182c2 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/events/EntityUpdatedEvent.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityUpdatedEvent.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.core.events; -import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; import lombok.Getter; import lombok.ToString; diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/EventPublisherService.java b/src/main/java/edu/kit/datamanager/idoris/core/events/EventPublisherService.java index 720d509..43c79a1 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/events/EventPublisherService.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/EventPublisherService.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.core.events; -import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/PIDGeneratedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/PIDGeneratedEvent.java index d5f1682..59c0af9 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/events/PIDGeneratedEvent.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/PIDGeneratedEvent.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.core.events; -import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; import lombok.Getter; import lombok.ToString; diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/SchemaGeneratedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/SchemaGeneratedEvent.java index 1259ce6..7a317eb 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/events/SchemaGeneratedEvent.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/SchemaGeneratedEvent.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.core.events; -import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; import lombok.Getter; import lombok.ToString; diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/VersionCreatedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/VersionCreatedEvent.java index 5aee872..2d67add 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/events/VersionCreatedEvent.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/VersionCreatedEvent.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.core.events; -import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; import lombok.Getter; import lombok.ToString; diff --git a/src/main/java/edu/kit/datamanager/idoris/datatypes/entities/DataType.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/entities/DataType.java index 78cbb2b..d575154 100644 --- a/src/main/java/edu/kit/datamanager/idoris/datatypes/entities/DataType.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/entities/DataType.java @@ -18,7 +18,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; -import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeNotifier.java b/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeNotifier.java index 590ce73..5aff5d5 100644 --- a/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeNotifier.java +++ b/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeNotifier.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.notification; -import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; import edu.kit.datamanager.idoris.core.events.EntityCreatedEvent; import edu.kit.datamanager.idoris.core.events.EntityDeletedEvent; import edu.kit.datamanager.idoris.core.events.EntityUpdatedEvent; diff --git a/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeSubscriber.java b/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeSubscriber.java index fd0bc1e..2b70afc 100644 --- a/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeSubscriber.java +++ b/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeSubscriber.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.notification; -import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; /** * Interface for subscribers that want to be notified of entity changes. diff --git a/src/main/java/edu/kit/datamanager/idoris/notification/LoggingEntityChangeSubscriber.java b/src/main/java/edu/kit/datamanager/idoris/notification/LoggingEntityChangeSubscriber.java index 59ebcbb..a1c076c 100644 --- a/src/main/java/edu/kit/datamanager/idoris/notification/LoggingEntityChangeSubscriber.java +++ b/src/main/java/edu/kit/datamanager/idoris/notification/LoggingEntityChangeSubscriber.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.notification; -import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; diff --git a/src/main/java/edu/kit/datamanager/idoris/operations/entities/Operation.java b/src/main/java/edu/kit/datamanager/idoris/operations/entities/Operation.java index a3c7cc2..29774ec 100644 --- a/src/main/java/edu/kit/datamanager/idoris/operations/entities/Operation.java +++ b/src/main/java/edu/kit/datamanager/idoris/operations/entities/Operation.java @@ -17,7 +17,7 @@ package edu.kit.datamanager.idoris.operations.entities; import edu.kit.datamanager.idoris.attributes.entities.Attribute; -import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; import edu.kit.datamanager.idoris.rules.logic.RuleOutput; import edu.kit.datamanager.idoris.rules.logic.Visitor; import lombok.AllArgsConstructor; diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/ConfigurablePIDGenerator.java b/src/main/java/edu/kit/datamanager/idoris/pids/ConfigurablePIDGenerator.java index 49666df..3f567d9 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/ConfigurablePIDGenerator.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/ConfigurablePIDGenerator.java @@ -17,7 +17,7 @@ package edu.kit.datamanager.idoris.pids; import edu.kit.datamanager.idoris.configuration.ApplicationProperties; -import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.ObjectProvider; diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/PIDGenerationEventListener.java b/src/main/java/edu/kit/datamanager/idoris/pids/PIDGenerationEventListener.java index 82ac7eb..db0c7be 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/PIDGenerationEventListener.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/PIDGenerationEventListener.java @@ -16,7 +16,7 @@ package edu.kit.datamanager.idoris.pids; -import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; import edu.kit.datamanager.idoris.core.events.EntityCreatedEvent; import edu.kit.datamanager.idoris.core.events.EventPublisherService; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/TypedPIDMakerIDGenerator.java b/src/main/java/edu/kit/datamanager/idoris/pids/TypedPIDMakerIDGenerator.java index 7af8ef5..25875c0 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/TypedPIDMakerIDGenerator.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/TypedPIDMakerIDGenerator.java @@ -19,7 +19,7 @@ import com.google.common.base.Ascii; import edu.kit.datamanager.idoris.configuration.ApplicationProperties; import edu.kit.datamanager.idoris.configuration.TypedPIDMakerConfig; -import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; import edu.kit.datamanager.idoris.pids.client.TypedPIDMakerClient; import edu.kit.datamanager.idoris.pids.client.model.PIDRecord; import edu.kit.datamanager.idoris.pids.client.model.PIDRecordEntry; diff --git a/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/entities/TechnologyInterface.java b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/entities/TechnologyInterface.java index 004b5e2..21abbe6 100644 --- a/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/entities/TechnologyInterface.java +++ b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/entities/TechnologyInterface.java @@ -17,7 +17,7 @@ package edu.kit.datamanager.idoris.technologyinterfaces.entities; import edu.kit.datamanager.idoris.attributes.entities.Attribute; -import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; import edu.kit.datamanager.idoris.rules.logic.RuleOutput; import edu.kit.datamanager.idoris.rules.logic.Visitor; import lombok.AllArgsConstructor; diff --git a/src/main/java/edu/kit/datamanager/idoris/users/services/UserService.java b/src/main/java/edu/kit/datamanager/idoris/users/services/UserService.java new file mode 100644 index 0000000..63b5f91 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/users/services/UserService.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2024-2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.users.services; + +import edu.kit.datamanager.idoris.users.entities.ORCiDUser; +import edu.kit.datamanager.idoris.users.entities.TextUser; +import edu.kit.datamanager.idoris.users.entities.User; + +import java.net.URL; +import java.util.List; +import java.util.Optional; + +/** + * Service interface for managing users in the system. + * Provides operations for both TextUsers and ORCiDUsers. + */ +public interface UserService { + + /** + * Find all users in the system. + * + * @return List of all users + */ + List findAllUsers(); + + /** + * Find a user by their internal ID. + * + * @param id The internal ID of the user + * @return Optional containing the user if found, empty otherwise + */ + Optional findUserById(String id); + + /** + * Find all TextUsers in the system. + * + * @return List of all TextUsers + */ + List findAllTextUsers(); + + /** + * Find a TextUser by their email. + * + * @param email The email of the TextUser + * @return Optional containing the TextUser if found, empty otherwise + */ + Optional findTextUserByEmail(String email); + + /** + * Find all ORCiDUsers in the system. + * + * @return List of all ORCiDUsers + */ + List findAllORCiDUsers(); + + /** + * Find an ORCiDUser by their ORCID. + * + * @param orcid The ORCID of the user + * @return Optional containing the ORCiDUser if found, empty otherwise + */ + Optional findORCiDUserByORCiD(URL orcid); + + /** + * Create a new TextUser. + * + * @param user The TextUser to create + * @return The created TextUser + */ + TextUser createTextUser(TextUser user); + + /** + * Create a new ORCiDUser. + * + * @param user The ORCiDUser to create + * @return The created ORCiDUser + */ + ORCiDUser createORCiDUser(ORCiDUser user); + + /** + * Update an existing user. + * + * @param id The internal ID of the user to update + * @param user The updated user information + * @return The updated user + * @throws IllegalArgumentException if the user is not found + */ + User updateUser(String id, User user); + + /** + * Delete a user by their internal ID. + * + * @param id The internal ID of the user to delete + * @throws IllegalArgumentException if the user is not found + */ + void deleteUser(String id); +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/users/services/UserServiceImpl.java b/src/main/java/edu/kit/datamanager/idoris/users/services/UserServiceImpl.java new file mode 100644 index 0000000..e6fe085 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/users/services/UserServiceImpl.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2024-2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.users.services; + +import edu.kit.datamanager.idoris.users.dao.IUserDao; +import edu.kit.datamanager.idoris.users.entities.ORCiDUser; +import edu.kit.datamanager.idoris.users.entities.TextUser; +import edu.kit.datamanager.idoris.users.entities.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.net.URL; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * Implementation of the UserService interface. + * Provides operations for managing both TextUsers and ORCiDUsers. + */ +@Service +@Transactional +public class UserServiceImpl implements UserService { + + private final IUserDao userDao; + + @Autowired + public UserServiceImpl(IUserDao userDao) { + this.userDao = userDao; + } + + @Override + public List findAllUsers() { + return userDao.findAll(); + } + + @Override + public Optional findUserById(String id) { + return userDao.findById(id); + } + + @Override + public List findAllTextUsers() { + return StreamSupport.stream(userDao.findAllTextUsers().spliterator(), false) + .map(user -> (TextUser) user) + .collect(Collectors.toList()); + } + + @Override + public Optional findTextUserByEmail(String email) { + User user = userDao.findTextUserByEmail(email); + return Optional.ofNullable(user).map(u -> (TextUser) u); + } + + @Override + public List findAllORCiDUsers() { + return StreamSupport.stream(userDao.findAllORCiDUsers().spliterator(), false) + .map(user -> (ORCiDUser) user) + .collect(Collectors.toList()); + } + + @Override + public Optional findORCiDUserByORCiD(URL orcid) { + User user = userDao.findORCiDUserByORCiD(orcid.toString()); + return Optional.ofNullable(user).map(u -> (ORCiDUser) u); + } + + @Override + public TextUser createTextUser(TextUser user) { + // Set the type field for proper serialization/deserialization + user.setType("text"); + return userDao.save(user); + } + + @Override + public ORCiDUser createORCiDUser(ORCiDUser user) { + // Set the type field for proper serialization/deserialization + user.setType("orcid"); + return userDao.save(user); + } + + @Override + public User updateUser(String id, User user) { + if (!userDao.existsById(id)) { + throw new IllegalArgumentException("User with ID " + id + " not found"); + } + + // Ensure the ID is set correctly + if (user instanceof TextUser) { + user.setInternalId(id); + } else if (user instanceof ORCiDUser) { + user.setInternalId(id); + } + + return userDao.save(user); + } + + @Override + public void deleteUser(String id) { + if (!userDao.existsById(id)) { + throw new IllegalArgumentException("User with ID " + id + " not found"); + } + userDao.deleteById(id); + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/users/web/api/IUserApi.java b/src/main/java/edu/kit/datamanager/idoris/users/web/api/IUserApi.java new file mode 100644 index 0000000..b01ba09 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/users/web/api/IUserApi.java @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2024-2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.users.web.api; + +import edu.kit.datamanager.idoris.users.entities.ORCiDUser; +import edu.kit.datamanager.idoris.users.entities.TextUser; +import edu.kit.datamanager.idoris.users.entities.User; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.EntityModel; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +/** + * API interface for User endpoints. + * This interface defines the REST API for managing User entities. + */ +@Tag(name = "User Management", description = "API for managing users in the system") +public interface IUserApi { + + /** + * Gets all User entities. + * + * @return a collection of all User entities + */ + @GetMapping + @Operation( + summary = "Get all users", + description = "Retrieves all users in the system", + responses = { + @ApiResponse(responseCode = "200", description = "Users retrieved successfully", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = User.class))) + } + ) + ResponseEntity>> getAllUsers(); + + /** + * Gets a User entity by its ID. + * + * @param id the ID of the User to retrieve + * @return the User entity + */ + @GetMapping("/{id}") + @Operation( + summary = "Get user by ID", + description = "Retrieves a user by their internal ID", + responses = { + @ApiResponse(responseCode = "200", description = "User retrieved successfully", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = User.class))), + @ApiResponse(responseCode = "404", description = "User not found") + } + ) + ResponseEntity> getUserById( + @Parameter(description = "ID of the User", required = true) + @PathVariable String id); + + /** + * Gets all TextUser entities. + * + * @return a collection of all TextUser entities + */ + @GetMapping("/text") + @Operation( + summary = "Get all text users", + description = "Retrieves all text users in the system", + responses = { + @ApiResponse(responseCode = "200", description = "Text users retrieved successfully", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = TextUser.class))) + } + ) + ResponseEntity>> getAllTextUsers(); + + /** + * Gets a TextUser entity by its email. + * + * @param email the email of the TextUser to retrieve + * @return the TextUser entity + */ + @GetMapping("/text/email/{email}") + @Operation( + summary = "Get text user by email", + description = "Retrieves a text user by their email", + responses = { + @ApiResponse(responseCode = "200", description = "Text user retrieved successfully", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = TextUser.class))), + @ApiResponse(responseCode = "404", description = "Text user not found") + } + ) + ResponseEntity> getTextUserByEmail( + @Parameter(description = "Email of the TextUser", required = true) + @PathVariable String email); + + /** + * Gets all ORCiDUser entities. + * + * @return a collection of all ORCiDUser entities + */ + @GetMapping("/orcid") + @Operation( + summary = "Get all ORCID users", + description = "Retrieves all ORCID users in the system", + responses = { + @ApiResponse(responseCode = "200", description = "ORCID users retrieved successfully", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = ORCiDUser.class))) + } + ) + ResponseEntity>> getAllORCiDUsers(); + + /** + * Gets an ORCiDUser entity by its ORCID. + * + * @param orcidStr the ORCID identifier string of the ORCiDUser to retrieve + * @return the ORCiDUser entity + */ + @GetMapping("/orcid/{orcidStr}") + @Operation( + summary = "Get ORCID user by ORCID", + description = "Retrieves an ORCID user by their ORCID identifier", + responses = { + @ApiResponse(responseCode = "200", description = "ORCID user retrieved successfully", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = ORCiDUser.class))), + @ApiResponse(responseCode = "404", description = "ORCID user not found") + } + ) + ResponseEntity> getORCiDUserByORCiD( + @Parameter(description = "ORCID identifier of the ORCiDUser", required = true) + @PathVariable String orcidStr); + + /** + * Creates a new TextUser entity. + * + * @param user the TextUser entity to create + * @return the created TextUser entity + */ + @PostMapping("/text") + @Operation( + summary = "Create text user", + description = "Creates a new text user", + responses = { + @ApiResponse(responseCode = "201", description = "Text user created successfully", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = TextUser.class))) + } + ) + ResponseEntity> createTextUser( + @Parameter(description = "TextUser to create", required = true) + @RequestBody TextUser user); + + /** + * Creates a new ORCiDUser entity. + * + * @param user the ORCiDUser entity to create + * @return the created ORCiDUser entity + */ + @PostMapping("/orcid") + @Operation( + summary = "Create ORCID user", + description = "Creates a new ORCID user", + responses = { + @ApiResponse(responseCode = "201", description = "ORCID user created successfully", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = ORCiDUser.class))) + } + ) + ResponseEntity> createORCiDUser( + @Parameter(description = "ORCiDUser to create", required = true) + @RequestBody ORCiDUser user); + + /** + * Updates an existing User entity. + * + * @param id the ID of the User to update + * @param user the updated User entity + * @return the updated User entity + */ + @PutMapping("/{id}") + @Operation( + summary = "Update user", + description = "Updates an existing user", + responses = { + @ApiResponse(responseCode = "200", description = "User updated successfully", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = User.class))), + @ApiResponse(responseCode = "404", description = "User not found") + } + ) + ResponseEntity> updateUser( + @Parameter(description = "ID of the User", required = true) + @PathVariable String id, + @Parameter(description = "Updated User", required = true) + @RequestBody User user); + + /** + * Deletes a User entity. + * + * @param id the ID of the User to delete + * @return no content + */ + @DeleteMapping("/{id}") + @Operation( + summary = "Delete user", + description = "Deletes a user by their internal ID", + responses = { + @ApiResponse(responseCode = "204", description = "User deleted successfully"), + @ApiResponse(responseCode = "404", description = "User not found") + } + ) + ResponseEntity deleteUser( + @Parameter(description = "ID of the User", required = true) + @PathVariable String id); +} diff --git a/src/main/java/edu/kit/datamanager/idoris/users/web/hateoas/UserModelAssembler.java b/src/main/java/edu/kit/datamanager/idoris/users/web/hateoas/UserModelAssembler.java new file mode 100644 index 0000000..61335a2 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/users/web/hateoas/UserModelAssembler.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024-2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.users.web.hateoas; + +import edu.kit.datamanager.idoris.users.entities.ORCiDUser; +import edu.kit.datamanager.idoris.users.entities.TextUser; +import edu.kit.datamanager.idoris.users.entities.User; +import edu.kit.datamanager.idoris.users.web.v1.UserController; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.server.RepresentationModelAssembler; +import org.springframework.stereotype.Component; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +/** + * Assembler for converting User entities to EntityModel objects with HATEOAS links. + */ +@Component +public class UserModelAssembler implements RepresentationModelAssembler> { + + /** + * Converts a User entity to an EntityModel with HATEOAS links. + * + * @param user the User entity to convert + * @return an EntityModel containing the User and links + */ + @Override + public EntityModel toModel(User user) { + EntityModel entityModel = EntityModel.of(user); + + // Add self link + entityModel.add(linkTo(methodOn(UserController.class).getUserById(user.getInternalId())).withSelfRel()); + + // Add link to all users + entityModel.add(linkTo(methodOn(UserController.class).getAllUsers()).withRel("users")); + + // Add type-specific links + if (user instanceof TextUser) { + entityModel.add(linkTo(methodOn(UserController.class).getAllTextUsers()).withRel("textUsers")); + entityModel.add(linkTo(methodOn(UserController.class).getTextUserByEmail(((TextUser) user).getEmail())).withRel("byEmail")); + } else if (user instanceof ORCiDUser) { + entityModel.add(linkTo(methodOn(UserController.class).getAllORCiDUsers()).withRel("orcidUsers")); + entityModel.add(linkTo(methodOn(UserController.class).getORCiDUserByORCiD(String.valueOf(((ORCiDUser) user).getOrcid()))).withRel("byOrcid")); + } + + return entityModel; + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/users/web/v1/UserController.java b/src/main/java/edu/kit/datamanager/idoris/users/web/v1/UserController.java new file mode 100644 index 0000000..c765303 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/users/web/v1/UserController.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2024-2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.users.web.v1; + +import edu.kit.datamanager.idoris.users.entities.ORCiDUser; +import edu.kit.datamanager.idoris.users.entities.TextUser; +import edu.kit.datamanager.idoris.users.entities.User; +import edu.kit.datamanager.idoris.users.services.UserService; +import edu.kit.datamanager.idoris.users.web.api.IUserApi; +import edu.kit.datamanager.idoris.users.web.hateoas.UserModelAssembler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.EntityModel; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import java.net.URL; +import java.util.List; +import java.util.stream.Collectors; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +/** + * REST controller for User entities. + * This controller provides endpoints for managing User entities. + */ +@RestController +@RequestMapping("/v1/users") +public class UserController implements IUserApi { + + private final UserService userService; + private final UserModelAssembler userModelAssembler; + + @Autowired + public UserController(UserService userService, UserModelAssembler userModelAssembler) { + this.userService = userService; + this.userModelAssembler = userModelAssembler; + } + + @Override + public ResponseEntity>> getAllUsers() { + List> users = userService.findAllUsers().stream() + .map(userModelAssembler::toModel) + .collect(Collectors.toList()); + + CollectionModel> collectionModel = CollectionModel.of( + users, + linkTo(methodOn(UserController.class).getAllUsers()).withSelfRel() + ); + + return ResponseEntity.ok(collectionModel); + } + + @Override + public ResponseEntity> getUserById(String id) { + return userService.findUserById(id) + .map(userModelAssembler::toModel) + .map(ResponseEntity::ok) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found")); + } + + @Override + public ResponseEntity>> getAllTextUsers() { + List> users = userService.findAllTextUsers().stream() + .map(user -> EntityModel.of(user, + linkTo(methodOn(UserController.class).getTextUserByEmail(user.getEmail())).withSelfRel(), + linkTo(methodOn(UserController.class).getAllTextUsers()).withRel("textUsers"))) + .collect(Collectors.toList()); + + CollectionModel> collectionModel = CollectionModel.of( + users, + linkTo(methodOn(UserController.class).getAllTextUsers()).withSelfRel(), + linkTo(methodOn(UserController.class).getAllUsers()).withRel("users") + ); + + return ResponseEntity.ok(collectionModel); + } + + @Override + public ResponseEntity> getTextUserByEmail(String email) { + return userService.findTextUserByEmail(email) + .map(user -> EntityModel.of(user, + linkTo(methodOn(UserController.class).getTextUserByEmail(email)).withSelfRel(), + linkTo(methodOn(UserController.class).getAllTextUsers()).withRel("textUsers"), + linkTo(methodOn(UserController.class).getAllUsers()).withRel("users"))) + .map(ResponseEntity::ok) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Text user not found")); + } + + @Override + public ResponseEntity>> getAllORCiDUsers() { + List> users = userService.findAllORCiDUsers().stream() + .map(user -> { + // Extract ORCID identifier from the URL + String orcidStr = user.getOrcid().toString().replace("https://orcid.org/", ""); + return EntityModel.of(user, + linkTo(methodOn(UserController.class).getORCiDUserByORCiD(orcidStr)).withSelfRel(), + linkTo(methodOn(UserController.class).getAllORCiDUsers()).withRel("orcidUsers")); + }) + .collect(Collectors.toList()); + + CollectionModel> collectionModel = CollectionModel.of( + users, + linkTo(methodOn(UserController.class).getAllORCiDUsers()).withSelfRel(), + linkTo(methodOn(UserController.class).getAllUsers()).withRel("users") + ); + + return ResponseEntity.ok(collectionModel); + } + + @Override + public ResponseEntity> getORCiDUserByORCiD(String orcidStr) { + try { + // Convert ORCID string to URL + URL orcid = new URL("https://orcid.org/" + orcidStr); + return userService.findORCiDUserByORCiD(orcid) + .map(user -> EntityModel.of(user, + linkTo(methodOn(UserController.class).getORCiDUserByORCiD(orcidStr)).withSelfRel(), + linkTo(methodOn(UserController.class).getAllORCiDUsers()).withRel("orcidUsers"), + linkTo(methodOn(UserController.class).getAllUsers()).withRel("users"))) + .map(ResponseEntity::ok) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "ORCID user not found")); + } catch (java.net.MalformedURLException e) { + // Only catch MalformedURLException to return BAD_REQUEST + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid ORCID format: " + orcidStr, e); + } + } + + @Override + public ResponseEntity> createTextUser(TextUser user) { + TextUser createdUser = userService.createTextUser(user); + EntityModel entityModel = EntityModel.of(createdUser, + linkTo(methodOn(UserController.class).getTextUserByEmail(createdUser.getEmail())).withSelfRel(), + linkTo(methodOn(UserController.class).getAllTextUsers()).withRel("textUsers"), + linkTo(methodOn(UserController.class).getAllUsers()).withRel("users")); + return ResponseEntity.status(HttpStatus.CREATED).body(entityModel); + } + + @Override + public ResponseEntity> createORCiDUser(ORCiDUser user) { + ORCiDUser createdUser = userService.createORCiDUser(user); + // Extract ORCID identifier from the URL + String orcidStr = createdUser.getOrcid().toString().replace("https://orcid.org/", ""); + EntityModel entityModel = EntityModel.of(createdUser, + linkTo(methodOn(UserController.class).getORCiDUserByORCiD(orcidStr)).withSelfRel(), + linkTo(methodOn(UserController.class).getAllORCiDUsers()).withRel("orcidUsers"), + linkTo(methodOn(UserController.class).getAllUsers()).withRel("users")); + return ResponseEntity.status(HttpStatus.CREATED).body(entityModel); + } + + @Override + public ResponseEntity> updateUser(String id, User user) { + try { + User updatedUser = userService.updateUser(id, user); + EntityModel entityModel = userModelAssembler.toModel(updatedUser); + return ResponseEntity.ok(entityModel); + } catch (IllegalArgumentException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); + } + } + + @Override + public ResponseEntity deleteUser(String id) { + try { + userService.deleteUser(id); + return ResponseEntity.noContent().build(); + } catch (IllegalArgumentException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); + } + } +} diff --git a/src/test/java/edu/kit/datamanager/idoris/users/services/UserServiceImplTest.java b/src/test/java/edu/kit/datamanager/idoris/users/services/UserServiceImplTest.java new file mode 100644 index 0000000..f1f6aa8 --- /dev/null +++ b/src/test/java/edu/kit/datamanager/idoris/users/services/UserServiceImplTest.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2024-2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.users.services; + +import edu.kit.datamanager.idoris.users.dao.IUserDao; +import edu.kit.datamanager.idoris.users.entities.ORCiDUser; +import edu.kit.datamanager.idoris.users.entities.TextUser; +import edu.kit.datamanager.idoris.users.entities.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class UserServiceImplTest { + + @Mock + private IUserDao userDao; + + @InjectMocks + private UserServiceImpl userService; + + private TextUser textUser; + private ORCiDUser orcidUser; + private final String userId = "test-id"; + + @BeforeEach + void setUp() throws MalformedURLException { + textUser = new TextUser("John Doe", "john.doe@example.com", "Test user"); + orcidUser = new ORCiDUser(new URL("https://orcid.org/0000-0000-0000-0000")); + } + + @Test + void findAllUsers() { + when(userDao.findAll()).thenReturn(Arrays.asList(textUser, orcidUser)); + + List users = userService.findAllUsers(); + + assertEquals(2, users.size()); + verify(userDao, times(1)).findAll(); + } + + @Test + void findUserById() { + when(userDao.findById(userId)).thenReturn(Optional.of(textUser)); + + Optional result = userService.findUserById(userId); + + assertTrue(result.isPresent()); + assertEquals(textUser, result.get()); + verify(userDao, times(1)).findById(userId); + } + + @Test + void findAllTextUsers() { + when(userDao.findAllTextUsers()).thenReturn(Collections.singletonList(textUser)); + + List users = userService.findAllTextUsers(); + + assertEquals(1, users.size()); + assertEquals(textUser, users.get(0)); + verify(userDao, times(1)).findAllTextUsers(); + } + + @Test + void findTextUserByEmail() { + String email = "john.doe@example.com"; + when(userDao.findTextUserByEmail(email)).thenReturn(textUser); + + Optional result = userService.findTextUserByEmail(email); + + assertTrue(result.isPresent()); + assertEquals(textUser, result.get()); + verify(userDao, times(1)).findTextUserByEmail(email); + } + + @Test + void findAllORCiDUsers() { + when(userDao.findAllORCiDUsers()).thenReturn(Collections.singletonList(orcidUser)); + + List users = userService.findAllORCiDUsers(); + + assertEquals(1, users.size()); + assertEquals(orcidUser, users.get(0)); + verify(userDao, times(1)).findAllORCiDUsers(); + } + + @Test + void findORCiDUserByORCiD() throws MalformedURLException { + URL orcid = new URL("https://orcid.org/0000-0000-0000-0000"); + when(userDao.findORCiDUserByORCiD(orcid.toString())).thenReturn(orcidUser); + + Optional result = userService.findORCiDUserByORCiD(orcid); + + assertTrue(result.isPresent()); + assertEquals(orcidUser, result.get()); + verify(userDao, times(1)).findORCiDUserByORCiD(orcid.toString()); + } + + @Test + void createTextUser() { + when(userDao.save(any(TextUser.class))).thenReturn(textUser); + + TextUser result = userService.createTextUser(textUser); + + assertEquals(textUser, result); + assertEquals("text", textUser.getType()); + verify(userDao, times(1)).save(textUser); + } + + @Test + void createORCiDUser() { + when(userDao.save(any(ORCiDUser.class))).thenReturn(orcidUser); + + ORCiDUser result = userService.createORCiDUser(orcidUser); + + assertEquals(orcidUser, result); + assertEquals("orcid", orcidUser.getType()); + verify(userDao, times(1)).save(orcidUser); + } + + @Test + void updateUser() { + when(userDao.existsById(userId)).thenReturn(true); + when(userDao.save(any(User.class))).thenReturn(textUser); + + User result = userService.updateUser(userId, textUser); + + assertEquals(textUser, result); + verify(userDao, times(1)).existsById(userId); + verify(userDao, times(1)).save(textUser); + } + + @Test + void updateUser_NotFound() { + when(userDao.existsById(userId)).thenReturn(false); + + assertThrows(IllegalArgumentException.class, () -> userService.updateUser(userId, textUser)); + verify(userDao, times(1)).existsById(userId); + verify(userDao, never()).save(any(User.class)); + } + + @Test + void deleteUser() { + when(userDao.existsById(userId)).thenReturn(true); + + userService.deleteUser(userId); + + verify(userDao, times(1)).existsById(userId); + verify(userDao, times(1)).deleteById(userId); + } + + @Test + void deleteUser_NotFound() { + when(userDao.existsById(userId)).thenReturn(false); + + assertThrows(IllegalArgumentException.class, () -> userService.deleteUser(userId)); + verify(userDao, times(1)).existsById(userId); + verify(userDao, never()).deleteById(anyString()); + } +} \ No newline at end of file diff --git a/src/test/java/edu/kit/datamanager/idoris/users/web/v1/UserControllerTest.java b/src/test/java/edu/kit/datamanager/idoris/users/web/v1/UserControllerTest.java new file mode 100644 index 0000000..5966b7e --- /dev/null +++ b/src/test/java/edu/kit/datamanager/idoris/users/web/v1/UserControllerTest.java @@ -0,0 +1,271 @@ +/* + * Copyright (c) 2024-2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.users.web.v1; + +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.kit.datamanager.idoris.users.entities.ORCiDUser; +import edu.kit.datamanager.idoris.users.entities.TextUser; +import edu.kit.datamanager.idoris.users.entities.User; +import edu.kit.datamanager.idoris.users.services.UserService; +import edu.kit.datamanager.idoris.users.web.hateoas.UserModelAssembler; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.hateoas.EntityModel; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +class UserControllerTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + private final String userId = "test-id"; + private MockMvc mockMvc; + @Mock + private UserService userService; + @Mock + private UserModelAssembler userModelAssembler; + private TextUser textUser; + private ORCiDUser orcidUser; + + @BeforeEach + void setUp() throws Exception { + textUser = new TextUser("John Doe", "john.doe@example.com", "Test user"); + orcidUser = new ORCiDUser(new URL("https://orcid.org/0000-0000-0000-0000")); + + // Set up the model assembler to return EntityModel of the entity + // Use lenient() to avoid "unnecessary stubbing" errors + lenient().when(userModelAssembler.toModel(any(User.class))).thenAnswer(invocation -> { + User user = invocation.getArgument(0); + return EntityModel.of(user); + }); + + // Initialize mockMvc with the controller and mocked services + UserController userController = new UserController(userService, userModelAssembler); + mockMvc = MockMvcBuilders.standaloneSetup(userController).build(); + } + + @Test + void getAllUsers() throws Exception { + List users = Arrays.asList(textUser, orcidUser); + when(userService.findAllUsers()).thenReturn(users); + + mockMvc.perform(get("/v1/users")) + .andExpect(status().isOk()); + + verify(userService, times(1)).findAllUsers(); + } + + @Test + void getUserById() throws Exception { + when(userService.findUserById(userId)).thenReturn(Optional.of(textUser)); + + mockMvc.perform(get("/v1/users/{id}", userId)) + .andExpect(status().isOk()); + + verify(userService, times(1)).findUserById(userId); + verify(userModelAssembler, times(1)).toModel(any(User.class)); + } + + @Test + void getUserById_NotFound() throws Exception { + when(userService.findUserById(userId)).thenReturn(Optional.empty()); + + mockMvc.perform(get("/v1/users/{id}", userId)) + .andExpect(status().isNotFound()); + + verify(userService, times(1)).findUserById(userId); + } + + @Test + void getAllTextUsers() throws Exception { + List users = Collections.singletonList(textUser); + when(userService.findAllTextUsers()).thenReturn(users); + + mockMvc.perform(get("/v1/users/text")) + .andExpect(status().isOk()); + + verify(userService, times(1)).findAllTextUsers(); + } + + @Test + void getTextUserByEmail() throws Exception { + String email = "john.doe@example.com"; + when(userService.findTextUserByEmail(email)).thenReturn(Optional.of(textUser)); + + mockMvc.perform(get("/v1/users/text/email/{email}", email)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name", is("John Doe"))) + .andExpect(jsonPath("$.email", is(email))); + + verify(userService, times(1)).findTextUserByEmail(email); + } + + @Test + void getTextUserByEmail_NotFound() throws Exception { + String email = "nonexistent@example.com"; + when(userService.findTextUserByEmail(email)).thenReturn(Optional.empty()); + + mockMvc.perform(get("/v1/users/text/email/{email}", email)) + .andExpect(status().isNotFound()); + + verify(userService, times(1)).findTextUserByEmail(email); + } + + @Test + void getAllORCiDUsers() throws Exception { + List users = Collections.singletonList(orcidUser); + when(userService.findAllORCiDUsers()).thenReturn(users); + + mockMvc.perform(get("/v1/users/orcid")) + .andExpect(status().isOk()); + + verify(userService, times(1)).findAllORCiDUsers(); + } + + @Test + void getORCiDUserByORCiD() throws Exception { + String orcidStr = "0000-0000-0000-0000"; + URL orcid = new URL("https://orcid.org/" + orcidStr); + when(userService.findORCiDUserByORCiD(any(URL.class))).thenReturn(Optional.of(orcidUser)); + + mockMvc.perform(get("/v1/users/orcid/{orcid}", orcidStr)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.orcid", is(orcid.toString()))); + + verify(userService, times(1)).findORCiDUserByORCiD(any(URL.class)); + } + + @Test + void getORCiDUserByORCiD_NotFound() throws Exception { + String orcidStr = "0000-0000-0000-0001"; + URL orcid = new URL("https://orcid.org/" + orcidStr); + when(userService.findORCiDUserByORCiD(any(URL.class))).thenReturn(Optional.empty()); + + mockMvc.perform(get("/v1/users/orcid/{orcid}", orcidStr)) + .andExpect(status().isNotFound()); + + verify(userService, times(1)).findORCiDUserByORCiD(any(URL.class)); + } + + @Test + void createTextUser() throws Exception { + when(userService.createTextUser(any(TextUser.class))).thenReturn(textUser); + + mockMvc.perform(post("/v1/users/text") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(textUser))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.name", is("John Doe"))) + .andExpect(jsonPath("$.email", is("john.doe@example.com"))); + + verify(userService, times(1)).createTextUser(any(TextUser.class)); + } + + @Test + void createORCiDUser() throws Exception { + when(userService.createORCiDUser(any(ORCiDUser.class))).thenReturn(orcidUser); + + mockMvc.perform(post("/v1/users/orcid") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(orcidUser))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.orcid", is("https://orcid.org/0000-0000-0000-0000"))); + + verify(userService, times(1)).createORCiDUser(any(ORCiDUser.class)); + } + + @Test + void updateUser() throws Exception { + when(userService.updateUser(eq(userId), any(User.class))).thenReturn(textUser); + + mockMvc.perform(put("/v1/users/{id}", userId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(textUser))) + .andExpect(status().isOk()); + + verify(userService, times(1)).updateUser(eq(userId), any(User.class)); + verify(userModelAssembler, times(1)).toModel(any(User.class)); + } + + @Test + void updateUser_NotFound() throws Exception { + when(userService.updateUser(eq(userId), any(User.class))) + .thenThrow(new IllegalArgumentException("User not found")); + + mockMvc.perform(put("/v1/users/{id}", userId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(textUser))) + .andExpect(status().isNotFound()); + + verify(userService, times(1)).updateUser(eq(userId), any(User.class)); + } + + @Test + void deleteUser() throws Exception { + doNothing().when(userService).deleteUser(userId); + + mockMvc.perform(delete("/v1/users/{id}", userId)) + .andExpect(status().isNoContent()); + + verify(userService, times(1)).deleteUser(userId); + } + + @Test + void deleteUser_NotFound() throws Exception { + doThrow(new IllegalArgumentException("User not found")).when(userService).deleteUser(userId); + + mockMvc.perform(delete("/v1/users/{id}", userId)) + .andExpect(status().isNotFound()); + + verify(userService, times(1)).deleteUser(userId); + } + + @Configuration + @Import(UserController.class) + static class TestConfig { + @Bean + public UserService userService() { + return mock(UserService.class); + } + + @Bean + public UserModelAssembler userModelAssembler() { + return mock(UserModelAssembler.class); + } + } +} From ec00634fcd9f129b5c6a9eb31c91bafb31490d00 Mon Sep 17 00:00:00 2001 From: Maximilian Inckmann Date: Thu, 17 Jul 2025 12:34:16 +0200 Subject: [PATCH 05/19] added PID creation Signed-off-by: Maximilian Inckmann --- .../idoris/configuration/HateoasConfig.java | 136 ----------- .../entities/AdministrativeMetadata.java | 5 + .../core/events/EntityDeletedEvent.java | 12 +- .../idoris/core/events/PIDGeneratedEvent.java | 22 ++ .../web/hateoas/DataTypeModelAssembler.java | 35 ++- .../hateoas/TypeProfileModelAssembler.java | 48 +++- .../notification/EntityChangeNotifier.java | 2 +- .../idoris/pids/MetadataEventListener.java | 135 +++++++++++ .../pids/PIDGenerationEventListener.java | 74 ------ .../idoris/pids/TypedPIDMakerIDGenerator.java | 193 +++------------ .../pids/entities/PersistentIdentifier.java | 157 ++++++++++++ .../datamanager/idoris/pids/package-info.java | 2 +- .../PersistentIdentifierRepository.java | 83 +++++++ .../services/PersistentIdentifierService.java | 223 ++++++++++++++++++ .../idoris/pids/utils/PIDRecordMapper.java | 197 ++++++++++++++++ .../idoris/pids/web/api/IPidApi.java | 97 ++++++++ .../PersistentIdentifierModelAssembler.java | 71 ++++++ .../idoris/pids/web/v1/PidController.java | 172 ++++++++++++++ .../pids/web/v1/PidRedirectController.java | 160 ------------- .../pids/PIDTombstoneEventListenerTest.java | 221 +++++++++++++++++ .../pids/TypedPIDMakerIDGeneratorTest.java | 205 ++++++++++++++++ .../PersistentIdentifierControllerTest.java | 164 +++++++++++++ .../idoris/pids/web/v1/PidControllerTest.java | 197 ++++++++++++++++ .../web/v1/PidRedirectControllerV2Test.java | 175 ++++++++++++++ 24 files changed, 2231 insertions(+), 555 deletions(-) delete mode 100644 src/main/java/edu/kit/datamanager/idoris/configuration/HateoasConfig.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/pids/MetadataEventListener.java delete mode 100644 src/main/java/edu/kit/datamanager/idoris/pids/PIDGenerationEventListener.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/pids/entities/PersistentIdentifier.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/pids/repositories/PersistentIdentifierRepository.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/pids/services/PersistentIdentifierService.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/pids/utils/PIDRecordMapper.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/pids/web/api/IPidApi.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/pids/web/hateoas/PersistentIdentifierModelAssembler.java create mode 100644 src/main/java/edu/kit/datamanager/idoris/pids/web/v1/PidController.java delete mode 100644 src/main/java/edu/kit/datamanager/idoris/pids/web/v1/PidRedirectController.java create mode 100644 src/test/java/edu/kit/datamanager/idoris/pids/PIDTombstoneEventListenerTest.java create mode 100644 src/test/java/edu/kit/datamanager/idoris/pids/TypedPIDMakerIDGeneratorTest.java create mode 100644 src/test/java/edu/kit/datamanager/idoris/pids/web/v1/PersistentIdentifierControllerTest.java create mode 100644 src/test/java/edu/kit/datamanager/idoris/pids/web/v1/PidControllerTest.java create mode 100644 src/test/java/edu/kit/datamanager/idoris/pids/web/v1/PidRedirectControllerV2Test.java diff --git a/src/main/java/edu/kit/datamanager/idoris/configuration/HateoasConfig.java b/src/main/java/edu/kit/datamanager/idoris/configuration/HateoasConfig.java deleted file mode 100644 index 384a984..0000000 --- a/src/main/java/edu/kit/datamanager/idoris/configuration/HateoasConfig.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (c) 2025 Karlsruhe Institute of Technology - * - * 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 edu.kit.datamanager.idoris.configuration; - -import edu.kit.datamanager.idoris.datatypes.entities.AtomicDataType; -import edu.kit.datamanager.idoris.datatypes.entities.DataType; -import edu.kit.datamanager.idoris.datatypes.entities.TypeProfile; -import edu.kit.datamanager.idoris.datatypes.web.v1.AtomicDataTypeController; -import edu.kit.datamanager.idoris.datatypes.web.v1.TypeProfileController; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.hateoas.EntityModel; -import org.springframework.hateoas.server.RepresentationModelProcessor; - -import java.util.Objects; - -import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; -import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; - -/** - * Configuration class for HATEOAS-related settings. - * This class configures representation model processors and other HATEOAS-related beans. - */ -@Configuration -public class HateoasConfig { - - /** - * Creates a representation model processor for TypeProfile entities. - * This processor adds links to validate, get inherited attributes, and get the inheritance tree. - * - * @return a representation model processor for TypeProfile entities - */ - @Bean - public RepresentationModelProcessor> typeProfileProcessor() { - return new RepresentationModelProcessor>() { - @Override - public EntityModel process(EntityModel model) { - TypeProfile typeProfile = Objects.requireNonNull(model.getContent()); - String pid = typeProfile.getPid(); - - // Add links to related resources - model.add(linkTo(methodOn(TypeProfileController.class).validate(pid)).withRel("validate")); - model.add(linkTo(methodOn(TypeProfileController.class).getInheritedAttributes(pid)).withRel("inheritedAttributes")); - model.add(linkTo(methodOn(TypeProfileController.class).getInheritanceTree(pid)).withRel("inheritanceTree")); - - return model; - } - }; - } - - /** - * Creates a representation model processor for DataType entities. - * This processor adds links to operations for the data type. - * - * @return a representation model processor for DataType entities - */ - @Bean - public RepresentationModelProcessor> dataTypeProcessor() { - return new RepresentationModelProcessor>() { - @Override - public EntityModel process(EntityModel model) { - DataType dataType = Objects.requireNonNull(model.getContent()); - String pid = dataType.getPid(); - - // Add link to operations for this data type - // The link depends on the type of DataType - if (dataType instanceof AtomicDataType) { - model.add(linkTo(methodOn(AtomicDataTypeController.class).getOperationsForAtomicDataType(pid)).withRel("operations")); - } else if (dataType instanceof TypeProfile) { - model.add(linkTo(methodOn(TypeProfileController.class).getOperationsForTypeProfile(pid)).withRel("operations")); - } - - return model; - } - }; - } - -// /** -// * Creates a representation model processor that adds validation results to entity models. -// * This processor executes validation rules and adds the results to the model. -// * -// * @param ruleService the rule service -// * @param applicationProperties the application properties -// * @return a representation model processor that adds validation results -// */ -// @Bean -// public RepresentationModelProcessor> validatorProcessor( -// RuleService ruleService, -// ApplicationProperties applicationProperties) { -// -// return model -> { -// if (model instanceof EntityModel && ((EntityModel) model).getContent() instanceof VisitableElement element) { -// // Use RuleService to process validation with the VALIDATE task -// ValidationResult validationResult = ruleService.executeRules( -// RuleTask.VALIDATE, -// element, -// ValidationResult::new -// ); -// -// // Convert ValidationResult to the expected format for the response -// if (!validationResult.isEmpty()) { -// Map> filteredMessages = -// validationResult.getOutputMessages() -// .entrySet() -// .stream() -// .filter(entry -> entry.getKey().isHigherOrEqualTo(applicationProperties.getValidationLevel())) -// .filter(entry -> !entry.getValue().isEmpty()) -// .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); -// -// if (!filteredMessages.isEmpty()) { -// Map results = Map.of( -// "validationResult", filteredMessages, -// "originalModel", model -// ); -// return CollectionModel.of(Set.of(results)); -// } -// } -// } -// return model; -// }; -// } -} diff --git a/src/main/java/edu/kit/datamanager/idoris/core/domain/entities/AdministrativeMetadata.java b/src/main/java/edu/kit/datamanager/idoris/core/domain/entities/AdministrativeMetadata.java index d5d9312..096fd7d 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/domain/entities/AdministrativeMetadata.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/domain/entities/AdministrativeMetadata.java @@ -38,6 +38,11 @@ @AllArgsConstructor(access = AccessLevel.PROTECTED) @Node("IDORIS") public abstract class AdministrativeMetadata extends VisitableElement implements Serializable { + /** + * @deprecated This field is deprecated and will be removed in a future release. + * Use PersistentIdentifierService to get the PID for an entity instead. + */ + @Deprecated @GeneratedValue(ConfigurablePIDGenerator.class) String pid; diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/EntityDeletedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityDeletedEvent.java index c57e4c5..a75955a 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/events/EntityDeletedEvent.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/EntityDeletedEvent.java @@ -32,7 +32,7 @@ public class EntityDeletedEvent extends AbstractDomainEvent { private final T entity; private final String entityType; - private final String entityPid; + private final String entityInternalId; /** * Creates a new EntityDeletedEvent for the given entity. @@ -42,7 +42,7 @@ public class EntityDeletedEvent extends Abstra public EntityDeletedEvent(T entity) { this.entity = entity; this.entityType = entity.getClass().getSimpleName(); - this.entityPid = entity.getPid(); + this.entityInternalId = entity.getInternalId(); } /** @@ -64,11 +64,11 @@ public String getEntityType() { } /** - * Gets the PID of the entity that was deleted. + * Gets the internal ID of the entity that was deleted. * - * @return the entity PID + * @return the entity internal ID */ - public String getEntityPid() { - return entityPid; + public String getEntityInternalId() { + return entityInternalId; } } diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/PIDGeneratedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/PIDGeneratedEvent.java index 59c0af9..c44be91 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/events/PIDGeneratedEvent.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/PIDGeneratedEvent.java @@ -33,6 +33,8 @@ public class PIDGeneratedEvent extends Abstrac private final T entity; private final String pid; private final boolean isNewPID; + private final String entityInternalId; + private final String entityType; /** * Creates a new PIDGeneratedEvent for the given entity and PID. @@ -45,6 +47,8 @@ public PIDGeneratedEvent(T entity, String pid, boolean isNewPID) { this.entity = entity; this.pid = pid; this.isNewPID = isNewPID; + this.entityInternalId = entity.getInternalId(); + this.entityType = entity.getClass().getSimpleName(); } /** @@ -84,4 +88,22 @@ public String getPid() { public boolean isNewPID() { return isNewPID; } + + /** + * Gets the internal ID of the entity for which the PID was generated. + * + * @return the entity internal ID + */ + public String getEntityInternalId() { + return entityInternalId; + } + + /** + * Gets the type of the entity for which the PID was generated. + * + * @return the entity type + */ + public String getEntityType() { + return entityType; + } } diff --git a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/DataTypeModelAssembler.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/DataTypeModelAssembler.java index 4d981c1..9d162fb 100644 --- a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/DataTypeModelAssembler.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/DataTypeModelAssembler.java @@ -24,6 +24,7 @@ import edu.kit.datamanager.idoris.datatypes.web.v1.TypeProfileController; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.server.RepresentationModelProcessor; import org.springframework.stereotype.Component; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; @@ -32,9 +33,15 @@ /** * Assembler for converting DataType entities to EntityModel objects with HATEOAS links. * This assembler delegates to specific assemblers based on the type of DataType. + *

+ * This class combines the functionality of both an EntityModelAssembler and a + * RepresentationModelProcessor, handling all HATEOAS concerns for DataType entities + * in one place, according to Domain-Driven Design principles. */ @Component -public class DataTypeModelAssembler implements EntityModelAssembler { +public class DataTypeModelAssembler implements + EntityModelAssembler, + RepresentationModelProcessor> { @Autowired private AtomicDataTypeModelAssembler atomicDataTypeModelAssembler; @@ -69,4 +76,30 @@ public EntityModel toModel(DataType dataType) { return entityModel; } } + + /** + * Processes an EntityModel of DataType to add additional HATEOAS links. + * This method adds type-specific operation links based on the DataType instance. + * + * @param model the EntityModel to process + * @return the processed EntityModel with additional links + */ + @Override + public EntityModel process(EntityModel model) { + DataType dataType = model.getContent(); + if (dataType == null) { + return model; + } + + String pid = dataType.getPid(); + + // Add link to operations for this data type based on its type + if (dataType instanceof AtomicDataType) { + model.add(linkTo(methodOn(AtomicDataTypeController.class).getOperationsForAtomicDataType(pid)).withRel("operations")); + } else if (dataType instanceof TypeProfile) { + model.add(linkTo(methodOn(TypeProfileController.class).getOperationsForTypeProfile(pid)).withRel("operations")); + } + + return model; + } } diff --git a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/TypeProfileModelAssembler.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/TypeProfileModelAssembler.java index bad6be7..97d4019 100644 --- a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/TypeProfileModelAssembler.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/TypeProfileModelAssembler.java @@ -20,6 +20,7 @@ import edu.kit.datamanager.idoris.datatypes.entities.TypeProfile; import edu.kit.datamanager.idoris.datatypes.web.v1.TypeProfileController; import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.server.RepresentationModelProcessor; import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder; import org.springframework.stereotype.Component; @@ -28,12 +29,18 @@ /** * Assembler for converting TypeProfile entities to EntityModel objects with HATEOAS links. + *

+ * This class combines the functionality of both an EntityModelAssembler and a + * RepresentationModelProcessor, handling all HATEOAS concerns for TypeProfile entities + * in one place, according to Domain-Driven Design principles. */ @Component -public class TypeProfileModelAssembler implements EntityModelAssembler { +public class TypeProfileModelAssembler implements + EntityModelAssembler, + RepresentationModelProcessor> { /** - * Converts a TypeProfile entity to an EntityModel with HATEOAS links. + * Converts a TypeProfile entity to an EntityModel with basic HATEOAS links. * * @param typeProfile the TypeProfile entity to convert * @return an EntityModel containing the TypeProfile and links @@ -45,22 +52,41 @@ public EntityModel toModel(TypeProfile typeProfile) { // Add self link entityModel.add(linkTo(methodOn(TypeProfileController.class).getTypeProfile(typeProfile.getPid())).withSelfRel()); + // Add link to all type profiles + entityModel.add(linkTo(methodOn(TypeProfileController.class).getAllTypeProfiles()).withRel("typeProfiles")); + + return entityModel; + } + + /** + * Processes an EntityModel of TypeProfile to add additional HATEOAS links. + * This method is called after toModel() and enhances the model with more context-specific links. + * + * @param model the EntityModel to process + * @return the processed EntityModel with additional links + */ + @Override + public EntityModel process(EntityModel model) { + TypeProfile typeProfile = model.getContent(); + if (typeProfile == null) { + return model; + } + + String pid = typeProfile.getPid(); + // Add link to validate - entityModel.add(linkTo(methodOn(TypeProfileController.class).validate(typeProfile.getPid())).withRel("validate")); + model.add(linkTo(methodOn(TypeProfileController.class).validate(pid)).withRel("validate")); // Add link to inherited attributes - entityModel.add(linkTo(methodOn(TypeProfileController.class).getInheritedAttributes(typeProfile.getPid())).withRel("inheritedAttributes")); + model.add(linkTo(methodOn(TypeProfileController.class).getInheritedAttributes(pid)).withRel("inheritedAttributes")); // Add link to inheritance tree - entityModel.add(linkTo(methodOn(TypeProfileController.class).getInheritanceTree(typeProfile.getPid())).withRel("inheritanceTree")); + model.add(linkTo(methodOn(TypeProfileController.class).getInheritanceTree(pid)).withRel("inheritanceTree")); // Add link to operations - WebMvcLinkBuilder operationsLinkBuilder = linkTo(methodOn(TypeProfileController.class).getOperationsForTypeProfile(typeProfile.getPid())); - entityModel.add(operationsLinkBuilder.withRel("operations")); + WebMvcLinkBuilder operationsLinkBuilder = linkTo(methodOn(TypeProfileController.class).getOperationsForTypeProfile(pid)); + model.add(operationsLinkBuilder.withRel("operations")); - // Add link to all type profiles - entityModel.add(linkTo(methodOn(TypeProfileController.class).getAllTypeProfiles()).withRel("typeProfiles")); - - return entityModel; + return model; } } \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeNotifier.java b/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeNotifier.java index 5aff5d5..ed01df2 100644 --- a/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeNotifier.java +++ b/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeNotifier.java @@ -185,7 +185,7 @@ public void handleEntityUpdated(EntityUpdatedEvent event public void handleEntityDeleted(EntityDeletedEvent event) { AdministrativeMetadata entity = event.getEntity(); String entityType = event.getEntityType(); - String entityPid = event.getEntityPid(); + String entityPid = entity.getPid(); log.debug("Handling EntityDeletedEvent for entity type: {}, PID: {}", entityType, entityPid); diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/MetadataEventListener.java b/src/main/java/edu/kit/datamanager/idoris/pids/MetadataEventListener.java new file mode 100644 index 0000000..1632f63 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/pids/MetadataEventListener.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.pids; + +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; +import edu.kit.datamanager.idoris.core.events.EntityCreatedEvent; +import edu.kit.datamanager.idoris.core.events.EntityDeletedEvent; +import edu.kit.datamanager.idoris.core.events.EntityUpdatedEvent; +import edu.kit.datamanager.idoris.core.events.EventPublisherService; +import edu.kit.datamanager.idoris.pids.entities.PersistentIdentifier; +import edu.kit.datamanager.idoris.pids.services.PersistentIdentifierService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +/** + * Event listener that generates PIDs for newly created entities. + * This listener subscribes to EntityCreatedEvent and uses the PersistentIdentifierService + * to create PersistentIdentifier entities for newly created AdministrativeMetadata entities. + */ +@Component +@Slf4j +public class MetadataEventListener { + private final PersistentIdentifierService pidService; + private final EventPublisherService eventPublisher; + + /** + * Creates a new MetadataEventListener with the given dependencies. + * + * @param pidService the PersistentIdentifierService + * @param eventPublisher the event publisher service + */ + public MetadataEventListener(PersistentIdentifierService pidService, EventPublisherService eventPublisher) { + this.pidService = pidService; + this.eventPublisher = eventPublisher; + } + + /** + * Handles EntityCreatedEvent by creating a PersistentIdentifier for the entity. + * This method is executed in a new transaction to ensure that the PID creation is isolated + * from the transaction that created the entity. + * + * @param event the entity created event + */ + @EventListener(classes = {EntityCreatedEvent.class}) + @Transactional + public void handleEntityCreatedEvent(EntityCreatedEvent event) { + AdministrativeMetadata entity = event.getEntity(); + log.debug("Handling EntityCreatedEvent for entity: {}", entity); + + // Check if a PersistentIdentifier already exists for this entity + Optional existingPid = pidService.getPersistentIdentifier(entity); + + if (existingPid.isPresent()) { + log.debug("Entity already has a PersistentIdentifier: {}", existingPid.get().getPid()); + return; + } + + // Create a new PersistentIdentifier for the entity + log.info("Creating PersistentIdentifier for entity: {}", entity); + PersistentIdentifier pid = pidService.createPersistentIdentifier(entity); + log.info("Created PersistentIdentifier with PID: {} for entity: {}", pid.getPid(), entity); + + // Publish a PID generated event + eventPublisher.publishPIDGenerated(entity, pid.getPid()); + } + + /** + * Handles EntityUpdatedEvent by updating the PersistentIdentifier for the entity. + * This method is executed in a new transaction to ensure that the PID update is isolated + * from the transaction that updated the entity. + * + * @param event the entity updated event + */ + @EventListener(classes = {EntityUpdatedEvent.class}) + @Transactional + public void handleEntityUpdatedEvent(EntityUpdatedEvent event) { + AdministrativeMetadata entity = event.getEntity(); + log.debug("Handling EntityUpdatedEvent for entity: {}", entity); + + // Check if a PersistentIdentifier exists for this entity + Optional existingPid = pidService.getPersistentIdentifier(entity); + + if (existingPid.isPresent()) { + PersistentIdentifier pid = existingPid.get(); + log.info("Updating PersistentIdentifier for entity: {}", entity); + pidService.updatePIDRecord(pid); + log.info("Updated PersistentIdentifier with PID: {} for entity: {}", pid.getPid(), entity); + } else { + log.warn("No PersistentIdentifier found for entity, cannot update: {}", entity); + } + } + + + /** + * Handles EntityDeletedEvent by marking the PersistentIdentifier as a tombstone. + * This method is executed in a new transaction to ensure that the tombstone creation is isolated + * from the transaction that deleted the entity. + * + * @param event the entity deleted event + */ + @EventListener(classes = {EntityDeletedEvent.class}) + @Transactional + public void handleEntityDeletedEvent(EntityDeletedEvent event) { + AdministrativeMetadata entity = event.getEntity(); + log.debug("Handling EntityDeletedEvent for entity: {}", entity); + + // Mark the PersistentIdentifier as a tombstone + Optional optionalPid = pidService.markAsTombstone(entity); + + if (optionalPid.isPresent()) { + PersistentIdentifier pid = optionalPid.get(); + log.info("Created tombstone for entity with PID: {}", pid.getPid()); + } else { + log.warn("No PersistentIdentifier found for entity, cannot create tombstone: {}", entity); + } + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/PIDGenerationEventListener.java b/src/main/java/edu/kit/datamanager/idoris/pids/PIDGenerationEventListener.java deleted file mode 100644 index db0c7be..0000000 --- a/src/main/java/edu/kit/datamanager/idoris/pids/PIDGenerationEventListener.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) 2025 Karlsruhe Institute of Technology - * - * 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 edu.kit.datamanager.idoris.pids; - -import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; -import edu.kit.datamanager.idoris.core.events.EntityCreatedEvent; -import edu.kit.datamanager.idoris.core.events.EventPublisherService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.event.EventListener; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -/** - * Event listener that generates PIDs for newly created entities. - * This listener subscribes to EntityCreatedEvent and uses the TypedPIDMakerIDGenerator - * to generate PIDs for entities that don't already have one. - */ -@Component -@Slf4j -public class PIDGenerationEventListener { - private final TypedPIDMakerIDGenerator pidGenerator; - private final EventPublisherService eventPublisher; - - /** - * Creates a new PIDGenerationEventListener with the given dependencies. - * - * @param pidGenerator the PID generator to use - * @param eventPublisher the event publisher service - */ - public PIDGenerationEventListener(TypedPIDMakerIDGenerator pidGenerator, EventPublisherService eventPublisher) { - this.pidGenerator = pidGenerator; - this.eventPublisher = eventPublisher; - } - - /** - * Handles EntityCreatedEvent by generating a PID for the entity if it doesn't already have one. - * This method is executed in a new transaction to ensure that the PID generation is isolated - * from the transaction that created the entity. - * - * @param event the entity created event - */ - @EventListener - @Transactional - public void handleEntityCreatedEvent(EntityCreatedEvent event) { - AdministrativeMetadata entity = event.getEntity(); - log.debug("Handling EntityCreatedEvent for entity: {}", entity); - - if (entity.getPid() == null || entity.getPid().isEmpty()) { - log.info("Generating PID for entity: {}", entity); - String pid = pidGenerator.generateId(entity.getClass().getSimpleName(), entity); - entity.setPid(pid); - log.info("Generated PID: {} for entity: {}", pid, entity); - - // Publish a PID generated event - eventPublisher.publishPIDGenerated(entity, pid); - } else { - log.debug("Entity already has a PID: {}", entity.getPid()); - } - } -} diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/TypedPIDMakerIDGenerator.java b/src/main/java/edu/kit/datamanager/idoris/pids/TypedPIDMakerIDGenerator.java index 25875c0..ac94dbe 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/TypedPIDMakerIDGenerator.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/TypedPIDMakerIDGenerator.java @@ -16,14 +16,10 @@ package edu.kit.datamanager.idoris.pids; -import com.google.common.base.Ascii; -import edu.kit.datamanager.idoris.configuration.ApplicationProperties; import edu.kit.datamanager.idoris.configuration.TypedPIDMakerConfig; import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; -import edu.kit.datamanager.idoris.pids.client.TypedPIDMakerClient; -import edu.kit.datamanager.idoris.pids.client.model.PIDRecord; -import edu.kit.datamanager.idoris.pids.client.model.PIDRecordEntry; -import edu.kit.datamanager.idoris.users.entities.ORCiDUser; +import edu.kit.datamanager.idoris.pids.entities.PersistentIdentifier; +import edu.kit.datamanager.idoris.pids.services.PersistentIdentifierService; import jakarta.annotation.Nonnull; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -31,191 +27,58 @@ import org.springframework.data.neo4j.core.schema.IdGenerator; import org.springframework.stereotype.Component; -import java.net.URL; -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; +import java.util.UUID; /** - * ID generator that uses the Typed PID Maker service to generate PIDs. - * It also creates PID records with metadata from the AdministrativeMetadata. + * ID generator that uses the PersistentIdentifierService to generate PIDs. + * This implementation creates separate nodes in Neo4j that point to the entities. */ @Component @Slf4j @ConditionalOnBean(TypedPIDMakerConfig.class) public class TypedPIDMakerIDGenerator implements IdGenerator { - private final TypedPIDMakerClient client; - private final TypedPIDMakerConfig config; - /** - * The base URL for the Typed PID Maker service. - * This is derived from the application properties. - * Without a trailing slash to ensure proper URL construction. - */ - private final String baseUrl; + private final PersistentIdentifierService pidService; /** * Constructor. * - * @param client The TypedPIDMakerClient - * @param config The TypedPIDMakerConfig + * @param pidService The PersistentIdentifierService */ @Autowired - public TypedPIDMakerIDGenerator(ApplicationProperties applicationProperties, TypedPIDMakerClient client, TypedPIDMakerConfig config) { - this.client = client; - this.config = config; - - String tempBaseUrl = applicationProperties.getBaseUrl(); - // Validate the base URL from application properties - if (tempBaseUrl == null || tempBaseUrl.trim().isEmpty()) { - log.error("Base URL for Typed PID Maker service is not configured or is empty."); - throw new IllegalArgumentException("Base URL for Typed PID Maker service must be configured."); - } - // Ensure the base URL does not end with a slash - if (tempBaseUrl.endsWith("/")) { - log.warn("Base URL for Typed PID Maker service should not end with a slash. Removing trailing slash."); - tempBaseUrl = tempBaseUrl.substring(0, tempBaseUrl.length() - 1); - } - this.baseUrl = tempBaseUrl; - log.info("Initialized TypedPIDMakerIDGenerator with base URL: {}", this.baseUrl); + public TypedPIDMakerIDGenerator(PersistentIdentifierService pidService) { + this.pidService = pidService; + log.info("Initialized TypedPIDMakerIDGenerator with PersistentIdentifierService"); } + /** + * Generates a PID for the given entity. + * This method creates a new PersistentIdentifier entity that points to the given entity. + * + * @param primaryLabel The primary label of the entity + * @param entity The entity to generate a PID for + * @return The generated PID + */ @Override @Nonnull public String generateId(String primaryLabel, Object entity) { if (!(entity instanceof AdministrativeMetadata idorisEntity)) { - log.warn("Entity is not a AdministrativeMetadata, falling back to UUID generation"); - return java.util.UUID.randomUUID().toString(); + log.warn("Entity is not an AdministrativeMetadata, falling back to UUID generation"); + return UUID.randomUUID().toString(); } - // If the entity already has a PID, check if we need to update the record - if (idorisEntity.getPid() != null && !idorisEntity.getPid().isEmpty()) { - if (config.isUpdatePIDRecords() && config.isMeaningfulPIDRecords()) { - try { - PIDRecord existingRecord = client.getPIDRecord(idorisEntity.getPid()); - PIDRecord updatedRecord = createPIDRecord(idorisEntity); - client.updatePIDRecord(existingRecord.pid(), updatedRecord); - } catch (Exception e) { - log.error("Failed to update PID record for entity with PID {}: {}", idorisEntity.getPid(), e.getMessage()); - } - } - return idorisEntity.getPid(); - } - - // Create a new PID record try { - log.debug("Creating PID record for entity with PID {}", idorisEntity.getPid()); - PIDRecord record = createPIDRecord(idorisEntity); - PIDRecord createdRecord = client.createPIDRecord(record); - log.info("Created new PID record with PID: {}", createdRecord.pid()); + log.debug("Creating PersistentIdentifier for entity: {}", idorisEntity); - // Update the entity with the new PID - idorisEntity.setPid(createdRecord.pid()); - PIDRecord updatedRecord = createPIDRecord(idorisEntity); - client.updatePIDRecord(createdRecord.pid(), updatedRecord); - log.info("Updated entity with new digitalObjectLocation: {}", updatedRecord); - return createdRecord.pid(); + // Create a new PersistentIdentifier for the entity + PersistentIdentifier pid = pidService.createPersistentIdentifier(idorisEntity); + + log.info("Created PersistentIdentifier with PID: {}", pid.getPid()); + return pid.getPid(); } catch (Exception e) { - log.error("Failed to create PID record: {}", e.getMessage()); + log.error("Failed to create PersistentIdentifier: {}", e.getMessage()); log.warn("Falling back to UUID generation"); - return java.util.UUID.randomUUID().toString(); + return UUID.randomUUID().toString(); } } - - /** - * Creates a PID record with metadata from the AdministrativeMetadata. - * Note: This method only adds metadata if the Helmholtz Kernel Information Profile allows it. - * - * @param entity The AdministrativeMetadata - * @return The PID record - */ - private PIDRecord createPIDRecord(AdministrativeMetadata entity) { - List recordEntries = new ArrayList<>(); - - // Only add metadata if configured to do so - if (config.isMeaningfulPIDRecords()) { - // Helmholtz Kernel Information Profile - recordEntries.add(new PIDRecordEntry("21.T11148/076759916209e5d62bd5", "21.T11148/b9b76f887845e32d29f7")); - -// // Add basic metadata -// if (entity.getName() != null) { -// recordEntries.add(new PIDRecordEntry("name", entity.getName())); -// } -// -// if (entity.getDescription() != null) { -// recordEntries.add(new PIDRecordEntry("description", entity.getDescription())); -// } - - // Add timestamps - Instant createdAt = entity.getCreatedAt(); - if (createdAt != null) { - recordEntries.add(new PIDRecordEntry("21.T11148/aafd5fb4c7222e2d950a", createdAt.toString())); - } - - Instant lastModifiedAt = entity.getLastModifiedAt(); - if (lastModifiedAt != null) { - recordEntries.add(new PIDRecordEntry("21.T11148/397d831aa3a9d18eb52c", lastModifiedAt.toString())); - } - - // Add version information - Long version = entity.getVersion(); - if (version != null) { - recordEntries.add(new PIDRecordEntry("21.T11148/c692273deb2772da307f", version.toString())); - } - -// // Add expected use cases -// if (entity.getExpectedUseCases() != null && !entity.getExpectedUseCases().isEmpty()) { -// recordEntries.add(new PIDRecordEntry("expectedUseCases", String.join(", ", entity.getExpectedUseCases()))); -// } - - // Add contributors - if (entity.getContributors() != null && !entity.getContributors().isEmpty()) { - entity.getContributors().forEach(contributor -> { - if (contributor instanceof ORCiDUser orcidUser) { - URL orcidURL = orcidUser.getOrcid(); - if (orcidURL != null && !orcidURL.toString().isEmpty()) { - recordEntries.add(new PIDRecordEntry("21.T11148/1a73af9e7ae00182733b", orcidURL.toExternalForm())); - log.debug("Added ORCiD URL: {}", orcidURL); - } else { - log.warn("This ORCiDUser is invalid, skipping contributor entry: {}", orcidUser); - } - } else { - log.warn("This contributor does not have a URL, skipping: {}", contributor); - } - }); - } - - // Add references - if (entity.getReferences() != null && !entity.getReferences().isEmpty()) { - entity.getReferences().forEach(reference -> { - String relationPID = reference.relationType(); - String targetPID = reference.targetPID(); - if (relationPID != null && !relationPID.isEmpty() && targetPID != null && !targetPID.isEmpty()) { - recordEntries.add(new PIDRecordEntry(relationPID, targetPID)); - log.debug("Added reference: {} -> {}", relationPID, targetPID); - } else { - log.warn("Invalid reference found, skipping: {}", reference); - } - }); - } - - - String doLocation; - if (entity.getPid() != null && !entity.getPid().isEmpty()) { - doLocation = String.format("%s/pid/%s", baseUrl, entity.getPid()); - } else { - log.warn("Entity PID is null or empty, creating temporary DO location with internal id."); - String classname = Ascii.toLowerCase(entity.getClass().getSimpleName()); - doLocation = String.format("%s/api/%s/%s", baseUrl, classname, entity.getInternalId()); - } - log.debug("Using DO location: {}", doLocation); - recordEntries.add(new PIDRecordEntry("digitalObjectLocation", doLocation)); - } - - // Create the PID record - PIDRecord pidRecord = new PIDRecord("", recordEntries); - log.info("Created PIDRecord: {}", pidRecord); - return pidRecord; - } - } diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/entities/PersistentIdentifier.java b/src/main/java/edu/kit/datamanager/idoris/pids/entities/PersistentIdentifier.java new file mode 100644 index 0000000..d9aba19 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/pids/entities/PersistentIdentifier.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.pids.entities; + +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.annotation.Version; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.Relationship; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + +/** + * Entity representing a Persistent Identifier (PID) in the system. + * This is stored as a separate node in Neo4j and points to the entity it identifies. + */ +@Getter +@Setter +@EqualsAndHashCode +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Node("PersistentIdentifier") +public class PersistentIdentifier { + + /** + * The PID value, which is also the primary key of this entity. + */ + @Id + private String pid; + + /** + * The type of entity this PID identifies. + */ + private String entityType; + + /** + * The internal ID of the entity this PID identifies. + */ + private String entityInternalId; + + /** + * Flag indicating whether this PID record is a tombstone (entity has been deleted). + */ + private boolean tombstone; + + /** + * Timestamp when the entity was deleted (only set if tombstone is true). + */ + private Instant deletedAt; + + /** + * Version of this PID record. + */ + @Version + private Long version; + + /** + * Timestamp when this PID record was created. + */ + @CreatedDate + private Instant createdAt; + + /** + * Timestamp when this PID record was last modified. + */ + @LastModifiedDate + private Instant lastModifiedAt; + + /** + * Relationship to the entity this PID identifies. + * This is null if the entity has been deleted (tombstone is true). + */ + @Relationship(value = "IDENTIFIES", direction = Relationship.Direction.OUTGOING) + private AdministrativeMetadata entity; + + /** + * Additional metadata stored in the PID record. + * This is a map of key-value pairs that can be used to store any additional information. + */ + private Map metadata = new HashMap<>(); + + /** + * Adds a metadata entry to this PID record. + * + * @param key The key of the metadata entry + * @param value The value of the metadata entry + * @return This PID record for method chaining + */ + public PersistentIdentifier addMetadata(String key, String value) { + metadata.put(key, value); + return this; + } + + /** + * Removes a metadata entry from this PID record. + * + * @param key The key of the metadata entry to remove + * @return This PID record for method chaining + */ + public PersistentIdentifier removeMetadata(String key) { + metadata.remove(key); + return this; + } + + /** + * Clears all metadata entries from this PID record. + * + * @return This PID record for method chaining + */ + public PersistentIdentifier clearMetadata() { + metadata.clear(); + return this; + } + + /** + * Gets the value of a metadata entry. + * + * @param key The key of the metadata entry + * @return The value of the metadata entry, or null if the key does not exist + */ + public String getMetadataValue(String key) { + return metadata.get(key); + } + + /** + * Marks this PID record as a tombstone, indicating that the entity it identifies has been deleted. + * + * @param deletedAt The timestamp when the entity was deleted + * @return This PID record for method chaining + */ + public PersistentIdentifier markAsTombstone(Instant deletedAt) { + this.tombstone = true; + this.deletedAt = deletedAt; + this.entity = null; // Remove the relationship to the entity + return this; + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/package-info.java b/src/main/java/edu/kit/datamanager/idoris/pids/package-info.java index 2e2d5dc..7255166 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/package-info.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/package-info.java @@ -24,6 +24,6 @@ */ @org.springframework.modulith.ApplicationModule( displayName = "IDORIS PID Management", - allowedDependencies = {"core", "domain"} + allowedDependencies = {"core"} ) package edu.kit.datamanager.idoris.pids; \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/repositories/PersistentIdentifierRepository.java b/src/main/java/edu/kit/datamanager/idoris/pids/repositories/PersistentIdentifierRepository.java new file mode 100644 index 0000000..9f9702b --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/pids/repositories/PersistentIdentifierRepository.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.pids.repositories; + +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; +import edu.kit.datamanager.idoris.pids.entities.PersistentIdentifier; +import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.data.neo4j.repository.query.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +/** + * Repository interface for PersistentIdentifier entities. + * This interface provides methods for CRUD operations on PersistentIdentifier entities. + */ +@Repository +public interface PersistentIdentifierRepository extends Neo4jRepository { + + /** + * Finds a PersistentIdentifier by the entity it identifies. + * + * @param entity The entity to find the PID for + * @return An Optional containing the PersistentIdentifier if found, or empty if not found + */ + Optional findByEntity(AdministrativeMetadata entity); + + /** + * Finds a PersistentIdentifier by the internal ID of the entity it identifies. + * + * @param entityInternalId The internal ID of the entity to find the PID for + * @return An Optional containing the PersistentIdentifier if found, or empty if not found + */ + Optional findByEntityInternalId(String entityInternalId); + + /** + * Finds all PersistentIdentifiers for entities of the given type. + * + * @param entityType The type of entity to find PIDs for + * @return A list of PersistentIdentifiers for entities of the given type + */ + List findByEntityType(String entityType); + + /** + * Finds all PersistentIdentifiers that are tombstones (entity has been deleted). + * + * @return A list of PersistentIdentifiers that are tombstones + */ + List findByTombstoneTrue(); + + /** + * Finds all PersistentIdentifiers that are not tombstones (entity has not been deleted). + * + * @return A list of PersistentIdentifiers that are not tombstones + */ + List findByTombstoneFalse(); + + /** + * Finds all PersistentIdentifiers that have a metadata entry with the given key and value. + * + * @param key The key of the metadata entry + * @param value The value of the metadata entry + * @return A list of PersistentIdentifiers that have a metadata entry with the given key and value + */ + @Query("MATCH (p:PersistentIdentifier) WHERE p.metadata[$key] = $value RETURN p") + List findByMetadata(@Param("key") String key, @Param("value") String value); +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/services/PersistentIdentifierService.java b/src/main/java/edu/kit/datamanager/idoris/pids/services/PersistentIdentifierService.java new file mode 100644 index 0000000..7c7ca9e --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/pids/services/PersistentIdentifierService.java @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.pids.services; + +import edu.kit.datamanager.idoris.configuration.TypedPIDMakerConfig; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; +import edu.kit.datamanager.idoris.pids.client.TypedPIDMakerClient; +import edu.kit.datamanager.idoris.pids.client.model.PIDRecord; +import edu.kit.datamanager.idoris.pids.entities.PersistentIdentifier; +import edu.kit.datamanager.idoris.pids.repositories.PersistentIdentifierRepository; +import edu.kit.datamanager.idoris.pids.utils.PIDRecordMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +/** + * Service class for PersistentIdentifier entities. + * This class provides methods for creating, updating, and retrieving PersistentIdentifier entities. + */ +@Service +@Slf4j +public class PersistentIdentifierService { + + private final PersistentIdentifierRepository repository; + private final TypedPIDMakerClient client; + private final TypedPIDMakerConfig config; + private final PIDRecordMapper mapper; + + /** + * Creates a new PersistentIdentifierService with the given dependencies. + * + * @param repository The repository for PersistentIdentifier entities + * @param client The client for the Typed PID Maker service + * @param config The configuration for the Typed PID Maker service + * @param mapper The mapper for converting between PersistentIdentifier and PIDRecord + */ + @Autowired + public PersistentIdentifierService(PersistentIdentifierRepository repository, + TypedPIDMakerClient client, + TypedPIDMakerConfig config, + PIDRecordMapper mapper) { + this.repository = repository; + this.client = client; + this.config = config; + this.mapper = mapper; + } + + /** + * Creates a new PersistentIdentifier for the given entity. + * This method creates a new PID record in the Typed PID Maker service and stores a corresponding + * PersistentIdentifier entity in the local database. + * + * @param entity The entity to create a PID for + * @return The created PersistentIdentifier + */ + @Transactional + public PersistentIdentifier createPersistentIdentifier(AdministrativeMetadata entity) { + log.debug("Creating PersistentIdentifier for entity: {}", entity); + + // Check if a PID already exists for this entity + Optional existingPid = repository.findByEntityInternalId(entity.getInternalId()); + if (existingPid.isPresent()) { + log.debug("PersistentIdentifier already exists for entity: {}", entity); + return existingPid.get(); + } + + // Create a new PID record in the Typed PID Maker service + PIDRecord record = mapper.createEmptyPIDRecord(); + PIDRecord createdRecord = client.createPIDRecord(record); + + // Create a new PersistentIdentifier entity + PersistentIdentifier pid = PersistentIdentifier.builder() + .pid(createdRecord.pid()) + .entityType(entity.getClass().getSimpleName()) + .entityInternalId(entity.getInternalId()) + .entity(entity) + .tombstone(false) + .build(); + + // Save the PersistentIdentifier entity + PersistentIdentifier savedPid = repository.save(pid); + + // Update the PID record with metadata if configured to do so + if (config.isMeaningfulPIDRecords()) { + updatePIDRecord(savedPid); + } + + log.info("Created PersistentIdentifier: {}", savedPid); + return savedPid; + } + + /** + * Updates the PID record for the given PersistentIdentifier. + * This method updates the PID record in the Typed PID Maker service with the latest metadata from the entity. + * + * @param pid The PersistentIdentifier to update the PID record for + * @return The updated PersistentIdentifier + */ + @Transactional + public PersistentIdentifier updatePIDRecord(PersistentIdentifier pid) { + log.debug("Updating PID record for PersistentIdentifier: {}", pid); + + // Skip update if not configured to do so + if (!config.isUpdatePIDRecords()) { + log.debug("Skipping PID record update because updatePIDRecords is false"); + return pid; + } + + // Create a PID record with metadata from the entity + PIDRecord record = mapper.toPIDRecord(pid); + + // Update the PID record in the Typed PID Maker service + client.updatePIDRecord(pid.getPid(), record); + + log.info("Updated PID record for PersistentIdentifier: {}", pid); + return pid; + } + + /** + * Marks the PersistentIdentifier for the given entity as a tombstone. + * This method updates the PID record in the Typed PID Maker service to indicate that the entity has been deleted. + * + * @param entity The entity that has been deleted + * @return The updated PersistentIdentifier, or empty if no PID exists for the entity + */ + @Transactional + public Optional markAsTombstone(AdministrativeMetadata entity) { + log.debug("Marking PersistentIdentifier as tombstone for entity: {}", entity); + + // Find the PID for the entity + Optional optionalPid = repository.findByEntityInternalId(entity.getInternalId()); + if (optionalPid.isEmpty()) { + log.warn("No PersistentIdentifier found for entity: {}", entity); + return Optional.empty(); + } + + PersistentIdentifier pid = optionalPid.get(); + + // Mark the PID as a tombstone + pid.markAsTombstone(Instant.now()); + + // Save the updated PID + PersistentIdentifier savedPid = repository.save(pid); + + // Update the PID record in the Typed PID Maker service + updatePIDRecord(savedPid); + + log.info("Marked PersistentIdentifier as tombstone: {}", savedPid); + return Optional.of(savedPid); + } + + /** + * Gets the PersistentIdentifier for the given entity. + * + * @param entity The entity to get the PID for + * @return An Optional containing the PersistentIdentifier if found, or empty if not found + */ + public Optional getPersistentIdentifier(AdministrativeMetadata entity) { + log.debug("Getting PersistentIdentifier for entity: {}", entity); + return repository.findByEntityInternalId(entity.getInternalId()); + } + + /** + * Gets the PersistentIdentifier with the given PID. + * + * @param pid The PID to get the PersistentIdentifier for + * @return An Optional containing the PersistentIdentifier if found, or empty if not found + */ + public Optional getPersistentIdentifier(String pid) { + log.debug("Getting PersistentIdentifier with PID: {}", pid); + return repository.findById(pid); + } + + /** + * Gets all PersistentIdentifiers. + * + * @return A list of all PersistentIdentifiers + */ + public List getAllPersistentIdentifiers() { + log.debug("Getting all PersistentIdentifiers"); + return repository.findAll(); + } + + /** + * Gets all PersistentIdentifiers for entities of the given type. + * + * @param entityType The type of entity to get PIDs for + * @return A list of PersistentIdentifiers for entities of the given type + */ + public List getPersistentIdentifiersByEntityType(String entityType) { + log.debug("Getting PersistentIdentifiers for entity type: {}", entityType); + return repository.findByEntityType(entityType); + } + + /** + * Gets all PersistentIdentifiers that are tombstones (entity has been deleted). + * + * @return A list of PersistentIdentifiers that are tombstones + */ + public List getTombstones() { + log.debug("Getting tombstone PersistentIdentifiers"); + return repository.findByTombstoneTrue(); + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/utils/PIDRecordMapper.java b/src/main/java/edu/kit/datamanager/idoris/pids/utils/PIDRecordMapper.java new file mode 100644 index 0000000..c9576ef --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/pids/utils/PIDRecordMapper.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.pids.utils; + +import edu.kit.datamanager.idoris.configuration.ApplicationProperties; +import edu.kit.datamanager.idoris.configuration.TypedPIDMakerConfig; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; +import edu.kit.datamanager.idoris.pids.client.model.PIDRecord; +import edu.kit.datamanager.idoris.pids.client.model.PIDRecordEntry; +import edu.kit.datamanager.idoris.pids.entities.PersistentIdentifier; +import edu.kit.datamanager.idoris.users.entities.ORCiDUser; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.net.URL; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Utility class for mapping between PersistentIdentifier entities and PIDRecord objects. + * This class extracts the mapping logic to reduce redundancies. + */ +@Component +@Slf4j +public class PIDRecordMapper { + + private final ApplicationProperties applicationProperties; + private final TypedPIDMakerConfig config; + + /** + * Creates a new PIDRecordMapper with the given dependencies. + * + * @param applicationProperties The application properties + * @param config The configuration for the Typed PID Maker service + */ + @Autowired + public PIDRecordMapper(ApplicationProperties applicationProperties, TypedPIDMakerConfig config) { + this.applicationProperties = applicationProperties; + this.config = config; + } + + /** + * Creates an empty PIDRecord with no entries. + * This is used when creating a new PID record in the Typed PID Maker service. + * + * @return An empty PIDRecord + */ + public PIDRecord createEmptyPIDRecord() { + return new PIDRecord("", new ArrayList<>()); + } + + /** + * Converts a PersistentIdentifier to a PIDRecord. + * This method creates a PIDRecord with metadata from the PersistentIdentifier and its associated entity. + * + * @param pid The PersistentIdentifier to convert + * @return The converted PIDRecord + */ + public PIDRecord toPIDRecord(PersistentIdentifier pid) { + List recordEntries = new ArrayList<>(); + + // Always add a pointer to the entity + String baseUrl = getBaseUrl(); + String doLocation; + + if (pid.isTombstone()) { + // For tombstones, use a special URL that indicates the entity has been deleted + doLocation = String.format("%s/tombstone/%s", baseUrl, pid.getPid()); + recordEntries.add(new PIDRecordEntry("tombstone", "true")); + recordEntries.add(new PIDRecordEntry("deletedAt", pid.getDeletedAt().toString())); + } else { + // For active entities, use a URL that points to the entity + doLocation = String.format("%s/pid/%s", baseUrl, pid.getPid()); + } + + log.debug("Using DO location: {}", doLocation); + recordEntries.add(new PIDRecordEntry("digitalObjectLocation", doLocation)); + + // Add entity type information + recordEntries.add(new PIDRecordEntry("entityType", pid.getEntityType())); + + // Add custom metadata from the PersistentIdentifier + for (Map.Entry entry : pid.getMetadata().entrySet()) { + recordEntries.add(new PIDRecordEntry(entry.getKey(), entry.getValue())); + } + + // Only add additional metadata if configured to do so and the entity is not null (not a tombstone) + if (config.isMeaningfulPIDRecords() && pid.getEntity() != null) { + addAdministrativeMetadata(recordEntries, pid.getEntity()); + } + + // Create the PID record + PIDRecord pidRecord = new PIDRecord(pid.getPid(), recordEntries); + log.debug("Created PIDRecord: {}", pidRecord); + return pidRecord; + } + + /** + * Adds administrative metadata from an AdministrativeMetadata entity to a list of PIDRecordEntry objects. + * This method is used to add metadata to a PID record based on the Helmholtz Kernel Information Profile. + * + * @param recordEntries The list of PIDRecordEntry objects to add metadata to + * @param entity The AdministrativeMetadata entity to get metadata from + */ + private void addAdministrativeMetadata(List recordEntries, AdministrativeMetadata entity) { + // Helmholtz Kernel Information Profile + recordEntries.add(new PIDRecordEntry("21.T11148/076759916209e5d62bd5", "21.T11148/b9b76f887845e32d29f7")); + + // Add basic metadata + if (entity.getName() != null) { + recordEntries.add(new PIDRecordEntry("21.T11148/6ae999552a0d2dca14d6", entity.getName())); + } + + // Add timestamps + Instant createdAt = entity.getCreatedAt(); + if (createdAt != null) { + recordEntries.add(new PIDRecordEntry("21.T11148/aafd5fb4c7222e2d950a", createdAt.toString())); + } + + Instant lastModifiedAt = entity.getLastModifiedAt(); + if (lastModifiedAt != null) { + recordEntries.add(new PIDRecordEntry("21.T11148/397d831aa3a9d18eb52c", lastModifiedAt.toString())); + } + + // Add version information + Long version = entity.getVersion(); + if (version != null) { + recordEntries.add(new PIDRecordEntry("21.T11148/c692273deb2772da307f", version.toString())); + } + + // Add contributors + if (entity.getContributors() != null && !entity.getContributors().isEmpty()) { + entity.getContributors().forEach(contributor -> { + if (contributor instanceof ORCiDUser orcidUser) { + URL orcidURL = orcidUser.getOrcid(); + if (orcidURL != null && !orcidURL.toString().isEmpty()) { + recordEntries.add(new PIDRecordEntry("21.T11148/1a73af9e7ae00182733b", orcidURL.toExternalForm())); + log.debug("Added ORCiD URL: {}", orcidURL); + } else { + log.warn("This ORCiDUser is invalid, skipping contributor entry: {}", orcidUser); + } + } else { + log.info("This contributor does not have a URL, skipping: {}", contributor); + } + }); + } + + // Add references + if (entity.getReferences() != null && !entity.getReferences().isEmpty()) { + entity.getReferences().forEach(reference -> { + String relationPID = reference.relationType(); + String targetPID = reference.targetPID(); + if (relationPID != null && !relationPID.isEmpty() && targetPID != null && !targetPID.isEmpty()) { + recordEntries.add(new PIDRecordEntry(relationPID, targetPID)); + log.debug("Added reference: {} -> {}", relationPID, targetPID); + } else { + log.warn("Invalid reference found, skipping: {}", reference); + } + }); + } + } + + /** + * Gets the base URL from the application properties. + * This method ensures that the base URL does not end with a slash. + * + * @return The base URL + */ + private String getBaseUrl() { + String baseUrl = applicationProperties.getBaseUrl(); + if (baseUrl == null || baseUrl.trim().isEmpty()) { + log.error("Base URL is not configured or is empty."); + throw new IllegalArgumentException("Base URL must be configured."); + } + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.substring(0, baseUrl.length() - 1); + } + return baseUrl; + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/web/api/IPidApi.java b/src/main/java/edu/kit/datamanager/idoris/pids/web/api/IPidApi.java new file mode 100644 index 0000000..678f02e --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/pids/web/api/IPidApi.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.pids.web.api; + +import edu.kit.datamanager.idoris.pids.entities.PersistentIdentifier; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.EntityModel; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +/** + * API interface for PID-related operations. + * This interface defines the REST API for accessing Persistent Identifiers (PIDs). + */ +@Tag(name = "Persistent Identifier", description = "API for accessing Persistent Identifiers (PIDs)") +public interface IPidApi { + + /** + * Lists all PIDs exposed by IDORIS. + * + * @return A collection of all PersistentIdentifiers + */ + @GetMapping + @Operation( + summary = "Get all Persistent Identifiers", + description = "Returns a collection of all Persistent Identifiers exposed by IDORIS", + responses = { + @ApiResponse(responseCode = "200", description = "Persistent Identifiers found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = PersistentIdentifier.class))) + } + ) + ResponseEntity>> getAllPersistentIdentifiers(); + + /** + * Redirects to the appropriate entity based on the PID value. + * If the PID is a tombstone, redirects to the tombstone page. + * Otherwise, redirects to the entity's page. + * + * @param pidValue The PID value to redirect to + * @return A ResponseEntity with a redirect status or not found if no entity is found + */ + @GetMapping("/{pidValue}") + @Operation( + summary = "Redirect to entity by PID", + description = "Redirects to the appropriate entity based on the PID value. If the PID is a tombstone, redirects to the tombstone page.", + responses = { + @ApiResponse(responseCode = "302", description = "Found - Redirect to entity or tombstone"), + @ApiResponse(responseCode = "404", description = "PID not found") + } + ) + ResponseEntity redirectToEntity( + @Parameter(description = "PID value to redirect to", required = true) + @PathVariable("pidValue") String pidValue); + + /** + * Handles requests to the tombstone page. + * Returns a 410 Gone status with information about the deleted entity. + * + * @param pidValue The PID value of the tombstone + * @return A ResponseEntity with a 410 Gone status and information about the deleted entity + */ + @GetMapping("/tombstone/{pidValue}") + @Operation( + summary = "Get tombstone information", + description = "Returns tombstone information for a deleted entity", + responses = { + @ApiResponse(responseCode = "410", description = "Gone - Entity has been deleted", + content = @Content(mediaType = "text/plain")), + @ApiResponse(responseCode = "302", description = "Found - Redirect to entity (if not a tombstone)"), + @ApiResponse(responseCode = "404", description = "PID not found") + } + ) + ResponseEntity handleTombstone( + @Parameter(description = "PID value of the tombstone", required = true) + @PathVariable("pidValue") String pidValue); +} diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/web/hateoas/PersistentIdentifierModelAssembler.java b/src/main/java/edu/kit/datamanager/idoris/pids/web/hateoas/PersistentIdentifierModelAssembler.java new file mode 100644 index 0000000..354d093 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/pids/web/hateoas/PersistentIdentifierModelAssembler.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.pids.web.hateoas; + +import edu.kit.datamanager.idoris.pids.entities.PersistentIdentifier; +import edu.kit.datamanager.idoris.pids.web.v1.PidController; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.server.RepresentationModelAssembler; +import org.springframework.hateoas.server.RepresentationModelProcessor; +import org.springframework.stereotype.Component; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +/** + * A model assembler for PersistentIdentifier entities. + * This class converts PersistentIdentifier entities to EntityModel + * with HATEOAS links. + *

+ * This class combines the functionality of both a RepresentationModelAssembler and a + * RepresentationModelProcessor, handling all HATEOAS concerns for PersistentIdentifier entities + * in one place, according to Domain-Driven Design principles. + */ +@Component +public class PersistentIdentifierModelAssembler implements + RepresentationModelAssembler>, + RepresentationModelProcessor> { + + @Override + public EntityModel toModel(PersistentIdentifier pid) { + EntityModel pidModel = EntityModel.of(pid); + + // Add self link + pidModel.add(linkTo(methodOn(PidController.class).getAllPersistentIdentifiers()).withRel("persistentIdentifiers")); + + return pidModel; + } + + @Override + public EntityModel process(EntityModel model) { + PersistentIdentifier pid = model.getContent(); + if (pid == null) { + return model; + } + + String pidValue = pid.getPid(); + + // Add link to resolve the entity + model.add(linkTo(methodOn(PidController.class).redirectToEntity(pidValue)).withRel("resolve")); + + // Add link to tombstone if it is a tombstone + if (pid.isTombstone()) { + model.add(linkTo(methodOn(PidController.class).handleTombstone(pidValue)).withRel("tombstone")); + } + + return model; + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/web/v1/PidController.java b/src/main/java/edu/kit/datamanager/idoris/pids/web/v1/PidController.java new file mode 100644 index 0000000..7a768ff --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/pids/web/v1/PidController.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.pids.web.v1; + +import edu.kit.datamanager.idoris.pids.entities.PersistentIdentifier; +import edu.kit.datamanager.idoris.pids.services.PersistentIdentifierService; +import edu.kit.datamanager.idoris.pids.web.api.IPidApi; +import edu.kit.datamanager.idoris.pids.web.hateoas.PersistentIdentifierModelAssembler; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.EntityModel; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.net.URI; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +/** + * Controller for PID-related operations. + * This controller handles: + * - /pid - Lists all PIDs exposed by IDORIS + * - /pid/{pidValue} - Redirects to the entity the PID refers to + * - /pid/tombstone/{pidValue} - Handles tombstone pages for deleted entities + */ +@RestController +@RequestMapping("/v1/pid") +@Slf4j +@Tag(name = "Persistent Identifier", description = "API for accessing Persistent Identifiers (PIDs)") +public class PidController implements IPidApi { + + private final PersistentIdentifierService pidService; + private final PersistentIdentifierModelAssembler pidModelAssembler; + + /** + * Creates a new PidController with the given dependencies. + * + * @param pidService The PersistentIdentifierService + * @param pidModelAssembler The PersistentIdentifierModelAssembler + */ + @Autowired + public PidController(PersistentIdentifierService pidService, PersistentIdentifierModelAssembler pidModelAssembler) { + this.pidService = pidService; + this.pidModelAssembler = pidModelAssembler; + } + + /** + * Lists all PIDs exposed by IDORIS. + * + * @return A collection of all PersistentIdentifiers + */ + @Override + @GetMapping + public ResponseEntity>> getAllPersistentIdentifiers() { + log.debug("Getting all PersistentIdentifiers"); + List> pids = pidService.getAllPersistentIdentifiers().stream() + .map(pidModelAssembler::toModel) + .collect(Collectors.toList()); + + CollectionModel> collectionModel = CollectionModel.of( + pids, + linkTo(methodOn(PidController.class).getAllPersistentIdentifiers()).withSelfRel() + ); + + return ResponseEntity.ok(collectionModel); + } + + /** + * Redirects to the appropriate entity based on the PID value. + * If the PID is a tombstone, redirects to the tombstone page. + * Otherwise, redirects to the entity's page. + * + * @param pidValue The PID value to redirect to + * @return A ResponseEntity with a redirect status or not found if no entity is found + */ + @Override + @GetMapping("/{pidValue}") + public ResponseEntity redirectToEntity(@PathVariable("pidValue") String pidValue) { + log.debug("Redirecting PID: {}", pidValue); + + // Get the PersistentIdentifier for the given PID + Optional optionalPid = pidService.getPersistentIdentifier(pidValue); + if (optionalPid.isEmpty()) { + log.warn("No PersistentIdentifier found for PID: {}", pidValue); + return ResponseEntity.notFound().build(); + } + + PersistentIdentifier pid = optionalPid.get(); + + // If the PID is a tombstone, redirect to the tombstone page + if (pid.isTombstone()) { + log.debug("PID is a tombstone, redirecting to tombstone page: {}", pidValue); + return ResponseEntity.status(HttpStatus.FOUND) + .location(URI.create("/tombstone/" + pidValue)) + .build(); + } + + // If the PID has an entity, redirect to the entity's page + if (pid.getEntity() != null) { + String entityType = pid.getEntityType().toLowerCase() + "s"; + String entityId = pid.getEntityInternalId(); + log.debug("Redirecting to entity: {}/{}", entityType, entityId); + return ResponseEntity.status(HttpStatus.FOUND) + .location(URI.create("/" + entityType + "/" + entityId)) + .build(); + } + + // If the PID has no entity and is not a tombstone, return not found + log.warn("PID has no entity and is not a tombstone: {}", pidValue); + return ResponseEntity.notFound().build(); + } + + /** + * Handles requests to the tombstone page. + * Returns a 410 Gone status with information about the deleted entity. + * + * @param pidValue The PID value of the tombstone + * @return A ResponseEntity with a 410 Gone status and information about the deleted entity + */ + @Override + @GetMapping("/tombstone/{pidValue}") + public ResponseEntity handleTombstone(@PathVariable("pidValue") String pidValue) { + log.debug("Handling tombstone request for PID: {}", pidValue); + + // Get the PersistentIdentifier for the given PID + Optional optionalPid = pidService.getPersistentIdentifier(pidValue); + if (optionalPid.isEmpty()) { + log.warn("No PersistentIdentifier found for PID: {}", pidValue); + return ResponseEntity.notFound().build(); + } + + PersistentIdentifier pid = optionalPid.get(); + + // If the PID is not a tombstone, redirect to the entity's page + if (!pid.isTombstone()) { + log.debug("PID is not a tombstone, redirecting to entity page: {}", pidValue); + return ResponseEntity.status(HttpStatus.FOUND) + .location(URI.create("/pid/" + pidValue)) + .build(); + } + + // Return a 410 Gone status with information about the deleted entity + String message = String.format("The entity with PID %s has been deleted at %s. Entity type: %s", + pidValue, pid.getDeletedAt(), pid.getEntityType()); + log.debug("Returning tombstone message: {}", message); + return ResponseEntity.status(HttpStatus.GONE) + .body(message); + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/web/v1/PidRedirectController.java b/src/main/java/edu/kit/datamanager/idoris/pids/web/v1/PidRedirectController.java deleted file mode 100644 index 3a62d2b..0000000 --- a/src/main/java/edu/kit/datamanager/idoris/pids/web/v1/PidRedirectController.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright (c) 2025 Karlsruhe Institute of Technology - * - * 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 edu.kit.datamanager.idoris.pids.web.v1; - -import com.google.common.base.Ascii; -import edu.kit.datamanager.idoris.attributes.dao.IAttributeDao; -import edu.kit.datamanager.idoris.core.domain.dao.IGenericRepo; -import edu.kit.datamanager.idoris.datatypes.dao.IAtomicDataTypeDao; -import edu.kit.datamanager.idoris.datatypes.dao.ITypeProfileDao; -import edu.kit.datamanager.idoris.operations.dao.IOperationDao; -import edu.kit.datamanager.idoris.technologyinterfaces.dao.ITechnologyInterfaceDao; -import lombok.extern.java.Log; -import org.neo4j.driver.Value; -import org.springframework.data.neo4j.core.Neo4jClient; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; - -import java.net.URI; -import java.util.*; - -@Controller -@RequestMapping("/v1/pid") -@Log -public class PidRedirectController { - private static final String FIND_ENTITY_BY_PID_QUERY = "MATCH (n {pid: $pid}) RETURN n.pid AS pid, labels(n) AS nodeLabels LIMIT 1"; - private final Neo4jClient neo4jClient; - private final Map> labelToDaoMap; - - public PidRedirectController(Neo4jClient neo4jClient, - IAtomicDataTypeDao atomicDataTypeDao, - IAttributeDao attributeDao, - IOperationDao operationDao, - ITechnologyInterfaceDao technologyInterfaceDao, - ITypeProfileDao typeProfileDao) { - this.neo4jClient = neo4jClient; - Map> mapBuilder = new HashMap<>(); - mapBuilder.put("AtomicDataType", atomicDataTypeDao); - mapBuilder.put("Attribute", attributeDao); - mapBuilder.put("Operation", operationDao); - mapBuilder.put("TechnologyInterface", technologyInterfaceDao); - mapBuilder.put("TypeProfile", typeProfileDao); - this.labelToDaoMap = Collections.unmodifiableMap(mapBuilder); - } - - /** - * Redirects to the appropriate entity based on the PID value. - * The method fetches the entity data from Neo4j, extracts the PID and labels, - * and determines the redirect path based on the labels. - * - * @param pidValue The PID value to redirect to. - * @return A ResponseEntity with a redirect status or not found if no entity is found. - */ - @GetMapping("/{pidValue}") - public ResponseEntity redirectToEntity(@PathVariable("pidValue") String pidValue) { - Optional> entityDataOptional = fetchNeo4jData(pidValue); - if (entityDataOptional.isEmpty()) { - log.warning("No entity data found in Neo4j for PID: " + pidValue); - return ResponseEntity.notFound().build(); - } - Map entityData = entityDataOptional.get(); - String entityPid = entityData.containsKey("pid") ? (String) entityData.get("pid") : null; - if (entityPid == null || entityPid.isEmpty()) { - log.warning("Extracted PID is null or empty for input: " + pidValue); - return ResponseEntity.notFound().build(); - } - List nodeLabels = extractAndFilterLabels(entityData); - if (nodeLabels.isEmpty()) { - log.warning("No suitable labels found for PID: " + pidValue + " after filtering."); - return ResponseEntity.notFound().build(); - } - Optional redirectPathBaseOptional = determineRedirectPathBase(entityPid, nodeLabels); - if (redirectPathBaseOptional.isPresent()) { - String redirectUrl = String.format("/%s/%s", redirectPathBaseOptional.get(), entityPid); - return ResponseEntity.status(HttpStatus.FOUND) - .location(URI.create(redirectUrl)) - .build(); - } else { - log.warning("No endpoint could be determined for PID: " + pidValue + " with labels: " + nodeLabels); - return ResponseEntity.notFound().build(); - } - } - - /** - * Fetches entity data from Neo4j based on the provided PID value. - * - * @param pidValue The PID value to search for. - * @return An Optional containing the entity data if found, otherwise empty. - */ - private Optional> fetchNeo4jData(String pidValue) { - return neo4jClient.query(FIND_ENTITY_BY_PID_QUERY) - .bind(pidValue).to("pid") - .fetch().one(); - } - - /** - * Extracts and filters labels from the entity data. - * Filters out null, empty, "AdministrativeMetadata", and labels starting with "_". - * - * @param entityData The entity data map containing labels. - * @return A list of filtered labels. - */ - private List extractAndFilterLabels(Map entityData) { - Object rawLabels = entityData.get("nodeLabels"); - if (!(rawLabels instanceof Value nodeLabelsValue)) { - log.fine("nodeLabels field is missing or not of type Value for entity data: " + entityData); - return Collections.emptyList(); - } - return nodeLabelsValue.asList(Value::asString) - .stream() - .filter(label -> label != null && !label.isEmpty()) - .map(String::trim) - .filter(label -> !label.equals("AdministrativeMetadata") && !label.startsWith("_")) - .toList(); - } - - /** - * Determines the redirect path base based on the entity PID and its labels. - * It looks up the DAO for each label, fetches the entity by PID, and constructs the path base. - * - * @param entityPid The PID of the entity. - * @param nodeLabels The list of labels associated with the entity. - * @return An Optional containing the redirect path base if found, otherwise empty. - */ - private Optional determineRedirectPathBase(String entityPid, List nodeLabels) { - return nodeLabels.stream() - .map(labelToDaoMap::get) // Get DAO for label - .filter(Objects::nonNull) // Filter out labels not in map (or DAOs that are null) - .map(dao -> { - try { - // Assuming IGenericRepo has findByPID method. - // The exact return type of findByPID (e.g., entity or Optional) - // is handled by the filter(Objects::nonNull) that follows. - return dao.findByPid(entityPid); - } catch (Exception e) { - log.warning("Error calling findByPID on DAO for PID " + entityPid + ": " + e.getMessage()); - return null; // Treat as not found if there's an error - } - }) - .filter(Objects::nonNull) // Filter out if entity not found by this DAO or if an error occurred - .map(entity -> Ascii.toLowerCase(entity.getClass().getSimpleName()) + "s") // Determine path base - .findFirst(); // Take the first one that matches - } -} \ No newline at end of file diff --git a/src/test/java/edu/kit/datamanager/idoris/pids/PIDTombstoneEventListenerTest.java b/src/test/java/edu/kit/datamanager/idoris/pids/PIDTombstoneEventListenerTest.java new file mode 100644 index 0000000..1f37d51 --- /dev/null +++ b/src/test/java/edu/kit/datamanager/idoris/pids/PIDTombstoneEventListenerTest.java @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.pids; + +import edu.kit.datamanager.idoris.configuration.TypedPIDMakerConfig; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; +import edu.kit.datamanager.idoris.core.events.EntityDeletedEvent; +import edu.kit.datamanager.idoris.pids.client.TypedPIDMakerClient; +import edu.kit.datamanager.idoris.pids.client.model.PIDRecord; +import edu.kit.datamanager.idoris.pids.client.model.PIDRecordEntry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Instant; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class PIDTombstoneEventListenerTest { + + @Mock + private TypedPIDMakerClient client; + + @Mock + private TypedPIDMakerConfig config; + + private PIDTombstoneEventListener listener; + + @BeforeEach + void setUp() { + listener = new PIDTombstoneEventListener(client, config); + } + + @Test + void handleEntityDeletedEvent_withValidPid_createsTombstone() { + // Arrange + String pid = "test-pid"; + TestEntity entity = new TestEntity(); + entity.setPid(pid); + entity.setName("Test Entity"); + entity.setDescription("Test Description"); + entity.setCreatedAt(Instant.now()); + entity.setLastModifiedAt(Instant.now()); + entity.setVersion(1L); + + EntityDeletedEvent event = new EntityDeletedEvent<>(entity); + + PIDRecord existingRecord = new PIDRecord(pid, List.of( + new PIDRecordEntry("digitalObjectLocation", "http://example.com/pid/" + pid), + new PIDRecordEntry("name", "Test Entity") + )); + + when(client.getPIDRecord(pid)).thenReturn(existingRecord); + when(config.isMeaningfulPIDRecords()).thenReturn(true); + + // Act + listener.handleEntityDeletedEvent(event); + + // Assert + ArgumentCaptor recordCaptor = ArgumentCaptor.forClass(PIDRecord.class); + verify(client).updatePIDRecord(eq(pid), recordCaptor.capture()); + + PIDRecord updatedRecord = recordCaptor.getValue(); + assertEquals(pid, updatedRecord.pid()); + + // Verify tombstone marker + assertTrue(updatedRecord.record().stream() + .anyMatch(entry -> entry.key().equals("tombstone") && entry.value().equals("true"))); + + // Verify deletedAt timestamp + assertTrue(updatedRecord.record().stream() + .anyMatch(entry -> entry.key().equals("deletedAt"))); + + // Verify entity type + assertTrue(updatedRecord.record().stream() + .anyMatch(entry -> entry.key().equals("entityType") && entry.value().equals("TestEntity"))); + + // Verify Helmholtz Kernel Information Profile + assertTrue(updatedRecord.record().stream() + .anyMatch(entry -> entry.key().equals("21.T11148/076759916209e5d62bd5") && + entry.value().equals("21.T11148/b9b76f887845e32d29f7"))); + + // Verify basic metadata + assertTrue(updatedRecord.record().stream() + .anyMatch(entry -> entry.key().equals("name") && entry.value().equals("Test Entity"))); + assertTrue(updatedRecord.record().stream() + .anyMatch(entry -> entry.key().equals("description") && entry.value().equals("Test Description"))); + } + + @Test + void handleEntityDeletedEvent_withoutMeaningfulRecords_createsTombstoneWithMinimalInfo() { + // Arrange + String pid = "test-pid"; + TestEntity entity = new TestEntity(); + entity.setPid(pid); + entity.setName("Test Entity"); + entity.setDescription("Test Description"); + + EntityDeletedEvent event = new EntityDeletedEvent<>(entity); + + PIDRecord existingRecord = new PIDRecord(pid, List.of( + new PIDRecordEntry("digitalObjectLocation", "http://example.com/pid/" + pid) + )); + + when(client.getPIDRecord(pid)).thenReturn(existingRecord); + when(config.isMeaningfulPIDRecords()).thenReturn(false); + + // Act + listener.handleEntityDeletedEvent(event); + + // Assert + ArgumentCaptor recordCaptor = ArgumentCaptor.forClass(PIDRecord.class); + verify(client).updatePIDRecord(eq(pid), recordCaptor.capture()); + + PIDRecord updatedRecord = recordCaptor.getValue(); + assertEquals(pid, updatedRecord.pid()); + + // Verify tombstone marker + assertTrue(updatedRecord.record().stream() + .anyMatch(entry -> entry.key().equals("tombstone") && entry.value().equals("true"))); + + // Verify deletedAt timestamp + assertTrue(updatedRecord.record().stream() + .anyMatch(entry -> entry.key().equals("deletedAt"))); + + // Verify entity type + assertTrue(updatedRecord.record().stream() + .anyMatch(entry -> entry.key().equals("entityType") && entry.value().equals("TestEntity"))); + + // Verify no Helmholtz Kernel Information Profile + assertFalse(updatedRecord.record().stream() + .anyMatch(entry -> entry.key().equals("21.T11148/076759916209e5d62bd5"))); + + // Verify no basic metadata + assertFalse(updatedRecord.record().stream() + .anyMatch(entry -> entry.key().equals("name"))); + assertFalse(updatedRecord.record().stream() + .anyMatch(entry -> entry.key().equals("description"))); + } + + @Test + void handleEntityDeletedEvent_withNullPid_doesNothing() { + // Arrange + TestEntity entity = new TestEntity(); + entity.setPid(null); + + EntityDeletedEvent event = new EntityDeletedEvent<>(entity); + + // Act + listener.handleEntityDeletedEvent(event); + + // Assert + verify(client, never()).getPIDRecord(any()); + verify(client, never()).updatePIDRecord(any(), any()); + } + + @Test + void handleEntityDeletedEvent_withEmptyPid_doesNothing() { + // Arrange + TestEntity entity = new TestEntity(); + entity.setPid(""); + + EntityDeletedEvent event = new EntityDeletedEvent<>(entity); + + // Act + listener.handleEntityDeletedEvent(event); + + // Assert + verify(client, never()).getPIDRecord(any()); + verify(client, never()).updatePIDRecord(any(), any()); + } + + @Test + void handleEntityDeletedEvent_whenClientThrowsException_handlesGracefully() { + // Arrange + String pid = "test-pid"; + TestEntity entity = new TestEntity(); + entity.setPid(pid); + + EntityDeletedEvent event = new EntityDeletedEvent<>(entity); + + when(client.getPIDRecord(pid)).thenThrow(new RuntimeException("Test exception")); + + // Act & Assert + assertDoesNotThrow(() -> listener.handleEntityDeletedEvent(event)); + verify(client, never()).updatePIDRecord(any(), any()); + } + + // Test entity class + private static class TestEntity extends AdministrativeMetadata { + @Override + protected > T accept( + edu.kit.datamanager.idoris.rules.logic.Visitor visitor, Object... args) { + // Simple implementation for testing purposes + // Since this is just a test class, we return null + return null; + } + } +} diff --git a/src/test/java/edu/kit/datamanager/idoris/pids/TypedPIDMakerIDGeneratorTest.java b/src/test/java/edu/kit/datamanager/idoris/pids/TypedPIDMakerIDGeneratorTest.java new file mode 100644 index 0000000..8d146d9 --- /dev/null +++ b/src/test/java/edu/kit/datamanager/idoris/pids/TypedPIDMakerIDGeneratorTest.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.pids; + +import edu.kit.datamanager.idoris.configuration.ApplicationProperties; +import edu.kit.datamanager.idoris.configuration.TypedPIDMakerConfig; +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; +import edu.kit.datamanager.idoris.pids.client.TypedPIDMakerClient; +import edu.kit.datamanager.idoris.pids.client.model.PIDRecord; +import edu.kit.datamanager.idoris.rules.logic.RuleOutput; +import edu.kit.datamanager.idoris.rules.logic.Visitor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Instant; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class TypedPIDMakerIDGeneratorTest { + + private final String baseUrl = "http://example.com"; + @Mock + private TypedPIDMakerClient client; + @Mock + private TypedPIDMakerConfig config; + @Mock + private ApplicationProperties applicationProperties; + private TypedPIDMakerIDGenerator generator; + + @BeforeEach + void setUp() { + when(applicationProperties.getBaseUrl()).thenReturn(baseUrl); + generator = new TypedPIDMakerIDGenerator(applicationProperties, client, config); + } + + @Test + void generateId_alwaysIncludesPointerToEntity() { + // Arrange + TestEntity entity = new TestEntity(); + entity.setInternalId("test-internal-id"); + + when(config.isMeaningfulPIDRecords()).thenReturn(false); + + PIDRecord createdRecord = new PIDRecord("test-pid", List.of()); + when(client.createPIDRecord(any())).thenReturn(createdRecord); + + // Act + String pid = generator.generateId("TestEntity", entity); + + // Assert + assertEquals("test-pid", pid); + + ArgumentCaptor recordCaptor = ArgumentCaptor.forClass(PIDRecord.class); + verify(client).createPIDRecord(recordCaptor.capture()); + + PIDRecord capturedRecord = recordCaptor.getValue(); + + // Verify that the record contains a pointer to the entity + assertTrue(capturedRecord.record().stream() + .anyMatch(entry -> entry.key().equals("digitalObjectLocation") && + entry.value().contains(entity.getInternalId()))); + + // Verify that the record contains the entity type + assertTrue(capturedRecord.record().stream() + .anyMatch(entry -> entry.key().equals("entityType") && + entry.value().equals("TestEntity"))); + } + + @Test + void generateId_withMeaningfulRecords_includesAdministrativeMetadata() { + // Arrange + TestEntity entity = new TestEntity(); + entity.setInternalId("test-internal-id"); + entity.setName("Test Entity"); + entity.setDescription("Test Description"); + entity.setCreatedAt(Instant.now()); + entity.setLastModifiedAt(Instant.now()); + entity.setVersion(1L); + + when(config.isMeaningfulPIDRecords()).thenReturn(true); + + PIDRecord createdRecord = new PIDRecord("test-pid", List.of()); + when(client.createPIDRecord(any())).thenReturn(createdRecord); + + // Act + String pid = generator.generateId("TestEntity", entity); + + // Assert + assertEquals("test-pid", pid); + + ArgumentCaptor recordCaptor = ArgumentCaptor.forClass(PIDRecord.class); + verify(client).createPIDRecord(recordCaptor.capture()); + + PIDRecord capturedRecord = recordCaptor.getValue(); + + // Verify that the record contains a pointer to the entity + assertTrue(capturedRecord.record().stream() + .anyMatch(entry -> entry.key().equals("digitalObjectLocation") && + entry.value().contains(entity.getInternalId()))); + + // Verify that the record contains the entity type + assertTrue(capturedRecord.record().stream() + .anyMatch(entry -> entry.key().equals("entityType") && + entry.value().equals("TestEntity"))); + + // Verify that the record contains the Helmholtz Kernel Information Profile + assertTrue(capturedRecord.record().stream() + .anyMatch(entry -> entry.key().equals("21.T11148/076759916209e5d62bd5") && + entry.value().equals("21.T11148/b9b76f887845e32d29f7"))); + + // Verify that the record contains basic metadata + assertTrue(capturedRecord.record().stream() + .anyMatch(entry -> entry.key().equals("name") && + entry.value().equals("Test Entity"))); + + assertTrue(capturedRecord.record().stream() + .anyMatch(entry -> entry.key().equals("description") && + entry.value().equals("Test Description"))); + + // Verify that the record contains timestamps + assertTrue(capturedRecord.record().stream() + .anyMatch(entry -> entry.key().equals("21.T11148/aafd5fb4c7222e2d950a"))); + + assertTrue(capturedRecord.record().stream() + .anyMatch(entry -> entry.key().equals("21.T11148/397d831aa3a9d18eb52c"))); + + // Verify that the record contains version information + assertTrue(capturedRecord.record().stream() + .anyMatch(entry -> entry.key().equals("21.T11148/c692273deb2772da307f") && + entry.value().equals("1"))); + } + + @Test + void generateId_withExistingPid_updatesRecord() { + // Arrange + TestEntity entity = new TestEntity(); + entity.setInternalId("test-internal-id"); + entity.setPid("existing-pid"); + entity.setName("Test Entity"); + + when(config.isMeaningfulPIDRecords()).thenReturn(true); + when(config.isUpdatePIDRecords()).thenReturn(true); + + PIDRecord existingRecord = new PIDRecord("existing-pid", List.of()); + when(client.getPIDRecord("existing-pid")).thenReturn(existingRecord); + + // Act + String pid = generator.generateId("TestEntity", entity); + + // Assert + assertEquals("existing-pid", pid); + + ArgumentCaptor recordCaptor = ArgumentCaptor.forClass(PIDRecord.class); + verify(client).updatePIDRecord(eq("existing-pid"), recordCaptor.capture()); + + PIDRecord capturedRecord = recordCaptor.getValue(); + + // Verify that the record contains a pointer to the entity + assertTrue(capturedRecord.record().stream() + .anyMatch(entry -> entry.key().equals("digitalObjectLocation") && + entry.value().contains("existing-pid"))); + + // Verify that the record contains the entity type + assertTrue(capturedRecord.record().stream() + .anyMatch(entry -> entry.key().equals("entityType") && + entry.value().equals("TestEntity"))); + + // Verify that the record contains basic metadata + assertTrue(capturedRecord.record().stream() + .anyMatch(entry -> entry.key().equals("name") && + entry.value().equals("Test Entity"))); + } + + // Test entity class + private static class TestEntity extends AdministrativeMetadata { + @Override + protected > T accept(Visitor visitor, Object... args) { + // Simple implementation for testing purposes + return null; + } + } +} \ No newline at end of file diff --git a/src/test/java/edu/kit/datamanager/idoris/pids/web/v1/PersistentIdentifierControllerTest.java b/src/test/java/edu/kit/datamanager/idoris/pids/web/v1/PersistentIdentifierControllerTest.java new file mode 100644 index 0000000..ceff516 --- /dev/null +++ b/src/test/java/edu/kit/datamanager/idoris/pids/web/v1/PersistentIdentifierControllerTest.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.pids.web.v1; + +import edu.kit.datamanager.idoris.pids.entities.PersistentIdentifier; +import edu.kit.datamanager.idoris.pids.services.PersistentIdentifierService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.time.Instant; +import java.util.HashMap; +import java.util.List; +import java.util.Optional; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +class PersistentIdentifierControllerTest { + + @Mock + private PersistentIdentifierService service; + + @InjectMocks + private PersistentIdentifierController controller; + + private MockMvc mockMvc; + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + @Test + void getAllPersistentIdentifiers_shouldReturnAllPersistentIdentifiers() throws Exception { + // Arrange + PersistentIdentifier pid1 = createPersistentIdentifier("pid1", "TestEntity", "entity1", false); + PersistentIdentifier pid2 = createPersistentIdentifier("pid2", "TestEntity", "entity2", false); + List pids = List.of(pid1, pid2); + + when(service.getAllPersistentIdentifiers()).thenReturn(pids); + + // Act & Assert + mockMvc.perform(get("/api/v1/pids") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[0].pid", is("pid1"))) + .andExpect(jsonPath("$[1].pid", is("pid2"))); + } + + @Test + void getPersistentIdentifier_withExistingPid_shouldReturnPersistentIdentifier() throws Exception { + // Arrange + String pid = "test-pid"; + PersistentIdentifier persistentIdentifier = createPersistentIdentifier(pid, "TestEntity", "entity1", false); + + when(service.getPersistentIdentifier(pid)).thenReturn(Optional.of(persistentIdentifier)); + + // Act & Assert + mockMvc.perform(get("/api/v1/pids/{pid}", pid) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.pid", is(pid))) + .andExpect(jsonPath("$.entityType", is("TestEntity"))) + .andExpect(jsonPath("$.entityInternalId", is("entity1"))) + .andExpect(jsonPath("$.tombstone", is(false))); + } + + @Test + void getPersistentIdentifier_withNonExistingPid_shouldReturnNotFound() throws Exception { + // Arrange + String pid = "non-existing-pid"; + + when(service.getPersistentIdentifier(pid)).thenReturn(Optional.empty()); + + // Act & Assert + mockMvc.perform(get("/api/v1/pids/{pid}", pid) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + void getPersistentIdentifiersByEntityType_shouldReturnPersistentIdentifiersForEntityType() throws Exception { + // Arrange + String entityType = "TestEntity"; + PersistentIdentifier pid1 = createPersistentIdentifier("pid1", entityType, "entity1", false); + PersistentIdentifier pid2 = createPersistentIdentifier("pid2", entityType, "entity2", false); + List pids = List.of(pid1, pid2); + + when(service.getPersistentIdentifiersByEntityType(entityType)).thenReturn(pids); + + // Act & Assert + mockMvc.perform(get("/api/v1/pids/byEntityType/{entityType}", entityType) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[0].pid", is("pid1"))) + .andExpect(jsonPath("$[1].pid", is("pid2"))) + .andExpect(jsonPath("$[0].entityType", is(entityType))) + .andExpect(jsonPath("$[1].entityType", is(entityType))); + } + + @Test + void getTombstones_shouldReturnTombstones() throws Exception { + // Arrange + PersistentIdentifier pid1 = createPersistentIdentifier("pid1", "TestEntity", "entity1", true); + PersistentIdentifier pid2 = createPersistentIdentifier("pid2", "TestEntity", "entity2", true); + List pids = List.of(pid1, pid2); + + when(service.getTombstones()).thenReturn(pids); + + // Act & Assert + mockMvc.perform(get("/api/v1/pids/tombstones") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[0].pid", is("pid1"))) + .andExpect(jsonPath("$[1].pid", is("pid2"))) + .andExpect(jsonPath("$[0].tombstone", is(true))) + .andExpect(jsonPath("$[1].tombstone", is(true))); + } + + private PersistentIdentifier createPersistentIdentifier(String pid, String entityType, String entityInternalId, boolean tombstone) { + PersistentIdentifier persistentIdentifier = new PersistentIdentifier(); + persistentIdentifier.setPid(pid); + persistentIdentifier.setEntityType(entityType); + persistentIdentifier.setEntityInternalId(entityInternalId); + persistentIdentifier.setTombstone(tombstone); + if (tombstone) { + persistentIdentifier.setDeletedAt(Instant.now()); + } + persistentIdentifier.setCreatedAt(Instant.now()); + persistentIdentifier.setLastModifiedAt(Instant.now()); + persistentIdentifier.setVersion(1L); + persistentIdentifier.setMetadata(new HashMap<>()); + return persistentIdentifier; + } +} \ No newline at end of file diff --git a/src/test/java/edu/kit/datamanager/idoris/pids/web/v1/PidControllerTest.java b/src/test/java/edu/kit/datamanager/idoris/pids/web/v1/PidControllerTest.java new file mode 100644 index 0000000..34edc9e --- /dev/null +++ b/src/test/java/edu/kit/datamanager/idoris/pids/web/v1/PidControllerTest.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.pids.web.v1; + +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; +import edu.kit.datamanager.idoris.pids.entities.PersistentIdentifier; +import edu.kit.datamanager.idoris.pids.services.PersistentIdentifierService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.time.Instant; +import java.util.HashMap; +import java.util.List; +import java.util.Optional; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@ExtendWith(MockitoExtension.class) +class PidControllerTest { + + @Mock + private PersistentIdentifierService pidService; + + @InjectMocks + private PidController controller; + + private MockMvc mockMvc; + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + @Test + void getAllPersistentIdentifiers_shouldReturnAllPersistentIdentifiers() throws Exception { + // Arrange + PersistentIdentifier pid1 = createPersistentIdentifier("pid1", "TestEntity", "entity1", false); + PersistentIdentifier pid2 = createPersistentIdentifier("pid2", "TestEntity", "entity2", false); + List pids = List.of(pid1, pid2); + + when(pidService.getAllPersistentIdentifiers()).thenReturn(pids); + + // Act & Assert + mockMvc.perform(get("/pid") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[0].pid", is("pid1"))) + .andExpect(jsonPath("$[1].pid", is("pid2"))); + } + + @Test + void redirectToEntity_withExistingPid_shouldRedirectToEntity() throws Exception { + // Arrange + String pidValue = "test-pid"; + String entityType = "testentity"; + String entityId = "entity-id"; + + PersistentIdentifier pid = createPersistentIdentifier(pidValue, "TestEntity", entityId, false); + pid.setEntity(mock(AdministrativeMetadata.class)); + + when(pidService.getPersistentIdentifier(pidValue)).thenReturn(Optional.of(pid)); + + // Act & Assert + mockMvc.perform(get("/pid/{pidValue}", pidValue)) + .andExpect(status().isFound()) + .andExpect(header().string("Location", "/" + entityType + "s/" + entityId)); + } + + @Test + void redirectToEntity_withTombstonePid_shouldRedirectToTombstone() throws Exception { + // Arrange + String pidValue = "test-pid"; + + PersistentIdentifier pid = createPersistentIdentifier(pidValue, "TestEntity", "entity-id", true); + + when(pidService.getPersistentIdentifier(pidValue)).thenReturn(Optional.of(pid)); + + // Act & Assert + mockMvc.perform(get("/pid/{pidValue}", pidValue)) + .andExpect(status().isFound()) + .andExpect(header().string("Location", "/tombstone/" + pidValue)); + } + + @Test + void redirectToEntity_withNonExistingPid_shouldReturnNotFound() throws Exception { + // Arrange + String pidValue = "non-existing-pid"; + + when(pidService.getPersistentIdentifier(pidValue)).thenReturn(Optional.empty()); + + // Act & Assert + mockMvc.perform(get("/pid/{pidValue}", pidValue)) + .andExpect(status().isNotFound()); + } + + @Test + void redirectToEntity_withPidWithoutEntityAndNotTombstone_shouldReturnNotFound() throws Exception { + // Arrange + String pidValue = "test-pid"; + + PersistentIdentifier pid = createPersistentIdentifier(pidValue, "TestEntity", "entity-id", false); + pid.setEntity(null); + + when(pidService.getPersistentIdentifier(pidValue)).thenReturn(Optional.of(pid)); + + // Act & Assert + mockMvc.perform(get("/pid/{pidValue}", pidValue)) + .andExpect(status().isNotFound()); + } + + @Test + void handleTombstone_withTombstonePid_shouldReturnGoneWithMessage() throws Exception { + // Arrange + String pidValue = "test-pid"; + Instant deletedAt = Instant.parse("2023-01-01T00:00:00Z"); + + PersistentIdentifier pid = createPersistentIdentifier(pidValue, "TestEntity", "entity-id", true); + pid.setDeletedAt(deletedAt); + + when(pidService.getPersistentIdentifier(pidValue)).thenReturn(Optional.of(pid)); + + // Act & Assert + mockMvc.perform(get("/pid/tombstone/{pidValue}", pidValue)) + .andExpect(status().isGone()) + .andExpect(content().string("The entity with PID test-pid has been deleted at 2023-01-01T00:00:00Z. Entity type: TestEntity")); + } + + @Test + void handleTombstone_withNonTombstonePid_shouldRedirectToEntity() throws Exception { + // Arrange + String pidValue = "test-pid"; + + PersistentIdentifier pid = createPersistentIdentifier(pidValue, "TestEntity", "entity-id", false); + + when(pidService.getPersistentIdentifier(pidValue)).thenReturn(Optional.of(pid)); + + // Act & Assert + mockMvc.perform(get("/pid/tombstone/{pidValue}", pidValue)) + .andExpect(status().isFound()) + .andExpect(header().string("Location", "/pid/" + pidValue)); + } + + @Test + void handleTombstone_withNonExistingPid_shouldReturnNotFound() throws Exception { + // Arrange + String pidValue = "non-existing-pid"; + + when(pidService.getPersistentIdentifier(pidValue)).thenReturn(Optional.empty()); + + // Act & Assert + mockMvc.perform(get("/pid/tombstone/{pidValue}", pidValue)) + .andExpect(status().isNotFound()); + } + + private PersistentIdentifier createPersistentIdentifier(String pid, String entityType, String entityInternalId, boolean tombstone) { + PersistentIdentifier persistentIdentifier = new PersistentIdentifier(); + persistentIdentifier.setPid(pid); + persistentIdentifier.setEntityType(entityType); + persistentIdentifier.setEntityInternalId(entityInternalId); + persistentIdentifier.setTombstone(tombstone); + if (tombstone) { + persistentIdentifier.setDeletedAt(Instant.now()); + } + persistentIdentifier.setCreatedAt(Instant.now()); + persistentIdentifier.setLastModifiedAt(Instant.now()); + persistentIdentifier.setVersion(1L); + persistentIdentifier.setMetadata(new HashMap<>()); + return persistentIdentifier; + } +} \ No newline at end of file diff --git a/src/test/java/edu/kit/datamanager/idoris/pids/web/v1/PidRedirectControllerV2Test.java b/src/test/java/edu/kit/datamanager/idoris/pids/web/v1/PidRedirectControllerV2Test.java new file mode 100644 index 0000000..9bfdf8e --- /dev/null +++ b/src/test/java/edu/kit/datamanager/idoris/pids/web/v1/PidRedirectControllerV2Test.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.pids.web.v1; + +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; +import edu.kit.datamanager.idoris.pids.entities.PersistentIdentifier; +import edu.kit.datamanager.idoris.pids.services.PersistentIdentifierService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Optional; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@ExtendWith(MockitoExtension.class) +class PidRedirectControllerV2Test { + + @Mock + private PersistentIdentifierService pidService; + + @InjectMocks + private PidRedirectControllerV2 controller; + + private MockMvc mockMvc; + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + @Test + void redirectToEntity_withExistingPid_shouldRedirectToEntity() throws Exception { + // Arrange + String pidValue = "test-pid"; + String entityType = "testentity"; + String entityId = "entity-id"; + + PersistentIdentifier pid = createPersistentIdentifier(pidValue, "TestEntity", entityId, false); + pid.setEntity(mock(AdministrativeMetadata.class)); + + when(pidService.getPersistentIdentifier(pidValue)).thenReturn(Optional.of(pid)); + + // Act & Assert + mockMvc.perform(get("/pid/{pidValue}", pidValue)) + .andExpect(status().isFound()) + .andExpect(header().string("Location", "/" + entityType + "s/" + entityId)); + } + + @Test + void redirectToEntity_withTombstonePid_shouldRedirectToTombstone() throws Exception { + // Arrange + String pidValue = "test-pid"; + + PersistentIdentifier pid = createPersistentIdentifier(pidValue, "TestEntity", "entity-id", true); + + when(pidService.getPersistentIdentifier(pidValue)).thenReturn(Optional.of(pid)); + + // Act & Assert + mockMvc.perform(get("/pid/{pidValue}", pidValue)) + .andExpect(status().isFound()) + .andExpect(header().string("Location", "/tombstone/" + pidValue)); + } + + @Test + void redirectToEntity_withNonExistingPid_shouldReturnNotFound() throws Exception { + // Arrange + String pidValue = "non-existing-pid"; + + when(pidService.getPersistentIdentifier(pidValue)).thenReturn(Optional.empty()); + + // Act & Assert + mockMvc.perform(get("/pid/{pidValue}", pidValue)) + .andExpect(status().isNotFound()); + } + + @Test + void redirectToEntity_withPidWithoutEntityAndNotTombstone_shouldReturnNotFound() throws Exception { + // Arrange + String pidValue = "test-pid"; + + PersistentIdentifier pid = createPersistentIdentifier(pidValue, "TestEntity", "entity-id", false); + pid.setEntity(null); + + when(pidService.getPersistentIdentifier(pidValue)).thenReturn(Optional.of(pid)); + + // Act & Assert + mockMvc.perform(get("/pid/{pidValue}", pidValue)) + .andExpect(status().isNotFound()); + } + + @Test + void handleTombstone_withTombstonePid_shouldReturnGoneWithMessage() throws Exception { + // Arrange + String pidValue = "test-pid"; + Instant deletedAt = Instant.parse("2023-01-01T00:00:00Z"); + + PersistentIdentifier pid = createPersistentIdentifier(pidValue, "TestEntity", "entity-id", true); + pid.setDeletedAt(deletedAt); + + when(pidService.getPersistentIdentifier(pidValue)).thenReturn(Optional.of(pid)); + + // Act & Assert + mockMvc.perform(get("/pid/tombstone/{pidValue}", pidValue)) + .andExpect(status().isGone()) + .andExpect(content().string("The entity with PID test-pid has been deleted at 2023-01-01T00:00:00Z. Entity type: TestEntity")); + } + + @Test + void handleTombstone_withNonTombstonePid_shouldRedirectToEntity() throws Exception { + // Arrange + String pidValue = "test-pid"; + + PersistentIdentifier pid = createPersistentIdentifier(pidValue, "TestEntity", "entity-id", false); + + when(pidService.getPersistentIdentifier(pidValue)).thenReturn(Optional.of(pid)); + + // Act & Assert + mockMvc.perform(get("/pid/tombstone/{pidValue}", pidValue)) + .andExpect(status().isFound()) + .andExpect(header().string("Location", "/pid/" + pidValue)); + } + + @Test + void handleTombstone_withNonExistingPid_shouldReturnNotFound() throws Exception { + // Arrange + String pidValue = "non-existing-pid"; + + when(pidService.getPersistentIdentifier(pidValue)).thenReturn(Optional.empty()); + + // Act & Assert + mockMvc.perform(get("/pid/tombstone/{pidValue}", pidValue)) + .andExpect(status().isNotFound()); + } + + private PersistentIdentifier createPersistentIdentifier(String pid, String entityType, String entityInternalId, boolean tombstone) { + PersistentIdentifier persistentIdentifier = new PersistentIdentifier(); + persistentIdentifier.setPid(pid); + persistentIdentifier.setEntityType(entityType); + persistentIdentifier.setEntityInternalId(entityInternalId); + persistentIdentifier.setTombstone(tombstone); + if (tombstone) { + persistentIdentifier.setDeletedAt(Instant.now()); + } + persistentIdentifier.setCreatedAt(Instant.now()); + persistentIdentifier.setLastModifiedAt(Instant.now()); + persistentIdentifier.setVersion(1L); + persistentIdentifier.setMetadata(new HashMap<>()); + return persistentIdentifier; + } +} \ No newline at end of file From 77445d604993a0b573b3efd5fc84598fb5b79454 Mon Sep 17 00:00:00 2001 From: Maximilian Inckmann Date: Fri, 18 Jul 2025 09:55:03 +0200 Subject: [PATCH 06/19] modified PID management Signed-off-by: Maximilian Inckmann --- .../idoris/attributes/dao/IAttributeDao.java | 3 +- .../attributes/services/AttributeService.java | 48 ++-- .../attributes/web/api/IAttributeApi.java | 46 ++-- .../web/hateoas/AttributeModelAssembler.java | 6 +- .../web/v1/AttributeController.java | 15 +- .../configuration/ApplicationProperties.java | 22 +- .../configuration/TypedPIDMakerConfig.java | 24 -- .../idoris/core/domain/dao/IGenericRepo.java | 41 +++- .../entities/AdministrativeMetadata.java | 10 - .../core/events/EventPublisherService.java | 28 +-- .../idoris/core/events/PIDGeneratedEvent.java | 60 ++--- .../datatypes/dao/IAtomicDataTypeDao.java | 32 ++- .../idoris/datatypes/dao/IDataTypeDao.java | 62 ++++- .../idoris/datatypes/dao/ITypeProfileDao.java | 48 +++- .../idoris/datatypes/entities/DataType.java | 9 - .../services/AtomicDataTypeService.java | 50 ++-- .../services/TypeProfileService.java | 78 +++--- .../datatypes/web/api/IAtomicDataTypeApi.java | 46 ++-- .../datatypes/web/api/ITypeProfileApi.java | 70 +++--- .../hateoas/AtomicDataTypeModelAssembler.java | 6 +- .../web/hateoas/DataTypeModelAssembler.java | 6 +- .../hateoas/TypeProfileModelAssembler.java | 4 +- .../web/v1/AtomicDataTypeController.java | 74 +++--- .../web/v1/TypeProfileController.java | 122 +++++----- .../notification/EntityChangeNotifier.java | 222 ------------------ .../notification/EntityChangeSubscriber.java | 49 ---- .../LoggingEntityChangeSubscriber.java | 76 ------ .../idoris/notification/package-info.java | 29 --- .../operations/dao/IAttributeMappingDao.java | 88 ++++++- .../idoris/operations/dao/IOperationDao.java | 59 ++++- .../services/AttributeMappingService.java | 30 ++- .../operations/services/OperationService.java | 56 ++--- .../operations/web/api/IOperationApi.java | 52 ++-- .../web/hateoas/OperationModelAssembler.java | 4 +- .../web/v1/OperationController.java | 72 +++--- .../idoris/pids/ConfigurablePIDGenerator.java | 37 +-- .../idoris/pids/MetadataEventListener.java | 16 +- .../pids/client/TypedPIDMakerClient.java | 12 +- .../client/TypedPIDMakerClientConfig.java | 17 +- .../idoris/pids/client/model/PIDRecord.java | 37 +++ .../pids/entities/PersistentIdentifier.java | 51 ---- .../PersistentIdentifierRepository.java | 12 - .../services/PersistentIdentifierService.java | 56 +++-- .../idoris/pids/utils/PIDRecordMapper.java | 79 +++---- .../validation/InheritanceValidator.java | 4 +- .../rules/validation/SyntaxValidator.java | 5 - .../validation/ValidationPolicyValidator.java | 30 +-- .../dao/ITechnologyInterfaceDao.java | 2 +- .../services/TechnologyInterfaceService.java | 48 ++-- .../web/api/ITechnologyInterfaceApi.java | 54 ++--- .../TechnologyInterfaceModelAssembler.java | 6 +- .../web/v1/TechnologyInterfaceController.java | 47 ++-- .../idoris/users/services/UserService.java | 12 +- .../idoris/users/web/api/IUserApi.java | 18 +- .../idoris/users/web/v1/UserController.java | 3 +- src/main/resources/application.properties | 9 +- .../core/domain/dao/IGenericRepoTest.java | 94 ++++++++ .../pids/PIDTombstoneEventListenerTest.java | 221 ----------------- .../PersistentIdentifierControllerTest.java | 164 ------------- .../web/v1/PidRedirectControllerV2Test.java | 175 -------------- 60 files changed, 1148 insertions(+), 1708 deletions(-) delete mode 100644 src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeNotifier.java delete mode 100644 src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeSubscriber.java delete mode 100644 src/main/java/edu/kit/datamanager/idoris/notification/LoggingEntityChangeSubscriber.java delete mode 100644 src/main/java/edu/kit/datamanager/idoris/notification/package-info.java create mode 100644 src/test/java/edu/kit/datamanager/idoris/core/domain/dao/IGenericRepoTest.java delete mode 100644 src/test/java/edu/kit/datamanager/idoris/pids/PIDTombstoneEventListenerTest.java delete mode 100644 src/test/java/edu/kit/datamanager/idoris/pids/web/v1/PersistentIdentifierControllerTest.java delete mode 100644 src/test/java/edu/kit/datamanager/idoris/pids/web/v1/PidRedirectControllerV2Test.java diff --git a/src/main/java/edu/kit/datamanager/idoris/attributes/dao/IAttributeDao.java b/src/main/java/edu/kit/datamanager/idoris/attributes/dao/IAttributeDao.java index 9ce8251..6c6e06a 100644 --- a/src/main/java/edu/kit/datamanager/idoris/attributes/dao/IAttributeDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/attributes/dao/IAttributeDao.java @@ -29,6 +29,5 @@ public interface IAttributeDao extends IGenericRepo { " WITH collect(DISTINCT x) as otherNodes, n" + " WHERE NOT n IN otherNodes" + " DETACH DELETE n") -// @RestResource(exported = false) void deleteOrphanedAttributes(); -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/idoris/attributes/services/AttributeService.java b/src/main/java/edu/kit/datamanager/idoris/attributes/services/AttributeService.java index ecfcb8f..05a6f24 100644 --- a/src/main/java/edu/kit/datamanager/idoris/attributes/services/AttributeService.java +++ b/src/main/java/edu/kit/datamanager/idoris/attributes/services/AttributeService.java @@ -59,7 +59,7 @@ public Attribute createAttribute(Attribute attribute) { log.debug("Creating Attribute: {}", attribute); Attribute saved = attributeDao.save(attribute); eventPublisher.publishEntityCreated(saved); - log.info("Created Attribute with PID: {}", saved.getPid()); + log.info("Created Attribute with PID: {}", saved.getId()); return saved; } @@ -74,50 +74,50 @@ public Attribute createAttribute(Attribute attribute) { public Attribute updateAttribute(Attribute attribute) { log.debug("Updating Attribute: {}", attribute); - if (attribute.getPid() == null || attribute.getPid().isEmpty()) { + if (attribute.getId() == null || attribute.getId().isEmpty()) { throw new IllegalArgumentException("Attribute must have a PID to be updated"); } // Get the current version before updating - Attribute existing = attributeDao.findById(attribute.getPid()) - .orElseThrow(() -> new IllegalArgumentException("Attribute not found with PID: " + attribute.getPid())); + Attribute existing = attributeDao.findById(attribute.getId()) + .orElseThrow(() -> new IllegalArgumentException("Attribute not found with PID: " + attribute.getId())); Long previousVersion = existing.getVersion(); Attribute saved = attributeDao.save(attribute); eventPublisher.publishEntityUpdated(saved, previousVersion); - log.info("Updated Attribute with PID: {}", saved.getPid()); + log.info("Updated Attribute with PID: {}", saved.getId()); return saved; } /** * Deletes an Attribute entity. * - * @param pid the PID of the Attribute to delete + * @param id the PID or internal ID of the Attribute to delete * @throws IllegalArgumentException if the Attribute does not exist */ @Transactional - public void deleteAttribute(String pid) { - log.debug("Deleting Attribute with PID: {}", pid); + public void deleteAttribute(String id) { + log.debug("Deleting Attribute with ID: {}", id); - Attribute attribute = attributeDao.findById(pid) - .orElseThrow(() -> new IllegalArgumentException("Attribute not found with PID: " + pid)); + Attribute attribute = attributeDao.findById(id) + .orElseThrow(() -> new IllegalArgumentException("Attribute not found with ID: " + id)); attributeDao.delete(attribute); eventPublisher.publishEntityDeleted(attribute); - log.info("Deleted Attribute with PID: {}", pid); + log.info("Deleted Attribute with ID: {}", id); } /** - * Retrieves an Attribute entity by its PID. + * Retrieves an Attribute entity by its PID or internal ID. * - * @param pid the PID of the Attribute to retrieve + * @param id the PID or internal ID of the Attribute to retrieve * @return an Optional containing the Attribute, or empty if not found */ @Transactional(readOnly = true) - public Optional getAttribute(String pid) { - log.debug("Retrieving Attribute with PID: {}", pid); - return attributeDao.findById(pid); + public Optional getAttribute(String id) { + log.debug("Retrieving Attribute with ID: {}", id); + return attributeDao.findById(id); } /** @@ -145,21 +145,21 @@ public void deleteOrphanedAttributes() { /** * Partially updates an existing Attribute entity. * - * @param pid the PID of the Attribute to patch + * @param id the PID or internal ID of the Attribute to patch * @param attributePatch the partial Attribute entity with fields to update * @return the patched Attribute entity * @throws IllegalArgumentException if the Attribute does not exist */ @Transactional - public Attribute patchAttribute(String pid, Attribute attributePatch) { - log.debug("Patching Attribute with PID: {}, patch: {}", pid, attributePatch); - if (pid == null || pid.isEmpty()) { - throw new IllegalArgumentException("Attribute PID cannot be null or empty"); + public Attribute patchAttribute(String id, Attribute attributePatch) { + log.debug("Patching Attribute with ID: {}, patch: {}", id, attributePatch); + if (id == null || id.isEmpty()) { + throw new IllegalArgumentException("Attribute ID cannot be null or empty"); } // Get the current entity - Attribute existing = attributeDao.findById(pid) - .orElseThrow(() -> new IllegalArgumentException("Attribute not found with PID: " + pid)); + Attribute existing = attributeDao.findById(id) + .orElseThrow(() -> new IllegalArgumentException("Attribute not found with ID: " + id)); Long previousVersion = existing.getVersion(); // Apply non-null fields from the patch to the existing entity @@ -194,7 +194,7 @@ public Attribute patchAttribute(String pid, Attribute attributePatch) { // Publish the patched event eventPublisher.publishEntityPatched(saved, previousVersion); - log.info("Patched Attribute with PID: {}", saved.getPid()); + log.info("Patched Attribute with PID: {}", saved.getId()); return saved; } } diff --git a/src/main/java/edu/kit/datamanager/idoris/attributes/web/api/IAttributeApi.java b/src/main/java/edu/kit/datamanager/idoris/attributes/web/api/IAttributeApi.java index 2c51b64..f3bc865 100644 --- a/src/main/java/edu/kit/datamanager/idoris/attributes/web/api/IAttributeApi.java +++ b/src/main/java/edu/kit/datamanager/idoris/attributes/web/api/IAttributeApi.java @@ -55,15 +55,15 @@ public interface IAttributeApi { ResponseEntity>> getAllAttributes(); /** - * Gets an Attribute entity by its PID. + * Gets an Attribute entity by its PID or internal ID. * - * @param pid the PID of the Attribute to retrieve + * @param id the PID or internal ID of the Attribute to retrieve * @return the Attribute entity */ - @GetMapping("/{pid}") + @GetMapping("/{id}") @Operation( - summary = "Get an Attribute by PID", - description = "Returns an Attribute entity by its PID", + summary = "Get an Attribute by PID or internal ID", + description = "Returns an Attribute entity by its PID or internal ID", responses = { @ApiResponse(responseCode = "200", description = "Attribute found", content = @Content(mediaType = "application/hal+json", @@ -72,16 +72,16 @@ public interface IAttributeApi { } ) ResponseEntity> getAttribute( - @Parameter(description = "PID of the Attribute", required = true) - @PathVariable String pid); + @Parameter(description = "PID or internal ID of the Attribute", required = true) + @PathVariable String id); /** * Gets the DataType of an Attribute. * - * @param pid the PID of the Attribute + * @param id the PID or internal ID of the Attribute * @return the DataType of the Attribute */ - @GetMapping("/{pid}/dataType") + @GetMapping("/{id}/dataType") @Operation( summary = "Get the DataType of an Attribute", description = "Returns the DataType of an Attribute", @@ -93,8 +93,8 @@ ResponseEntity> getAttribute( } ) ResponseEntity> getDataType( - @Parameter(description = "PID of the Attribute", required = true) - @PathVariable String pid); + @Parameter(description = "PID or internal ID of the Attribute", required = true) + @PathVariable String id); /** * Creates a new Attribute entity. @@ -120,11 +120,11 @@ ResponseEntity> createAttribute( /** * Updates an existing Attribute entity. * - * @param pid the PID of the Attribute to update + * @param id the PID or internal ID of the Attribute to update * @param attribute the updated Attribute entity * @return the updated Attribute entity */ - @PutMapping("/{pid}") + @PutMapping("/{id}") @Operation( summary = "Update an Attribute", description = "Updates an existing Attribute entity", @@ -137,18 +137,18 @@ ResponseEntity> createAttribute( } ) ResponseEntity> updateAttribute( - @Parameter(description = "PID of the Attribute", required = true) - @PathVariable String pid, + @Parameter(description = "PID or internal ID of the Attribute", required = true) + @PathVariable String id, @Parameter(description = "Updated Attribute", required = true) @Valid @RequestBody Attribute attribute); /** * Deletes an Attribute entity. * - * @param pid the PID of the Attribute to delete + * @param id the PID or internal ID of the Attribute to delete * @return no content */ - @DeleteMapping("/{pid}") + @DeleteMapping("/{id}") @Operation( summary = "Delete an Attribute", description = "Deletes an Attribute entity", @@ -158,8 +158,8 @@ ResponseEntity> updateAttribute( } ) ResponseEntity deleteAttribute( - @Parameter(description = "PID of the Attribute", required = true) - @PathVariable String pid); + @Parameter(description = "PID or internal ID of the Attribute", required = true) + @PathVariable String id); /** * Deletes orphaned Attribute entities. @@ -180,11 +180,11 @@ ResponseEntity deleteAttribute( /** * Partially updates an Attribute entity. * - * @param pid the PID of the Attribute to patch + * @param id the PID or internal ID of the Attribute to patch * @param attributePatch the partial Attribute entity with fields to update * @return the patched Attribute entity */ - @PatchMapping("/{pid}") + @PatchMapping("/{id}") @Operation( summary = "Partially update an Attribute", description = "Updates specific fields of an existing Attribute entity", @@ -197,8 +197,8 @@ ResponseEntity deleteAttribute( } ) ResponseEntity> patchAttribute( - @Parameter(description = "PID of the Attribute", required = true) - @PathVariable String pid, + @Parameter(description = "PID or internal ID of the Attribute", required = true) + @PathVariable String id, @Parameter(description = "Partial Attribute with fields to update", required = true) @RequestBody Attribute attributePatch); } diff --git a/src/main/java/edu/kit/datamanager/idoris/attributes/web/hateoas/AttributeModelAssembler.java b/src/main/java/edu/kit/datamanager/idoris/attributes/web/hateoas/AttributeModelAssembler.java index 93bc7f3..64ee631 100644 --- a/src/main/java/edu/kit/datamanager/idoris/attributes/web/hateoas/AttributeModelAssembler.java +++ b/src/main/java/edu/kit/datamanager/idoris/attributes/web/hateoas/AttributeModelAssembler.java @@ -42,16 +42,16 @@ public EntityModel toModel(Attribute attribute) { EntityModel entityModel = toModelWithoutLinks(attribute); // Add self link - entityModel.add(linkTo(methodOn(AttributeController.class).getAttribute(attribute.getPid())).withSelfRel()); + entityModel.add(linkTo(methodOn(AttributeController.class).getAttribute(attribute.getId())).withSelfRel()); // Add link to data type if (attribute.getDataType() != null) { - entityModel.add(linkTo(methodOn(AttributeController.class).getDataType(attribute.getPid())).withRel("dataType")); + entityModel.add(linkTo(methodOn(AttributeController.class).getDataType(attribute.getId())).withRel("dataType")); } // Add link to override attribute if it exists if (attribute.getOverride() != null) { - entityModel.add(linkTo(methodOn(AttributeController.class).getAttribute(attribute.getOverride().getPid())).withRel("override")); + entityModel.add(linkTo(methodOn(AttributeController.class).getAttribute(attribute.getOverride().getId())).withRel("override")); } // Add link to all attributes diff --git a/src/main/java/edu/kit/datamanager/idoris/attributes/web/v1/AttributeController.java b/src/main/java/edu/kit/datamanager/idoris/attributes/web/v1/AttributeController.java index 5a8d051..2f53a56 100644 --- a/src/main/java/edu/kit/datamanager/idoris/attributes/web/v1/AttributeController.java +++ b/src/main/java/edu/kit/datamanager/idoris/attributes/web/v1/AttributeController.java @@ -107,12 +107,21 @@ public ResponseEntity> createAttribute(Attribute attribut * {@inheritDoc} */ @Override - public ResponseEntity> updateAttribute(String pid, Attribute attribute) { - if (attributeService.getAttribute(pid).isEmpty()) { + public ResponseEntity> updateAttribute(String id, Attribute attribute) { + // Check if the entity exists + if (attributeService.getAttribute(id).isEmpty()) { return ResponseEntity.notFound().build(); } - attribute.setPid(pid); + // Get the existing entity to get its PID and internalId + Attribute existing = attributeService.getAttribute(id).get(); + + // Set the PID from the existing entity + attribute.setInternalId(existing.getId()); + + // Ensure internal ID is preserved + attribute.setInternalId(existing.getInternalId()); + Attribute updatedAttribute = attributeService.updateAttribute(attribute); EntityModel entityModel = attributeModelAssembler.toModel(updatedAttribute); return ResponseEntity.ok(entityModel); diff --git a/src/main/java/edu/kit/datamanager/idoris/configuration/ApplicationProperties.java b/src/main/java/edu/kit/datamanager/idoris/configuration/ApplicationProperties.java index 521d66c..4e6d4d5 100644 --- a/src/main/java/edu/kit/datamanager/idoris/configuration/ApplicationProperties.java +++ b/src/main/java/edu/kit/datamanager/idoris/configuration/ApplicationProperties.java @@ -38,7 +38,7 @@ public class ApplicationProperties { /** * The base URL of the IDORIS service, used in e.g., the PID records. */ - @Value("${idoris.base-url") + @Value("${idoris.base-url}") @NotNull(message = "Base URL is required") private String baseUrl; @@ -60,22 +60,6 @@ public class ApplicationProperties { @NotNull private OutputMessage.MessageSeverity validationLevel = INFO; - /** - * The PID generation strategy to use. - *

  • - * LOCAL: Use the local PID generation strategy. - * This is the default strategy and uses the local database to generate PIDs. - *
  • - * TYPED_PID_MAKER: Use the Typed PID Maker service to generate PIDs. - * This strategy uses an external service to generate PIDs and therefore requires additional configuration. - * - * @see PIDGeneration - * @see TypedPIDMakerConfig - */ - @Value("${idoris.pid-generation}") - @NotNull - private PIDGeneration pidGeneration = PIDGeneration.LOCAL; - /** * The policy to use for validating the input. *

    @@ -87,8 +71,4 @@ public enum ValidationPolicy { STRICT, LAX } - public enum PIDGeneration { - LOCAL, - TYPED_PID_MAKER, - } } diff --git a/src/main/java/edu/kit/datamanager/idoris/configuration/TypedPIDMakerConfig.java b/src/main/java/edu/kit/datamanager/idoris/configuration/TypedPIDMakerConfig.java index 9f73338..8fc0cab 100644 --- a/src/main/java/edu/kit/datamanager/idoris/configuration/TypedPIDMakerConfig.java +++ b/src/main/java/edu/kit/datamanager/idoris/configuration/TypedPIDMakerConfig.java @@ -19,9 +19,6 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; @@ -29,30 +26,9 @@ @Component @ConfigurationProperties("idoris.typed-pid-maker") @Validated -@AutoConfigureAfter(value = ApplicationProperties.class) -@ConditionalOnBean(value = ApplicationProperties.class) -@ConditionalOnExpression( - "#{ '${idoris.pid-generation}' eq T(edu.kit.datamanager.idoris.configuration.ApplicationProperties.PIDGeneration).TYPED_PID_MAKER.name() }" -) @Getter @Setter public class TypedPIDMakerConfig { - /** - * Determines whether the PID records should only contain a pointer to the entity in IDORIS - * or if they should contain meaningful metadata. - *

    - * If set to true, the PID records will contain metadata. - * If set to false, the PID records will only contain a pointer to the entity in IDORIS. - */ - private boolean meaningfulPIDRecords = true; - - /** - * Update existing PID records with the latest metadata from the AdministrativeMetadata. - * If set to false, existing PID records will not be updated, - * but new records will still be created with the latest metadata. - */ - private boolean updatePIDRecords = true; - /** * The base URL for the Typed PID Maker service. * This is required when the service is enabled. diff --git a/src/main/java/edu/kit/datamanager/idoris/core/domain/dao/IGenericRepo.java b/src/main/java/edu/kit/datamanager/idoris/core/domain/dao/IGenericRepo.java index 1c8830a..cf71739 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/domain/dao/IGenericRepo.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/domain/dao/IGenericRepo.java @@ -18,13 +18,44 @@ import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.data.neo4j.repository.query.Query; import org.springframework.data.repository.ListCrudRepository; import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; public interface IGenericRepo extends Neo4jRepository, ListCrudRepository, PagingAndSortingRepository { - // This interface serves as a marker for generic repositories. - // It can be extended by specific repositories to inherit common methods. - // Additional methods can be defined here if needed. + /** + * Finds an entity by its PID. + * This method queries the PersistentIdentifier table to find the entity associated with the given PID. + * + * @param pid The PID of the entity to find + * @return An Optional containing the entity if found, or empty if not found + */ + @Query("MATCH (p:PersistentIdentifier {pid: $pid})-[:IDENTIFIES]->(e) RETURN e") + Optional findByPid(@Param("pid") String pid); + + /** + * Finds an entity by its internal ID. + * This method queries the PersistentIdentifier table to find the entity with the given internal ID. + * + * @param internalId The internal ID of the entity to find + * @return An Optional containing the entity if found, or empty if not found + */ + @Query("MATCH (e) WHERE e.internalId = $internalId RETURN e") + Optional findByInternalId(@Param("internalId") String internalId); - T findByPid(String pid); -} \ No newline at end of file + /** + * Finds an entity by its ID, which can be either a PID or an internal ID. + * This method first tries to find the entity by PID, and if not found, tries to find it by internal ID. + * + * @param id The ID of the entity to find (either PID or internal ID) + * @return An Optional containing the entity if found, or empty if not found + */ + @Override + default Optional findById(String id) { + Optional byPid = findByPid(id); + return byPid.isPresent() ? byPid : findByInternalId(id); + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/core/domain/entities/AdministrativeMetadata.java b/src/main/java/edu/kit/datamanager/idoris/core/domain/entities/AdministrativeMetadata.java index 096fd7d..f533de0 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/domain/entities/AdministrativeMetadata.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/domain/entities/AdministrativeMetadata.java @@ -17,13 +17,11 @@ package edu.kit.datamanager.idoris.core.domain.entities; import edu.kit.datamanager.idoris.core.domain.VisitableElement; -import edu.kit.datamanager.idoris.pids.ConfigurablePIDGenerator; import edu.kit.datamanager.idoris.users.entities.User; import lombok.*; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.annotation.Version; -import org.springframework.data.neo4j.core.schema.GeneratedValue; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Relationship; @@ -38,14 +36,6 @@ @AllArgsConstructor(access = AccessLevel.PROTECTED) @Node("IDORIS") public abstract class AdministrativeMetadata extends VisitableElement implements Serializable { - /** - * @deprecated This field is deprecated and will be removed in a future release. - * Use PersistentIdentifierService to get the PID for an entity instead. - */ - @Deprecated - @GeneratedValue(ConfigurablePIDGenerator.class) - String pid; - String name; String description; diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/EventPublisherService.java b/src/main/java/edu/kit/datamanager/idoris/core/events/EventPublisherService.java index 43c79a1..1edf865 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/events/EventPublisherService.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/EventPublisherService.java @@ -107,28 +107,28 @@ public void publishEntityDeleted(Object entity, String entityType) { } /** - * Publishes a PID generated event. + * Publishes an ID generated event. * - * @param entity the entity for which the PID was generated - * @param pid the generated PID - * @param isNewPID indicates whether this is a newly generated PID or an existing one - * @param the type of entity + * @param entity the entity for which the ID was generated + * @param id the generated ID + * @param isNewID indicates whether this is a newly generated ID or an existing one + * @param the type of entity */ - public void publishPIDGenerated(T entity, String pid, boolean isNewPID) { - log.debug("Publishing PIDGeneratedEvent for entity: {}, PID: {}, isNewPID: {}", entity, pid, isNewPID); - eventPublisher.publishEvent(new PIDGeneratedEvent<>(entity, pid, isNewPID)); + public void publishIDGenerated(T entity, String id, boolean isNewID) { + log.debug("Publishing IDGeneratedEvent for entity: {}, ID: {}, isNewID: {}", entity, id, isNewID); + eventPublisher.publishEvent(new PIDGeneratedEvent<>(entity, id, isNewID)); } /** - * Publishes a PID generated event. - * Assumes that the PID is newly generated. + * Publishes an ID generated event. + * Assumes that the ID is newly generated. * - * @param entity the entity for which the PID was generated - * @param pid the generated PID + * @param entity the entity for which the ID was generated + * @param id the generated ID * @param the type of entity */ - public void publishPIDGenerated(T entity, String pid) { - publishPIDGenerated(entity, pid, true); + public void publishIDGenerated(T entity, String id) { + publishIDGenerated(entity, id, true); } /** diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/PIDGeneratedEvent.java b/src/main/java/edu/kit/datamanager/idoris/core/events/PIDGeneratedEvent.java index c44be91..56baaa7 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/events/PIDGeneratedEvent.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/PIDGeneratedEvent.java @@ -21,49 +21,49 @@ import lombok.ToString; /** - * Event that is published when a PID is generated for an entity. - * This event carries the entity and the generated PID, and can be used by listeners - * to perform additional operations like PID record creation, indexing, etc. + * Event that is published when an ID is generated for an entity. + * This event carries the entity and the generated ID, and can be used by listeners + * to perform additional operations like ID record creation, indexing, etc. * - * @param the type of entity for which the PID was generated, must extend AdministrativeMetadata + * @param the type of entity for which the ID was generated, must extend AdministrativeMetadata */ @Getter @ToString(callSuper = true) public class PIDGeneratedEvent extends AbstractDomainEvent { private final T entity; - private final String pid; - private final boolean isNewPID; + private final String id; + private final boolean isNewID; private final String entityInternalId; private final String entityType; /** - * Creates a new PIDGeneratedEvent for the given entity and PID. + * Creates a new PIDGeneratedEvent for the given entity and ID. * - * @param entity the entity for which the PID was generated - * @param pid the generated PID - * @param isNewPID indicates whether this is a newly generated PID or an existing one + * @param entity the entity for which the ID was generated + * @param id the generated ID + * @param isNewID indicates whether this is a newly generated ID or an existing one */ - public PIDGeneratedEvent(T entity, String pid, boolean isNewPID) { + public PIDGeneratedEvent(T entity, String id, boolean isNewID) { this.entity = entity; - this.pid = pid; - this.isNewPID = isNewPID; + this.id = id; + this.isNewID = isNewID; this.entityInternalId = entity.getInternalId(); this.entityType = entity.getClass().getSimpleName(); } /** - * Creates a new PIDGeneratedEvent for the given entity and PID. - * Assumes that the PID is newly generated. + * Creates a new PIDGeneratedEvent for the given entity and ID. + * Assumes that the ID is newly generated. * - * @param entity the entity for which the PID was generated - * @param pid the generated PID + * @param entity the entity for which the ID was generated + * @param id the generated ID */ - public PIDGeneratedEvent(T entity, String pid) { - this(entity, pid, true); + public PIDGeneratedEvent(T entity, String id) { + this(entity, id, true); } /** - * Gets the entity for which the PID was generated. + * Gets the entity for which the ID was generated. * * @return the entity */ @@ -72,25 +72,25 @@ public T getEntity() { } /** - * Gets the generated PID. + * Gets the generated ID. * - * @return the PID + * @return the ID */ - public String getPid() { - return pid; + public String getId() { + return id; } /** - * Indicates whether this is a newly generated PID or an existing one. + * Indicates whether this is a newly generated ID or an existing one. * - * @return true if the PID was newly generated, false if it already existed + * @return true if the ID was newly generated, false if it already existed */ - public boolean isNewPID() { - return isNewPID; + public boolean isNewID() { + return isNewID; } /** - * Gets the internal ID of the entity for which the PID was generated. + * Gets the internal ID of the entity for which the ID was generated. * * @return the entity internal ID */ @@ -99,7 +99,7 @@ public String getEntityInternalId() { } /** - * Gets the type of the entity for which the PID was generated. + * Gets the type of the entity for which the ID was generated. * * @return the entity type */ diff --git a/src/main/java/edu/kit/datamanager/idoris/datatypes/dao/IAtomicDataTypeDao.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/dao/IAtomicDataTypeDao.java index 3588dd4..2ea643a 100644 --- a/src/main/java/edu/kit/datamanager/idoris/datatypes/dao/IAtomicDataTypeDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/dao/IAtomicDataTypeDao.java @@ -20,6 +20,8 @@ import edu.kit.datamanager.idoris.datatypes.entities.AtomicDataType; import org.springframework.data.neo4j.repository.query.Query; +import java.util.Optional; + /** * Repository interface for AtomicDataType entities. */ @@ -31,5 +33,31 @@ public interface IAtomicDataTypeDao extends IGenericRepo { * @return an Iterable of AtomicDataType entities in the inheritance chain */ @Query("MATCH (d:AtomicDataType {pid: $pid})-[:inheritsFrom*]->(d2:AtomicDataType) RETURN d2") - Iterable findAllInInheritanceChain(String pid); -} \ No newline at end of file + Iterable findAllInInheritanceChainByPid(String pid); + + /** + * Finds all AtomicDataType entities in the inheritance chain of the given AtomicDataType. + * + * @param internalId the internal ID of the AtomicDataType + * @return an Iterable of AtomicDataType entities in the inheritance chain + */ + @Query("MATCH (d:AtomicDataType {internalId: $internalId})-[:inheritsFrom*]->(d2:AtomicDataType) RETURN d2") + Iterable findAllInInheritanceChainByInternalId(String internalId); + + /** + * Finds all AtomicDataType entities in the inheritance chain of the given AtomicDataType. + * This method first tries to find the entity by PID, and if not found, tries to find it by internal ID. + * + * @param id the ID of the AtomicDataType (either PID or internal ID) + * @return an Iterable of AtomicDataType entities in the inheritance chain + */ + default Iterable findAllInInheritanceChain(String id) { + // First try to find by PID + Optional byPid = findByPid(id); + if (byPid.isPresent()) { + return findAllInInheritanceChainByPid(id); + } + // If not found by PID, try to find by internal ID + return findAllInInheritanceChainByInternalId(id); + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/datatypes/dao/IDataTypeDao.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/dao/IDataTypeDao.java index 56ae83d..e4ebe37 100644 --- a/src/main/java/edu/kit/datamanager/idoris/datatypes/dao/IDataTypeDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/dao/IDataTypeDao.java @@ -21,6 +21,8 @@ import edu.kit.datamanager.idoris.operations.entities.Operation; import org.springframework.data.neo4j.repository.query.Query; +import java.util.Optional; + /** * Repository interface for DataType entities. */ @@ -32,7 +34,33 @@ public interface IDataTypeDao extends IGenericRepo { * @return an Iterable of DataType entities in the inheritance chain */ @Query("MATCH (d:DataType {pid: $pid})-[:inheritsFrom*]->(d2:DataType) RETURN d2") - Iterable findAllInInheritanceChain(String pid); + Iterable findAllInInheritanceChainByPid(String pid); + + /** + * Finds all DataType entities in the inheritance chain of the given DataType. + * + * @param internalId the internal ID of the DataType + * @return an Iterable of DataType entities in the inheritance chain + */ + @Query("MATCH (d:DataType {internalId: $internalId})-[:inheritsFrom*]->(d2:DataType) RETURN d2") + Iterable findAllInInheritanceChainByInternalId(String internalId); + + /** + * Finds all DataType entities in the inheritance chain of the given DataType. + * This method first tries to find the entity by PID, and if not found, tries to find it by internal ID. + * + * @param id the ID of the DataType (either PID or internal ID) + * @return an Iterable of DataType entities in the inheritance chain + */ + default Iterable findAllInInheritanceChain(String id) { + // First try to find by PID + Optional byPid = findByPid(id); + if (byPid.isPresent()) { + return findAllInInheritanceChainByPid(id); + } + // If not found by PID, try to find by internal ID + return findAllInInheritanceChainByInternalId(id); + } /** * Gets operations that can be executed on a data type. @@ -43,5 +71,35 @@ public interface IDataTypeDao extends IGenericRepo { * @return an Iterable of Operation entities */ @Query("Match (:DataType {pid: $pid})-[:attributes|inheritsFrom*]->(:DataType)<-[:dataType]-(:Attribute)<-[:executableOn]-(o:Operation) return o") - Iterable getOperations(String pid); + Iterable getOperationsByPid(String pid); + + /** + * Gets operations that can be executed on a data type. + * This method finds operations that are executable on the given data type, its attributes, + * or any data type in its inheritance chain. + * + * @param internalId the internal ID of the data type + * @return an Iterable of Operation entities + */ + @Query("Match (:DataType {internalId: $internalId})-[:attributes|inheritsFrom*]->(:DataType)<-[:dataType]-(:Attribute)<-[:executableOn]-(o:Operation) return o") + Iterable getOperationsByInternalId(String internalId); + + /** + * Gets operations that can be executed on a data type. + * This method finds operations that are executable on the given data type, its attributes, + * or any data type in its inheritance chain. + * This method first tries to find the entity by PID, and if not found, tries to find it by internal ID. + * + * @param id the ID of the data type (either PID or internal ID) + * @return an Iterable of Operation entities + */ + default Iterable getOperations(String id) { + // First try to find by PID + Optional byPid = findByPid(id); + if (byPid.isPresent()) { + return getOperationsByPid(id); + } + // If not found by PID, try to find by internal ID + return getOperationsByInternalId(id); + } } diff --git a/src/main/java/edu/kit/datamanager/idoris/datatypes/dao/ITypeProfileDao.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/dao/ITypeProfileDao.java index 79e71dc..460c052 100644 --- a/src/main/java/edu/kit/datamanager/idoris/datatypes/dao/ITypeProfileDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/dao/ITypeProfileDao.java @@ -21,11 +21,53 @@ import io.swagger.v3.oas.annotations.OpenAPIDefinition; import org.springframework.data.neo4j.repository.query.Query; +import java.util.Optional; + @OpenAPIDefinition public interface ITypeProfileDao extends IGenericRepo { @Query("MATCH (d:TypeProfile {pid: $pid})-[i:inheritsFrom*]->(d2:TypeProfile)-[profileAttribute:attributes]->(dataType:DataType) RETURN i, d2, collect(profileAttribute), collect(dataType)") - Iterable findAllTypeProfilesWithTheirAttributesInInheritanceChain(String pid); + Iterable findAllTypeProfilesWithTheirAttributesInInheritanceChainByPid(String pid); + + @Query("MATCH (d:TypeProfile {internalId: $internalId})-[i:inheritsFrom*]->(d2:TypeProfile)-[profileAttribute:attributes]->(dataType:DataType) RETURN i, d2, collect(profileAttribute), collect(dataType)") + Iterable findAllTypeProfilesWithTheirAttributesInInheritanceChainByInternalId(String internalId); + + /** + * Finds all TypeProfile entities with their attributes in the inheritance chain of the given TypeProfile. + * This method first tries to find the entity by PID, and if not found, tries to find it by internal ID. + * + * @param id the ID of the TypeProfile (either PID or internal ID) + * @return an Iterable of TypeProfile entities with their attributes in the inheritance chain + */ + default Iterable findAllTypeProfilesWithTheirAttributesInInheritanceChain(String id) { + // First try to find by PID + Optional byPid = findByPid(id); + if (byPid.isPresent()) { + return findAllTypeProfilesWithTheirAttributesInInheritanceChainByPid(id); + } + // If not found by PID, try to find by internal ID + return findAllTypeProfilesWithTheirAttributesInInheritanceChainByInternalId(id); + } @Query("MATCH (d:TypeProfile {pid: $pid})-[:inheritsFrom*]->(typeProfile:TypeProfile) return typeProfile") - Iterable findAllTypeProfilesInInheritanceChain(String pid); -} \ No newline at end of file + Iterable findAllTypeProfilesInInheritanceChainByPid(String pid); + + @Query("MATCH (d:TypeProfile {internalId: $internalId})-[:inheritsFrom*]->(typeProfile:TypeProfile) return typeProfile") + Iterable findAllTypeProfilesInInheritanceChainByInternalId(String internalId); + + /** + * Finds all TypeProfile entities in the inheritance chain of the given TypeProfile. + * This method first tries to find the entity by PID, and if not found, tries to find it by internal ID. + * + * @param id the ID of the TypeProfile (either PID or internal ID) + * @return an Iterable of TypeProfile entities in the inheritance chain + */ + default Iterable findAllTypeProfilesInInheritanceChain(String id) { + // First try to find by PID + Optional byPid = findByPid(id); + if (byPid.isPresent()) { + return findAllTypeProfilesInInheritanceChainByPid(id); + } + // If not found by PID, try to find by internal ID + return findAllTypeProfilesInInheritanceChainByInternalId(id); + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/datatypes/entities/DataType.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/entities/DataType.java index d575154..4b3e75a 100644 --- a/src/main/java/edu/kit/datamanager/idoris/datatypes/entities/DataType.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/entities/DataType.java @@ -16,8 +16,6 @@ package edu.kit.datamanager.idoris.datatypes.entities; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; import lombok.AllArgsConstructor; import lombok.Getter; @@ -30,14 +28,7 @@ @Setter @AllArgsConstructor @RequiredArgsConstructor -@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(value = AtomicDataType.class, name = "AtomicDataType"), - @JsonSubTypes.Type(value = TypeProfile.class, name = "TypeProfile"), -}) public abstract class DataType extends AdministrativeMetadata { - private TYPES type; - private String defaultValue; public abstract boolean inheritsFrom(DataType dataType); diff --git a/src/main/java/edu/kit/datamanager/idoris/datatypes/services/AtomicDataTypeService.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/services/AtomicDataTypeService.java index a14c1b5..f2a8cd5 100644 --- a/src/main/java/edu/kit/datamanager/idoris/datatypes/services/AtomicDataTypeService.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/services/AtomicDataTypeService.java @@ -57,9 +57,11 @@ public AtomicDataTypeService(IAtomicDataTypeDao atomicDataTypeDao, EventPublishe @Transactional public AtomicDataType createAtomicDataType(AtomicDataType atomicDataType) { log.debug("Creating AtomicDataType: {}", atomicDataType); + atomicDataType.setInternalId(null); + atomicDataType.setVersion(null); AtomicDataType saved = atomicDataTypeDao.save(atomicDataType); eventPublisher.publishEntityCreated(saved); - log.info("Created AtomicDataType with PID: {}", saved.getPid()); + log.info("Created AtomicDataType with PID: {}", saved.getId()); return saved; } @@ -74,50 +76,50 @@ public AtomicDataType createAtomicDataType(AtomicDataType atomicDataType) { public AtomicDataType updateAtomicDataType(AtomicDataType atomicDataType) { log.debug("Updating AtomicDataType: {}", atomicDataType); - if (atomicDataType.getPid() == null || atomicDataType.getPid().isEmpty()) { + if (atomicDataType.getId() == null || atomicDataType.getId().isEmpty()) { throw new IllegalArgumentException("AtomicDataType must have a PID to be updated"); } // Get the current version before updating - AtomicDataType existing = atomicDataTypeDao.findById(atomicDataType.getPid()) - .orElseThrow(() -> new IllegalArgumentException("AtomicDataType not found with PID: " + atomicDataType.getPid())); + AtomicDataType existing = atomicDataTypeDao.findById(atomicDataType.getId()) + .orElseThrow(() -> new IllegalArgumentException("AtomicDataType not found with PID: " + atomicDataType.getId())); Long previousVersion = existing.getVersion(); AtomicDataType saved = atomicDataTypeDao.save(atomicDataType); eventPublisher.publishEntityUpdated(saved, previousVersion); - log.info("Updated AtomicDataType with PID: {}", saved.getPid()); + log.info("Updated AtomicDataType with PID: {}", saved.getId()); return saved; } /** * Deletes an AtomicDataType entity. * - * @param pid the PID of the AtomicDataType to delete + * @param id the PID or internal ID of the AtomicDataType to delete * @throws IllegalArgumentException if the AtomicDataType does not exist */ @Transactional - public void deleteAtomicDataType(String pid) { - log.debug("Deleting AtomicDataType with PID: {}", pid); + public void deleteAtomicDataType(String id) { + log.debug("Deleting AtomicDataType with ID: {}", id); - AtomicDataType atomicDataType = atomicDataTypeDao.findById(pid) - .orElseThrow(() -> new IllegalArgumentException("AtomicDataType not found with PID: " + pid)); + AtomicDataType atomicDataType = atomicDataTypeDao.findById(id) + .orElseThrow(() -> new IllegalArgumentException("AtomicDataType not found with ID: " + id)); atomicDataTypeDao.delete(atomicDataType); eventPublisher.publishEntityDeleted(atomicDataType); - log.info("Deleted AtomicDataType with PID: {}", pid); + log.info("Deleted AtomicDataType with ID: {}", id); } /** - * Retrieves an AtomicDataType entity by its PID. + * Retrieves an AtomicDataType entity by its PID or internal ID. * - * @param pid the PID of the AtomicDataType to retrieve + * @param id the PID or internal ID of the AtomicDataType to retrieve * @return an Optional containing the AtomicDataType, or empty if not found */ @Transactional(readOnly = true) - public Optional getAtomicDataType(String pid) { - log.debug("Retrieving AtomicDataType with PID: {}", pid); - return atomicDataTypeDao.findById(pid); + public Optional getAtomicDataType(String id) { + log.debug("Retrieving AtomicDataType with ID: {}", id); + return atomicDataTypeDao.findById(id); } /** @@ -134,21 +136,21 @@ public List getAllAtomicDataTypes() { /** * Partially updates an existing AtomicDataType entity. * - * @param pid the PID of the AtomicDataType to patch + * @param id the PID or internal ID of the AtomicDataType to patch * @param atomicDataTypePatch the partial AtomicDataType entity with fields to update * @return the patched AtomicDataType entity * @throws IllegalArgumentException if the AtomicDataType does not exist */ @Transactional - public AtomicDataType patchAtomicDataType(String pid, AtomicDataType atomicDataTypePatch) { - log.debug("Patching AtomicDataType with PID: {}, patch: {}", pid, atomicDataTypePatch); - if (pid == null || pid.isEmpty()) { - throw new IllegalArgumentException("AtomicDataType PID cannot be null or empty"); + public AtomicDataType patchAtomicDataType(String id, AtomicDataType atomicDataTypePatch) { + log.debug("Patching AtomicDataType with ID: {}, patch: {}", id, atomicDataTypePatch); + if (id == null || id.isEmpty()) { + throw new IllegalArgumentException("AtomicDataType ID cannot be null or empty"); } // Get the current entity - AtomicDataType existing = atomicDataTypeDao.findById(pid) - .orElseThrow(() -> new IllegalArgumentException("AtomicDataType not found with PID: " + pid)); + AtomicDataType existing = atomicDataTypeDao.findById(id) + .orElseThrow(() -> new IllegalArgumentException("AtomicDataType not found with ID: " + id)); Long previousVersion = existing.getVersion(); // Apply non-null fields from the patch to the existing entity @@ -189,7 +191,7 @@ public AtomicDataType patchAtomicDataType(String pid, AtomicDataType atomicDataT // Publish the patched event eventPublisher.publishEntityPatched(saved, previousVersion); - log.info("Patched AtomicDataType with PID: {}", saved.getPid()); + log.info("Patched AtomicDataType with PID: {}", saved.getId()); return saved; } } diff --git a/src/main/java/edu/kit/datamanager/idoris/datatypes/services/TypeProfileService.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/services/TypeProfileService.java index 59796f8..bd78ad5 100644 --- a/src/main/java/edu/kit/datamanager/idoris/datatypes/services/TypeProfileService.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/services/TypeProfileService.java @@ -60,7 +60,7 @@ public TypeProfile createTypeProfile(TypeProfile typeProfile) { log.debug("Creating TypeProfile: {}", typeProfile); TypeProfile saved = typeProfileDao.save(typeProfile); eventPublisher.publishEntityCreated(saved); - log.info("Created TypeProfile with PID: {}", saved.getPid()); + log.info("Created TypeProfile with PID: {}", saved.getId()); return saved; } @@ -74,45 +74,47 @@ public TypeProfile createTypeProfile(TypeProfile typeProfile) { @Transactional public TypeProfile updateTypeProfile(TypeProfile typeProfile) { log.debug("Updating TypeProfile: {}", typeProfile); - if (typeProfile.getPid() == null || typeProfile.getPid().isEmpty()) { + if (typeProfile.getId() == null || typeProfile.getId().isEmpty()) { throw new IllegalArgumentException("TypeProfile must have a PID to be updated"); } // Get the current version before updating - TypeProfile existing = typeProfileDao.findById(typeProfile.getPid()) - .orElseThrow(() -> new IllegalArgumentException("TypeProfile not found with PID: " + typeProfile.getPid())); + TypeProfile existing = typeProfileDao.findById(typeProfile.getId()) + .orElseThrow(() -> new IllegalArgumentException("TypeProfile not found with PID: " + typeProfile.getId())); Long previousVersion = existing.getVersion(); TypeProfile saved = typeProfileDao.save(typeProfile); eventPublisher.publishEntityUpdated(saved, previousVersion); - log.info("Updated TypeProfile with PID: {}", saved.getPid()); + log.info("Updated TypeProfile with PID: {}", saved.getId()); return saved; } /** * Deletes a TypeProfile entity. * - * @param pid the PID of the TypeProfile to delete + * @param id the PID or internal ID of the TypeProfile to delete * @throws IllegalArgumentException if the TypeProfile does not exist */ @Transactional - public void deleteTypeProfile(String pid) { - log.debug("Deleting TypeProfile with PID: {}", pid); - TypeProfile typeProfile = typeProfileDao.findById(pid) - .orElseThrow(() -> new IllegalArgumentException("TypeProfile not found with PID: " + pid)); + public void deleteTypeProfile(String id) { + log.debug("Deleting TypeProfile with ID: {}", id); + + TypeProfile typeProfile = typeProfileDao.findById(id) + .orElseThrow(() -> new IllegalArgumentException("TypeProfile not found with ID: " + id)); + typeProfileDao.delete(typeProfile); eventPublisher.publishEntityDeleted(typeProfile); - log.info("Deleted TypeProfile with PID: {}", pid); + log.info("Deleted TypeProfile with ID: {}", id); } /** - * Retrieves a TypeProfile entity by its PID. + * Retrieves a TypeProfile entity by its PID or internal ID. * - * @param pid the PID of the TypeProfile to retrieve + * @param id the PID or internal ID of the TypeProfile to retrieve * @return an Optional containing the TypeProfile, or empty if not found */ @Transactional(readOnly = true) - public Optional getTypeProfile(String pid) { - log.debug("Retrieving TypeProfile with PID: {}", pid); - return typeProfileDao.findById(pid); + public Optional getTypeProfile(String id) { + log.debug("Retrieving TypeProfile with ID: {}", id); + return typeProfileDao.findById(id); } /** @@ -129,15 +131,17 @@ public List getAllTypeProfiles() { /** * Validates a TypeProfile entity. * - * @param pid the PID of the TypeProfile to validate + * @param id the PID or internal ID of the TypeProfile to validate * @return the validation result * @throws IllegalArgumentException if the TypeProfile does not exist */ @Transactional(readOnly = true) - public ValidationResult validateTypeProfile(String pid) { - log.debug("Validating TypeProfile with PID: {}", pid); - TypeProfile typeProfile = typeProfileDao.findById(pid) - .orElseThrow(() -> new IllegalArgumentException("TypeProfile not found with PID: " + pid)); + public ValidationResult validateTypeProfile(String id) { + log.debug("Validating TypeProfile with ID: {}", id); + + TypeProfile typeProfile = typeProfileDao.findById(id) + .orElseThrow(() -> new IllegalArgumentException("TypeProfile not found with ID: " + id)); + ValidationPolicyValidator validator = new ValidationPolicyValidator(); return typeProfile.execute(validator); } @@ -145,38 +149,38 @@ public ValidationResult validateTypeProfile(String pid) { /** * Retrieves all TypeProfiles in the inheritance chain of a TypeProfile. * - * @param pid the PID of the TypeProfile + * @param id the PID or internal ID of the TypeProfile * @return an Iterable of TypeProfiles in the inheritance chain * @throws IllegalArgumentException if the TypeProfile does not exist */ @Transactional(readOnly = true) - public Iterable getInheritanceChain(String pid) { - log.debug("Retrieving inheritance chain for TypeProfile with PID: {}", pid); - // Check if the TypeProfile exists - if (!typeProfileDao.existsById(pid)) { - throw new IllegalArgumentException("TypeProfile not found with PID: " + pid); - } - return typeProfileDao.findAllTypeProfilesInInheritanceChain(pid); + public Iterable getInheritanceChain(String id) { + log.debug("Retrieving inheritance chain for TypeProfile with ID: {}", id); + + TypeProfile typeProfile = typeProfileDao.findById(id) + .orElseThrow(() -> new IllegalArgumentException("TypeProfile not found with ID: " + id)); + + return typeProfileDao.findAllTypeProfilesInInheritanceChain(typeProfile.getId()); } /** * Partially updates an existing TypeProfile entity. * - * @param pid the PID of the TypeProfile to patch + * @param id the PID or internal ID of the TypeProfile to patch * @param typeProfilePatch the partial TypeProfile entity with fields to update * @return the patched TypeProfile entity * @throws IllegalArgumentException if the TypeProfile does not exist */ @Transactional - public TypeProfile patchTypeProfile(String pid, TypeProfile typeProfilePatch) { - log.debug("Patching TypeProfile with PID: {}, patch: {}", pid, typeProfilePatch); - if (pid == null || pid.isEmpty()) { - throw new IllegalArgumentException("TypeProfile PID cannot be null or empty"); + public TypeProfile patchTypeProfile(String id, TypeProfile typeProfilePatch) { + log.debug("Patching TypeProfile with ID: {}, patch: {}", id, typeProfilePatch); + if (id == null || id.isEmpty()) { + throw new IllegalArgumentException("TypeProfile ID cannot be null or empty"); } // Get the current entity - TypeProfile existing = typeProfileDao.findById(pid) - .orElseThrow(() -> new IllegalArgumentException("TypeProfile not found with PID: " + pid)); + TypeProfile existing = typeProfileDao.findById(id) + .orElseThrow(() -> new IllegalArgumentException("TypeProfile not found with ID: " + id)); Long previousVersion = existing.getVersion(); // Apply non-null fields from the patch to the existing entity @@ -199,7 +203,7 @@ public TypeProfile patchTypeProfile(String pid, TypeProfile typeProfilePatch) { // Publish the patched event eventPublisher.publishEntityPatched(saved, previousVersion); - log.info("Patched TypeProfile with PID: {}", saved.getPid()); + log.info("Patched TypeProfile with PID: {}", saved.getId()); return saved; } } diff --git a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/api/IAtomicDataTypeApi.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/api/IAtomicDataTypeApi.java index e81599d..7db9e7f 100644 --- a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/api/IAtomicDataTypeApi.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/api/IAtomicDataTypeApi.java @@ -53,15 +53,15 @@ public interface IAtomicDataTypeApi { ResponseEntity>> getAllAtomicDataTypes(); /** - * Gets an AtomicDataType entity by its PID. + * Gets an AtomicDataType entity by its PID or internal ID. * - * @param pid the PID of the AtomicDataType to retrieve + * @param id the PID or internal ID of the AtomicDataType to retrieve * @return the AtomicDataType entity */ - @GetMapping("/{pid}") + @GetMapping("/{id}") @io.swagger.v3.oas.annotations.Operation( - summary = "Get an AtomicDataType by PID", - description = "Returns an AtomicDataType entity by its PID", + summary = "Get an AtomicDataType by PID or internal ID", + description = "Returns an AtomicDataType entity by its PID or internal ID", responses = { @ApiResponse(responseCode = "200", description = "AtomicDataType found", content = @Content(mediaType = "application/hal+json", @@ -70,8 +70,8 @@ public interface IAtomicDataTypeApi { } ) ResponseEntity> getAtomicDataType( - @Parameter(description = "PID of the AtomicDataType", required = true) - @PathVariable String pid); + @Parameter(description = "PID or internal ID of the AtomicDataType", required = true) + @PathVariable String id); /** * Creates a new AtomicDataType entity. @@ -99,11 +99,11 @@ ResponseEntity> createAtomicDataType( * Updates an existing AtomicDataType entity. * The entity is validated before saving. * - * @param pid the PID of the AtomicDataType to update + * @param id the PID or internal ID of the AtomicDataType to update * @param atomicDataType the updated AtomicDataType entity * @return the updated AtomicDataType entity */ - @PutMapping("/{pid}") + @PutMapping("/{id}") @io.swagger.v3.oas.annotations.Operation( summary = "Update an AtomicDataType", description = "Updates an existing AtomicDataType entity after validating it", @@ -116,18 +116,18 @@ ResponseEntity> createAtomicDataType( } ) ResponseEntity> updateAtomicDataType( - @Parameter(description = "PID of the AtomicDataType", required = true) - @PathVariable String pid, + @Parameter(description = "PID or internal ID of the AtomicDataType", required = true) + @PathVariable String id, @Parameter(description = "Updated AtomicDataType", required = true) @Valid @RequestBody AtomicDataType atomicDataType); /** * Deletes an AtomicDataType entity. * - * @param pid the PID of the AtomicDataType to delete + * @param id the PID or internal ID of the AtomicDataType to delete * @return no content */ - @DeleteMapping("/{pid}") + @DeleteMapping("/{id}") @io.swagger.v3.oas.annotations.Operation( summary = "Delete an AtomicDataType", description = "Deletes an AtomicDataType entity", @@ -137,16 +137,16 @@ ResponseEntity> updateAtomicDataType( } ) ResponseEntity deleteAtomicDataType( - @Parameter(description = "PID of the AtomicDataType", required = true) - @PathVariable String pid); + @Parameter(description = "PID or internal ID of the AtomicDataType", required = true) + @PathVariable String id); /** * Gets operations for an AtomicDataType. * - * @param pid the PID of the AtomicDataType + * @param id the PID or internal ID of the AtomicDataType * @return a collection of operations for the AtomicDataType */ - @GetMapping("/{pid}/operations") + @GetMapping("/{id}/operations") @io.swagger.v3.oas.annotations.Operation( summary = "Get operations for an AtomicDataType", description = "Returns a collection of operations that can be executed on an AtomicDataType", @@ -158,17 +158,17 @@ ResponseEntity deleteAtomicDataType( } ) ResponseEntity>> getOperationsForAtomicDataType( - @Parameter(description = "PID of the AtomicDataType", required = true) - @PathVariable String pid); + @Parameter(description = "PID or internal ID of the AtomicDataType", required = true) + @PathVariable String id); /** * Partially updates an AtomicDataType entity. * - * @param pid the PID of the AtomicDataType to patch + * @param id the PID or internal ID of the AtomicDataType to patch * @param atomicDataTypePatch the partial AtomicDataType entity with fields to update * @return the patched AtomicDataType entity */ - @PatchMapping("/{pid}") + @PatchMapping("/{id}") @io.swagger.v3.oas.annotations.Operation( summary = "Partially update an AtomicDataType", description = "Updates specific fields of an existing AtomicDataType entity", @@ -181,8 +181,8 @@ ResponseEntity> patchAtomicDataType( - @Parameter(description = "PID of the AtomicDataType", required = true) - @PathVariable String pid, + @Parameter(description = "PID or internal ID of the AtomicDataType", required = true) + @PathVariable String id, @Parameter(description = "Partial AtomicDataType with fields to update", required = true) @RequestBody AtomicDataType atomicDataTypePatch); } diff --git a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/api/ITypeProfileApi.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/api/ITypeProfileApi.java index 8e0749c..bbdf315 100644 --- a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/api/ITypeProfileApi.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/api/ITypeProfileApi.java @@ -57,15 +57,15 @@ public interface ITypeProfileApi { ResponseEntity>> getAllTypeProfiles(); /** - * Gets a TypeProfile entity by its PID. + * Gets a TypeProfile entity by its PID or internal ID. * - * @param pid the PID of the TypeProfile to retrieve + * @param id the PID or internal ID of the TypeProfile to retrieve * @return the TypeProfile entity */ - @GetMapping("/{pid}") + @GetMapping("/{id}") @io.swagger.v3.oas.annotations.Operation( - summary = "Get a TypeProfile by PID", - description = "Returns a TypeProfile entity by its PID", + summary = "Get a TypeProfile by PID or internal ID", + description = "Returns a TypeProfile entity by its PID or internal ID", responses = { @ApiResponse(responseCode = "200", description = "TypeProfile found", content = @Content(mediaType = "application/hal+json", @@ -74,16 +74,16 @@ public interface ITypeProfileApi { } ) ResponseEntity> getTypeProfile( - @Parameter(description = "PID of the TypeProfile", required = true) - @PathVariable String pid); + @Parameter(description = "PID or internal ID of the TypeProfile", required = true) + @PathVariable String id); /** * Gets operations for a TypeProfile. * - * @param pid the PID of the TypeProfile + * @param id the PID or internal ID of the TypeProfile * @return a collection of operations for the TypeProfile */ - @GetMapping("/{pid}/operations") + @GetMapping("/{id}/operations") @io.swagger.v3.oas.annotations.Operation( summary = "Get operations for a TypeProfile", description = "Returns a collection of operations that can be executed on a TypeProfile", @@ -95,16 +95,16 @@ ResponseEntity> getTypeProfile( } ) ResponseEntity>> getOperationsForTypeProfile( - @Parameter(description = "PID of the TypeProfile", required = true) - @PathVariable String pid); + @Parameter(description = "PID or internal ID of the TypeProfile", required = true) + @PathVariable String id); /** * Validates a TypeProfile entity. * - * @param pid the PID of the TypeProfile to validate + * @param id the PID or internal ID of the TypeProfile to validate * @return the validation result */ - @GetMapping("/{pid}/validate") + @GetMapping("/{id}/validate") @io.swagger.v3.oas.annotations.Operation( summary = "Validate a TypeProfile", description = "Validates a TypeProfile entity and returns the validation result", @@ -115,16 +115,16 @@ ResponseEntity>> getOperationsForTypeProf } ) ResponseEntity validate( - @Parameter(description = "PID of the TypeProfile", required = true) - @PathVariable String pid); + @Parameter(description = "PID or internal ID of the TypeProfile", required = true) + @PathVariable String id); /** * Gets inherited attributes for a TypeProfile. * - * @param pid the PID of the TypeProfile + * @param id the PID or internal ID of the TypeProfile * @return a collection of inherited attributes */ - @GetMapping("/{pid}/inheritedAttributes") + @GetMapping("/{id}/inheritedAttributes") @io.swagger.v3.oas.annotations.Operation( summary = "Get inherited attributes of a TypeProfile", description = "Returns a collection of attributes inherited by a TypeProfile", @@ -136,8 +136,8 @@ ResponseEntity validate( } ) ResponseEntity>> getInheritedAttributes( - @Parameter(description = "PID of the TypeProfile", required = true) - @PathVariable String pid); + @Parameter(description = "PID or internal ID of the TypeProfile", required = true) + @PathVariable String id); /** * Creates a new TypeProfile entity. @@ -165,11 +165,11 @@ ResponseEntity> createTypeProfile( * Updates an existing TypeProfile entity. * The entity is validated before saving. * - * @param pid the PID of the TypeProfile to update + * @param id the PID or internal ID of the TypeProfile to update * @param typeProfile the updated TypeProfile entity * @return the updated TypeProfile entity */ - @PutMapping("/{pid}") + @PutMapping("/{id}") @io.swagger.v3.oas.annotations.Operation( summary = "Update a TypeProfile", description = "Updates an existing TypeProfile entity after validating it", @@ -182,18 +182,18 @@ ResponseEntity> createTypeProfile( } ) ResponseEntity> updateTypeProfile( - @Parameter(description = "PID of the TypeProfile", required = true) - @PathVariable String pid, + @Parameter(description = "PID or internal ID of the TypeProfile", required = true) + @PathVariable String id, @Parameter(description = "Updated TypeProfile", required = true) @Valid @RequestBody TypeProfile typeProfile); /** * Deletes a TypeProfile entity. * - * @param pid the PID of the TypeProfile to delete + * @param id the PID or internal ID of the TypeProfile to delete * @return no content */ - @DeleteMapping("/{pid}") + @DeleteMapping("/{id}") @io.swagger.v3.oas.annotations.Operation( summary = "Delete a TypeProfile", description = "Deletes a TypeProfile entity", @@ -203,16 +203,16 @@ ResponseEntity> updateTypeProfile( } ) ResponseEntity deleteTypeProfile( - @Parameter(description = "PID of the TypeProfile", required = true) - @PathVariable String pid); + @Parameter(description = "PID or internal ID of the TypeProfile", required = true) + @PathVariable String id); /** * Gets the inheritance tree of a TypeProfile. * - * @param pid the PID of the TypeProfile + * @param id the PID or internal ID of the TypeProfile * @return the inheritance tree */ - @GetMapping("/{pid}/inheritanceTree") + @GetMapping("/{id}/inheritanceTree") @io.swagger.v3.oas.annotations.Operation( summary = "Get inheritance tree of a TypeProfile", description = "Returns the inheritance tree of a TypeProfile", @@ -223,17 +223,17 @@ ResponseEntity deleteTypeProfile( } ) ResponseEntity> getInheritanceTree( - @Parameter(description = "PID of the TypeProfile", required = true) - @NotNull @PathVariable String pid); + @Parameter(description = "PID or internal ID of the TypeProfile", required = true) + @NotNull @PathVariable String id); /** * Partially updates a TypeProfile entity. * - * @param pid the PID of the TypeProfile to patch + * @param id the PID or internal ID of the TypeProfile to patch * @param typeProfilePatch the partial TypeProfile entity with fields to update * @return the patched TypeProfile entity */ - @PatchMapping("/{pid}") + @PatchMapping("/{id}") @io.swagger.v3.oas.annotations.Operation( summary = "Partially update a TypeProfile", description = "Updates specific fields of an existing TypeProfile entity", @@ -246,8 +246,8 @@ ResponseEntity> getInheritanceTree( } ) ResponseEntity> patchTypeProfile( - @Parameter(description = "PID of the TypeProfile", required = true) - @PathVariable String pid, + @Parameter(description = "PID or internal ID of the TypeProfile", required = true) + @PathVariable String id, @Parameter(description = "Partial TypeProfile with fields to update", required = true) @RequestBody TypeProfile typeProfilePatch); } diff --git a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/AtomicDataTypeModelAssembler.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/AtomicDataTypeModelAssembler.java index 1121840..2358954 100644 --- a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/AtomicDataTypeModelAssembler.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/AtomicDataTypeModelAssembler.java @@ -42,17 +42,17 @@ public EntityModel toModel(AtomicDataType atomicDataType) { EntityModel entityModel = toModelWithoutLinks(atomicDataType); // Add self link - entityModel.add(linkTo(methodOn(AtomicDataTypeController.class).getAtomicDataType(atomicDataType.getPid())).withSelfRel()); + entityModel.add(linkTo(methodOn(AtomicDataTypeController.class).getAtomicDataType(atomicDataType.getId())).withSelfRel()); // Add link to all atomic data types entityModel.add(linkTo(methodOn(AtomicDataTypeController.class).getAllAtomicDataTypes()).withRel("atomicDataTypes")); // Add link to operations - entityModel.add(linkTo(methodOn(AtomicDataTypeController.class).getOperationsForAtomicDataType(atomicDataType.getPid())).withRel("operations")); + entityModel.add(linkTo(methodOn(AtomicDataTypeController.class).getOperationsForAtomicDataType(atomicDataType.getId())).withRel("operations")); // Add link to inherits from if present if (atomicDataType.getInheritsFrom() != null) { - entityModel.add(linkTo(methodOn(AtomicDataTypeController.class).getAtomicDataType(atomicDataType.getInheritsFrom().getPid())).withRel("inheritsFrom")); + entityModel.add(linkTo(methodOn(AtomicDataTypeController.class).getAtomicDataType(atomicDataType.getInheritsFrom().getId())).withRel("inheritsFrom")); } return entityModel; diff --git a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/DataTypeModelAssembler.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/DataTypeModelAssembler.java index 9d162fb..af9af0f 100644 --- a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/DataTypeModelAssembler.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/DataTypeModelAssembler.java @@ -68,9 +68,9 @@ public EntityModel toModel(DataType dataType) { // Add self link based on the type if (dataType instanceof AtomicDataType) { - entityModel.add(linkTo(methodOn(AtomicDataTypeController.class).getAtomicDataType(dataType.getPid())).withSelfRel()); + entityModel.add(linkTo(methodOn(AtomicDataTypeController.class).getAtomicDataType(dataType.getId())).withSelfRel()); } else if (dataType instanceof TypeProfile) { - entityModel.add(linkTo(methodOn(TypeProfileController.class).getTypeProfile(dataType.getPid())).withSelfRel()); + entityModel.add(linkTo(methodOn(TypeProfileController.class).getTypeProfile(dataType.getId())).withSelfRel()); } return entityModel; @@ -91,7 +91,7 @@ public EntityModel process(EntityModel model) { return model; } - String pid = dataType.getPid(); + String pid = dataType.getId(); // Add link to operations for this data type based on its type if (dataType instanceof AtomicDataType) { diff --git a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/TypeProfileModelAssembler.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/TypeProfileModelAssembler.java index 97d4019..c457e4c 100644 --- a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/TypeProfileModelAssembler.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/hateoas/TypeProfileModelAssembler.java @@ -50,7 +50,7 @@ public EntityModel toModel(TypeProfile typeProfile) { EntityModel entityModel = toModelWithoutLinks(typeProfile); // Add self link - entityModel.add(linkTo(methodOn(TypeProfileController.class).getTypeProfile(typeProfile.getPid())).withSelfRel()); + entityModel.add(linkTo(methodOn(TypeProfileController.class).getTypeProfile(typeProfile.getId())).withSelfRel()); // Add link to all type profiles entityModel.add(linkTo(methodOn(TypeProfileController.class).getAllTypeProfiles()).withRel("typeProfiles")); @@ -72,7 +72,7 @@ public EntityModel process(EntityModel model) { return model; } - String pid = typeProfile.getPid(); + String pid = typeProfile.getId(); // Add link to validate model.add(linkTo(methodOn(TypeProfileController.class).validate(pid)).withRel("validate")); diff --git a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/v1/AtomicDataTypeController.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/v1/AtomicDataTypeController.java index 670de63..c891831 100644 --- a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/v1/AtomicDataTypeController.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/v1/AtomicDataTypeController.java @@ -86,7 +86,7 @@ public AtomicDataTypeController(AtomicDataTypeService atomicDataTypeService, Ope } ) public ResponseEntity>> getAllAtomicDataTypes() { - List> atomicDataTypes = StreamSupport.stream(atomicDataTypeService.getAllAtomicDataTypes().spliterator(), false) + List> atomicDataTypes = atomicDataTypeService.getAllAtomicDataTypes().stream() .map(atomicDataTypeModelAssembler::toModel) .collect(Collectors.toList()); @@ -102,10 +102,10 @@ public ResponseEntity>> getAllAtomic * {@inheritDoc} */ @Override - @GetMapping("/{pid}") + @GetMapping("/{id}") @io.swagger.v3.oas.annotations.Operation( - summary = "Get an AtomicDataType by PID", - description = "Returns an AtomicDataType entity by its PID", + summary = "Get an AtomicDataType by PID or internal ID", + description = "Returns an AtomicDataType entity by its PID or internal ID", responses = { @ApiResponse(responseCode = "200", description = "AtomicDataType found", content = @Content(mediaType = "application/hal+json", @@ -114,9 +114,9 @@ public ResponseEntity>> getAllAtomic } ) public ResponseEntity> getAtomicDataType( - @Parameter(description = "PID of the AtomicDataType", required = true) - @PathVariable String pid) { - return atomicDataTypeService.getAtomicDataType(pid) + @Parameter(description = "PID or internal ID of the AtomicDataType", required = true) + @PathVariable String id) { + return atomicDataTypeService.getAtomicDataType(id) .map(atomicDataTypeModelAssembler::toModel) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); @@ -164,7 +164,7 @@ public ResponseEntity> createAtomicDataType( * {@inheritDoc} */ @Override - @PutMapping("/{pid}") + @PutMapping("/{id}") @io.swagger.v3.oas.annotations.Operation( summary = "Update an AtomicDataType", description = "Updates an existing AtomicDataType entity after validating it", @@ -177,15 +177,23 @@ public ResponseEntity> createAtomicDataType( } ) public ResponseEntity> updateAtomicDataType( - @Parameter(description = "PID of the AtomicDataType", required = true) - @PathVariable String pid, + @Parameter(description = "PID or internal ID of the AtomicDataType", required = true) + @PathVariable String id, @Parameter(description = "Updated AtomicDataType", required = true) @Valid @RequestBody AtomicDataType atomicDataType) { - if (!atomicDataTypeService.getAtomicDataType(pid).isPresent()) { + // Check if the entity exists + if (!atomicDataTypeService.getAtomicDataType(id).isPresent()) { return ResponseEntity.notFound().build(); } - atomicDataType.setPid(pid); + // Get the existing entity to get its PID and internalId + AtomicDataType existing = atomicDataTypeService.getAtomicDataType(id).get(); + + // Set the PID from the existing entity + atomicDataType.setInternalId(existing.getId()); + + // Ensure internal ID is preserved + atomicDataType.setInternalId(existing.getInternalId()); // Validate BEFORE saving ValidationResult validationResult = ruleService.executeRules( @@ -210,7 +218,7 @@ public ResponseEntity> updateAtomicDataType( * {@inheritDoc} */ @Override - @DeleteMapping("/{pid}") + @DeleteMapping("/{id}") @io.swagger.v3.oas.annotations.Operation( summary = "Delete an AtomicDataType", description = "Deletes an AtomicDataType entity", @@ -220,13 +228,13 @@ public ResponseEntity> updateAtomicDataType( } ) public ResponseEntity deleteAtomicDataType( - @Parameter(description = "PID of the AtomicDataType", required = true) - @PathVariable String pid) { - if (!atomicDataTypeService.getAtomicDataType(pid).isPresent()) { + @Parameter(description = "PID or internal ID of the AtomicDataType", required = true) + @PathVariable String id) { + if (!atomicDataTypeService.getAtomicDataType(id).isPresent()) { return ResponseEntity.notFound().build(); } - atomicDataTypeService.deleteAtomicDataType(pid); + atomicDataTypeService.deleteAtomicDataType(id); return ResponseEntity.noContent().build(); } @@ -234,7 +242,7 @@ public ResponseEntity deleteAtomicDataType( * {@inheritDoc} */ @Override - @PatchMapping("/{pid}") + @PatchMapping("/{id}") @io.swagger.v3.oas.annotations.Operation( summary = "Partially update an AtomicDataType", description = "Updates specific fields of an existing AtomicDataType entity", @@ -247,11 +255,11 @@ public ResponseEntity deleteAtomicDataType( } ) public ResponseEntity> patchAtomicDataType( - @Parameter(description = "PID of the AtomicDataType", required = true) - @PathVariable String pid, + @Parameter(description = "PID or internal ID of the AtomicDataType", required = true) + @PathVariable String id, @Parameter(description = "Partial AtomicDataType with fields to update", required = true) @RequestBody AtomicDataType atomicDataTypePatch) { - if (!atomicDataTypeService.getAtomicDataType(pid).isPresent()) { + if (!atomicDataTypeService.getAtomicDataType(id).isPresent()) { return ResponseEntity.notFound().build(); } @@ -265,11 +273,11 @@ public ResponseEntity> patchAtomicDataType( atomicDataTypePatch.getInheritsFrom() != null) { // Get the current entity - AtomicDataType existing = atomicDataTypeService.getAtomicDataType(pid).get(); + AtomicDataType existing = atomicDataTypeService.getAtomicDataType(id).get(); // Create a merged entity for validation AtomicDataType merged = new AtomicDataType(); - merged.setPid(existing.getPid()); + merged.setInternalId(existing.getId()); merged.setName(atomicDataTypePatch.getName() != null ? atomicDataTypePatch.getName() : existing.getName()); merged.setDescription(atomicDataTypePatch.getDescription() != null ? atomicDataTypePatch.getDescription() : existing.getDescription()); merged.setDefaultValue(atomicDataTypePatch.getDefaultValue() != null ? atomicDataTypePatch.getDefaultValue() : existing.getDefaultValue()); @@ -294,7 +302,7 @@ public ResponseEntity> patchAtomicDataType( } } - AtomicDataType patchedAtomicDataType = atomicDataTypeService.patchAtomicDataType(pid, atomicDataTypePatch); + AtomicDataType patchedAtomicDataType = atomicDataTypeService.patchAtomicDataType(id, atomicDataTypePatch); EntityModel entityModel = atomicDataTypeModelAssembler.toModel(patchedAtomicDataType); return ResponseEntity.ok(entityModel); } @@ -303,7 +311,7 @@ public ResponseEntity> patchAtomicDataType( * {@inheritDoc} */ @Override - @GetMapping("/{pid}/operations") + @GetMapping("/{id}/operations") @io.swagger.v3.oas.annotations.Operation( summary = "Get operations for an AtomicDataType", description = "Returns a collection of operations that can be executed on an AtomicDataType", @@ -315,22 +323,22 @@ public ResponseEntity> patchAtomicDataType( } ) public ResponseEntity>> getOperationsForAtomicDataType( - @Parameter(description = "PID of the AtomicDataType", required = true) - @PathVariable String pid) { - if (!atomicDataTypeService.getAtomicDataType(pid).isPresent()) { + @Parameter(description = "PID or internal ID of the AtomicDataType", required = true) + @PathVariable String id) { + if (!atomicDataTypeService.getAtomicDataType(id).isPresent()) { return ResponseEntity.notFound().build(); } - List> operations = StreamSupport.stream(operationService.getOperationsForDataType(pid).spliterator(), false) + List> operations = StreamSupport.stream(operationService.getOperationsForDataType(id).spliterator(), false) .map(operation -> EntityModel.of(operation, - linkTo(methodOn(AtomicDataTypeController.class).getOperationsForAtomicDataType(pid)).withSelfRel(), - linkTo(methodOn(AtomicDataTypeController.class).getAtomicDataType(pid)).withRel("atomicDataType"))) + linkTo(methodOn(AtomicDataTypeController.class).getOperationsForAtomicDataType(id)).withSelfRel(), + linkTo(methodOn(AtomicDataTypeController.class).getAtomicDataType(id)).withRel("atomicDataType"))) .collect(Collectors.toList()); CollectionModel> collectionModel = CollectionModel.of( operations, - linkTo(methodOn(AtomicDataTypeController.class).getOperationsForAtomicDataType(pid)).withSelfRel(), - linkTo(methodOn(AtomicDataTypeController.class).getAtomicDataType(pid)).withRel("atomicDataType") + linkTo(methodOn(AtomicDataTypeController.class).getOperationsForAtomicDataType(id)).withSelfRel(), + linkTo(methodOn(AtomicDataTypeController.class).getAtomicDataType(id)).withRel("atomicDataType") ); return ResponseEntity.ok(collectionModel); diff --git a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/v1/TypeProfileController.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/v1/TypeProfileController.java index 4de8c81..9023ef0 100644 --- a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/v1/TypeProfileController.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/v1/TypeProfileController.java @@ -106,10 +106,10 @@ public ResponseEntity>> getAllTypeProfi * {@inheritDoc} */ @Override - @GetMapping("/{pid}") + @GetMapping("/{id}") @io.swagger.v3.oas.annotations.Operation( - summary = "Get a TypeProfile by PID", - description = "Returns a TypeProfile entity by its PID", + summary = "Get a TypeProfile by PID or internal ID", + description = "Returns a TypeProfile entity by its PID or internal ID", responses = { @ApiResponse(responseCode = "200", description = "TypeProfile found", content = @Content(mediaType = "application/hal+json", @@ -118,9 +118,9 @@ public ResponseEntity>> getAllTypeProfi } ) public ResponseEntity> getTypeProfile( - @Parameter(description = "PID of the TypeProfile", required = true) - @PathVariable String pid) { - return typeProfileService.getTypeProfile(pid) + @Parameter(description = "PID or internal ID of the TypeProfile", required = true) + @PathVariable String id) { + return typeProfileService.getTypeProfile(id) .map(typeProfileModelAssembler::toModel) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); @@ -130,7 +130,7 @@ public ResponseEntity> getTypeProfile( * {@inheritDoc} */ @Override - @GetMapping("/{pid}/operations") + @GetMapping("/{id}/operations") @io.swagger.v3.oas.annotations.Operation( summary = "Get operations for a TypeProfile", description = "Returns a collection of operations that can be executed on a TypeProfile", @@ -142,22 +142,22 @@ public ResponseEntity> getTypeProfile( } ) public ResponseEntity>> getOperationsForTypeProfile( - @Parameter(description = "PID of the TypeProfile", required = true) - @PathVariable String pid) { - if (!typeProfileService.getTypeProfile(pid).isPresent()) { + @Parameter(description = "PID or internal ID of the TypeProfile", required = true) + @PathVariable String id) { + if (!typeProfileService.getTypeProfile(id).isPresent()) { return ResponseEntity.notFound().build(); } - List> operations = StreamSupport.stream(operationService.getOperationsForDataType(pid).spliterator(), false) + List> operations = StreamSupport.stream(operationService.getOperationsForDataType(id).spliterator(), false) .map(operation -> EntityModel.of(operation, - linkTo(methodOn(TypeProfileController.class).getOperationsForTypeProfile(pid)).withSelfRel(), - linkTo(methodOn(TypeProfileController.class).getTypeProfile(pid)).withRel("typeProfile"))) + linkTo(methodOn(TypeProfileController.class).getOperationsForTypeProfile(id)).withSelfRel(), + linkTo(methodOn(TypeProfileController.class).getTypeProfile(id)).withRel("typeProfile"))) .collect(Collectors.toList()); CollectionModel> collectionModel = CollectionModel.of( operations, - linkTo(methodOn(TypeProfileController.class).getOperationsForTypeProfile(pid)).withSelfRel(), - linkTo(methodOn(TypeProfileController.class).getTypeProfile(pid)).withRel("typeProfile") + linkTo(methodOn(TypeProfileController.class).getOperationsForTypeProfile(id)).withSelfRel(), + linkTo(methodOn(TypeProfileController.class).getTypeProfile(id)).withRel("typeProfile") ); return ResponseEntity.ok(collectionModel); @@ -167,7 +167,7 @@ public ResponseEntity>> getOperationsForT * {@inheritDoc} */ @Override - @GetMapping("/{pid}/validate") + @GetMapping("/{id}/validate") @io.swagger.v3.oas.annotations.Operation( summary = "Validate a TypeProfile", description = "Validates a TypeProfile entity and returns the validation result", @@ -178,9 +178,9 @@ public ResponseEntity>> getOperationsForT } ) public ResponseEntity validate( - @Parameter(description = "PID of the TypeProfile", required = true) - @PathVariable String pid) { - ValidationResult result = typeProfileService.validateTypeProfile(pid); + @Parameter(description = "PID or internal ID of the TypeProfile", required = true) + @PathVariable String id) { + ValidationResult result = typeProfileService.validateTypeProfile(id); if (result.isValid()) { return ResponseEntity.ok(result); } else { @@ -192,7 +192,7 @@ public ResponseEntity validate( * {@inheritDoc} */ @Override - @GetMapping("/{pid}/inheritedAttributes") + @GetMapping("/{id}/inheritedAttributes") @io.swagger.v3.oas.annotations.Operation( summary = "Get inherited attributes of a TypeProfile", description = "Returns a collection of attributes inherited by a TypeProfile", @@ -204,21 +204,21 @@ public ResponseEntity validate( } ) public ResponseEntity>> getInheritedAttributes( - @Parameter(description = "PID of the TypeProfile", required = true) - @PathVariable String pid) { - Iterable inheritanceChain = typeProfileService.getInheritanceChain(pid); + @Parameter(description = "PID or internal ID of the TypeProfile", required = true) + @PathVariable String id) { + Iterable inheritanceChain = typeProfileService.getInheritanceChain(id); List> attributes = new ArrayList<>(); inheritanceChain.forEach(typeProfile -> { - typeProfileService.getTypeProfile(typeProfile.getPid()).orElseThrow().getAttributes().forEach(profileAttribute -> { + typeProfileService.getTypeProfile(typeProfile.getId()).orElseThrow().getAttributes().forEach(profileAttribute -> { EntityModel attribute = EntityModel.of(profileAttribute); - attribute.add(linkTo(methodOn(TypeProfileController.class).getTypeProfile(profileAttribute.getDataType().getPid())).withRel("dataType")); + attribute.add(linkTo(methodOn(TypeProfileController.class).getTypeProfile(profileAttribute.getDataType().getId())).withRel("dataType")); attributes.add(attribute); }); }); CollectionModel> resources = CollectionModel.of(attributes); - resources.add(linkTo(methodOn(TypeProfileController.class).getInheritedAttributes(pid)).withSelfRel()); - resources.add(linkTo(methodOn(TypeProfileController.class).getTypeProfile(pid)).withRel("typeProfile")); + resources.add(linkTo(methodOn(TypeProfileController.class).getInheritedAttributes(id)).withSelfRel()); + resources.add(linkTo(methodOn(TypeProfileController.class).getTypeProfile(id)).withRel("typeProfile")); return ResponseEntity.ok(resources); } @@ -264,7 +264,7 @@ public ResponseEntity> createTypeProfile( * {@inheritDoc} */ @Override - @PutMapping("/{pid}") + @PutMapping("/{id}") @io.swagger.v3.oas.annotations.Operation( summary = "Update a TypeProfile", description = "Updates an existing TypeProfile entity after validating it", @@ -277,15 +277,23 @@ public ResponseEntity> createTypeProfile( } ) public ResponseEntity> updateTypeProfile( - @Parameter(description = "PID of the TypeProfile", required = true) - @PathVariable String pid, + @Parameter(description = "PID or internal ID of the TypeProfile", required = true) + @PathVariable String id, @Parameter(description = "Updated TypeProfile", required = true) @Valid @RequestBody TypeProfile typeProfile) { - if (!typeProfileService.getTypeProfile(pid).isPresent()) { + // Check if the entity exists + if (!typeProfileService.getTypeProfile(id).isPresent()) { return ResponseEntity.notFound().build(); } - typeProfile.setPid(pid); + // Get the existing entity to get its PID and internalId + TypeProfile existing = typeProfileService.getTypeProfile(id).get(); + + // Set the PID from the existing entity + typeProfile.setInternalId(existing.getId()); + + // Ensure internal ID is preserved + typeProfile.setInternalId(existing.getInternalId()); // Validate BEFORE saving ValidationResult validationResult = ruleService.executeRules( @@ -309,7 +317,7 @@ public ResponseEntity> updateTypeProfile( * {@inheritDoc} */ @Override - @DeleteMapping("/{pid}") + @DeleteMapping("/{id}") @io.swagger.v3.oas.annotations.Operation( summary = "Delete a TypeProfile", description = "Deletes a TypeProfile entity", @@ -319,13 +327,13 @@ public ResponseEntity> updateTypeProfile( } ) public ResponseEntity deleteTypeProfile( - @Parameter(description = "PID of the TypeProfile", required = true) - @PathVariable String pid) { - if (!typeProfileService.getTypeProfile(pid).isPresent()) { + @Parameter(description = "PID or internal ID of the TypeProfile", required = true) + @PathVariable String id) { + if (!typeProfileService.getTypeProfile(id).isPresent()) { return ResponseEntity.notFound().build(); } - typeProfileService.deleteTypeProfile(pid); + typeProfileService.deleteTypeProfile(id); return ResponseEntity.noContent().build(); } @@ -333,7 +341,7 @@ public ResponseEntity deleteTypeProfile( * {@inheritDoc} */ @Override - @PatchMapping("/{pid}") + @PatchMapping("/{id}") @io.swagger.v3.oas.annotations.Operation( summary = "Partially update a TypeProfile", description = "Updates specific fields of an existing TypeProfile entity", @@ -346,22 +354,22 @@ public ResponseEntity deleteTypeProfile( } ) public ResponseEntity> patchTypeProfile( - @Parameter(description = "PID of the TypeProfile", required = true) - @PathVariable String pid, + @Parameter(description = "PID or internal ID of the TypeProfile", required = true) + @PathVariable String id, @Parameter(description = "Partial TypeProfile with fields to update", required = true) @RequestBody TypeProfile typeProfilePatch) { - if (!typeProfileService.getTypeProfile(pid).isPresent()) { + if (!typeProfileService.getTypeProfile(id).isPresent()) { return ResponseEntity.notFound().build(); } // Validate the patch if it contains fields that need validation if (typeProfilePatch.getAttributes() != null || typeProfilePatch.getInheritsFrom() != null) { // Get the current entity - TypeProfile existing = typeProfileService.getTypeProfile(pid).get(); + TypeProfile existing = typeProfileService.getTypeProfile(id).get(); // Create a merged entity for validation TypeProfile merged = new TypeProfile(); - merged.setPid(existing.getPid()); + merged.setInternalId(existing.getId()); merged.setName(typeProfilePatch.getName() != null ? typeProfilePatch.getName() : existing.getName()); merged.setDescription(typeProfilePatch.getDescription() != null ? typeProfilePatch.getDescription() : existing.getDescription()); merged.setAttributes(typeProfilePatch.getAttributes() != null ? typeProfilePatch.getAttributes() : existing.getAttributes()); @@ -380,7 +388,7 @@ public ResponseEntity> patchTypeProfile( } } - TypeProfile patchedTypeProfile = typeProfileService.patchTypeProfile(pid, typeProfilePatch); + TypeProfile patchedTypeProfile = typeProfileService.patchTypeProfile(id, typeProfilePatch); EntityModel entityModel = typeProfileModelAssembler.toModel(patchedTypeProfile); return ResponseEntity.ok(entityModel); } @@ -389,7 +397,7 @@ public ResponseEntity> patchTypeProfile( * {@inheritDoc} */ @Override - @GetMapping("/{pid}/inheritanceTree") + @GetMapping("/{id}/inheritanceTree") @io.swagger.v3.oas.annotations.Operation( summary = "Get inheritance tree of a TypeProfile", description = "Returns the inheritance tree of a TypeProfile", @@ -400,11 +408,11 @@ public ResponseEntity> patchTypeProfile( } ) public ResponseEntity> getInheritanceTree( - @Parameter(description = "PID of the TypeProfile", required = true) - @NotNull @PathVariable String pid) { - EntityModel resources = buildInheritanceTree(typeProfileService.getTypeProfile(pid).orElseThrow()); - resources.add(linkTo(methodOn(TypeProfileController.class).getInheritanceTree(pid)).withSelfRel()); - resources.add(linkTo(methodOn(TypeProfileController.class).getTypeProfile(pid)).withRel("typeProfile")); + @Parameter(description = "PID or internal ID of the TypeProfile", required = true) + @NotNull @PathVariable String id) { + EntityModel resources = buildInheritanceTree(typeProfileService.getTypeProfile(id).orElseThrow()); + resources.add(linkTo(methodOn(TypeProfileController.class).getInheritanceTree(id)).withSelfRel()); + resources.add(linkTo(methodOn(TypeProfileController.class).getTypeProfile(id)).withRel("typeProfile")); return ResponseEntity.ok(resources); } @@ -419,7 +427,7 @@ private EntityModel buildInheritanceTree(TypeProfile typ List> attributes = new ArrayList<>(); typeProfile.getAttributes().forEach(profileAttribute -> { EntityModel attribute = EntityModel.of(profileAttribute); - attribute.add(linkTo(methodOn(TypeProfileController.class).getTypeProfile(profileAttribute.getDataType().getPid())).withRel("dataType")); + attribute.add(linkTo(methodOn(TypeProfileController.class).getTypeProfile(profileAttribute.getDataType().getId())).withRel("dataType")); attributes.add(attribute); }); @@ -429,17 +437,17 @@ private EntityModel buildInheritanceTree(TypeProfile typ }); EntityModel node = EntityModel.of( - new TypeProfileInheritance(typeProfile.getPid(), + new TypeProfileInheritance(typeProfile.getId(), typeProfile.getName(), typeProfile.getDescription(), CollectionModel.of(attributes), CollectionModel.of(inheritsFrom))); // Add links - node.add(linkTo(methodOn(TypeProfileController.class).getInheritanceTree(typeProfile.getPid())).withRel("inheritanceTree")); - node.add(linkTo(methodOn(TypeProfileController.class).getTypeProfile(typeProfile.getPid())).withRel("typeProfile")); - node.add(linkTo(methodOn(TypeProfileController.class).getInheritedAttributes(typeProfile.getPid())).withRel("inheritedAttributes")); - node.add(linkTo(methodOn(TypeProfileController.class).getOperationsForTypeProfile(typeProfile.getPid())).withRel("operations")); + node.add(linkTo(methodOn(TypeProfileController.class).getInheritanceTree(typeProfile.getId())).withRel("inheritanceTree")); + node.add(linkTo(methodOn(TypeProfileController.class).getTypeProfile(typeProfile.getId())).withRel("typeProfile")); + node.add(linkTo(methodOn(TypeProfileController.class).getInheritedAttributes(typeProfile.getId())).withRel("inheritedAttributes")); + node.add(linkTo(methodOn(TypeProfileController.class).getOperationsForTypeProfile(typeProfile.getId())).withRel("operations")); return node; } @@ -459,7 +467,7 @@ private boolean hasValidationErrors(ValidationResult validationResult) { } public record TypeProfileInheritance( - String pid, + String id, String name, String description, CollectionModel> attributes, diff --git a/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeNotifier.java b/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeNotifier.java deleted file mode 100644 index ed01df2..0000000 --- a/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeNotifier.java +++ /dev/null @@ -1,222 +0,0 @@ -/* - * Copyright (c) 2025 Karlsruhe Institute of Technology - * - * 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 edu.kit.datamanager.idoris.notification; - -import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; -import edu.kit.datamanager.idoris.core.events.EntityCreatedEvent; -import edu.kit.datamanager.idoris.core.events.EntityDeletedEvent; -import edu.kit.datamanager.idoris.core.events.EntityUpdatedEvent; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.event.EventListener; -import org.springframework.stereotype.Component; - -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArraySet; - -/** - * Component that listens for entity change events and notifies subscribers. - * This is a simple implementation of a callback system for entity changes. - * In a real-world scenario, this would likely use a more sophisticated notification mechanism. - */ -@Component -@Slf4j -public class EntityChangeNotifier { - - // Map of entity type to set of subscribers for that type - private final Map> typeSubscribers = new ConcurrentHashMap<>(); - - // Map of entity PID to set of subscribers for that specific entity - private final Map> entitySubscribers = new ConcurrentHashMap<>(); - - /** - * Subscribes to changes for a specific entity type. - * - * @param entityType the entity type to subscribe to - * @param subscriber the subscriber to notify - */ - public void subscribeToType(String entityType, EntityChangeSubscriber subscriber) { - log.debug("Subscribing to changes for entity type: {}", entityType); - typeSubscribers.computeIfAbsent(entityType, k -> new CopyOnWriteArraySet<>()).add(subscriber); - } - - /** - * Subscribes to changes for a specific entity. - * - * @param entityPid the PID of the entity to subscribe to - * @param subscriber the subscriber to notify - */ - public void subscribeToEntity(String entityPid, EntityChangeSubscriber subscriber) { - log.debug("Subscribing to changes for entity with PID: {}", entityPid); - entitySubscribers.computeIfAbsent(entityPid, k -> new CopyOnWriteArraySet<>()).add(subscriber); - } - - /** - * Unsubscribes from changes for a specific entity type. - * - * @param entityType the entity type to unsubscribe from - * @param subscriber the subscriber to remove - */ - public void unsubscribeFromType(String entityType, EntityChangeSubscriber subscriber) { - log.debug("Unsubscribing from changes for entity type: {}", entityType); - Set subscribers = typeSubscribers.get(entityType); - if (subscribers != null) { - subscribers.remove(subscriber); - } - } - - /** - * Unsubscribes from changes for a specific entity. - * - * @param entityPid the PID of the entity to unsubscribe from - * @param subscriber the subscriber to remove - */ - public void unsubscribeFromEntity(String entityPid, EntityChangeSubscriber subscriber) { - log.debug("Unsubscribing from changes for entity with PID: {}", entityPid); - Set subscribers = entitySubscribers.get(entityPid); - if (subscribers != null) { - subscribers.remove(subscriber); - } - } - - /** - * Handles entity created events. - * - * @param event the entity created event - */ - @EventListener - public void handleEntityCreated(EntityCreatedEvent event) { - AdministrativeMetadata entity = event.getEntity(); - String entityType = entity.getClass().getSimpleName(); - String entityPid = entity.getPid(); - - log.debug("Handling EntityCreatedEvent for entity type: {}, PID: {}", entityType, entityPid); - - // Notify type subscribers - Set typeSubscribersSet = typeSubscribers.get(entityType); - if (typeSubscribersSet != null) { - for (EntityChangeSubscriber subscriber : typeSubscribersSet) { - try { - subscriber.onEntityCreated(entity); - } catch (Exception e) { - log.error("Error notifying subscriber for entity creation: {}", e.getMessage(), e); - } - } - } - - // Notify entity subscribers (unlikely for creation, but included for completeness) - // Skip if entityPid is null to avoid NullPointerException - if (entityPid != null) { - Set entitySubscribersSet = entitySubscribers.get(entityPid); - if (entitySubscribersSet != null) { - for (EntityChangeSubscriber subscriber : entitySubscribersSet) { - try { - subscriber.onEntityCreated(entity); - } catch (Exception e) { - log.error("Error notifying subscriber for entity creation: {}", e.getMessage(), e); - } - } - } - } - } - - /** - * Handles entity updated events. - * - * @param event the entity updated event - */ - @EventListener - public void handleEntityUpdated(EntityUpdatedEvent event) { - AdministrativeMetadata entity = event.getEntity(); - String entityType = entity.getClass().getSimpleName(); - String entityPid = entity.getPid(); - - log.debug("Handling EntityUpdatedEvent for entity type: {}, PID: {}", entityType, entityPid); - - // Notify type subscribers - Set typeSubscribersSet = typeSubscribers.get(entityType); - if (typeSubscribersSet != null) { - for (EntityChangeSubscriber subscriber : typeSubscribersSet) { - try { - subscriber.onEntityUpdated(entity, event.getPreviousVersion()); - } catch (Exception e) { - log.error("Error notifying subscriber for entity update: {}", e.getMessage(), e); - } - } - } - - // Notify entity subscribers - // Skip if entityPid is null to avoid NullPointerException - if (entityPid != null) { - Set entitySubscribersSet = entitySubscribers.get(entityPid); - if (entitySubscribersSet != null) { - for (EntityChangeSubscriber subscriber : entitySubscribersSet) { - try { - subscriber.onEntityUpdated(entity, event.getPreviousVersion()); - } catch (Exception e) { - log.error("Error notifying subscriber for entity update: {}", e.getMessage(), e); - } - } - } - } - } - - /** - * Handles entity deleted events. - * - * @param event the entity deleted event - */ - @EventListener - public void handleEntityDeleted(EntityDeletedEvent event) { - AdministrativeMetadata entity = event.getEntity(); - String entityType = event.getEntityType(); - String entityPid = entity.getPid(); - - log.debug("Handling EntityDeletedEvent for entity type: {}, PID: {}", entityType, entityPid); - - // Notify type subscribers - Set typeSubscribersSet = typeSubscribers.get(entityType); - if (typeSubscribersSet != null) { - for (EntityChangeSubscriber subscriber : typeSubscribersSet) { - try { - subscriber.onEntityDeleted(entity); - } catch (Exception e) { - log.error("Error notifying subscriber for entity deletion: {}", e.getMessage(), e); - } - } - } - - // Notify entity subscribers - // Skip if entityPid is null to avoid NullPointerException - if (entityPid != null) { - Set entitySubscribersSet = entitySubscribers.get(entityPid); - if (entitySubscribersSet != null) { - for (EntityChangeSubscriber subscriber : entitySubscribersSet) { - try { - subscriber.onEntityDeleted(entity); - } catch (Exception e) { - log.error("Error notifying subscriber for entity deletion: {}", e.getMessage(), e); - } - } - - // Remove subscribers for this entity since it no longer exists - entitySubscribers.remove(entityPid); - } - } - } -} diff --git a/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeSubscriber.java b/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeSubscriber.java deleted file mode 100644 index 2b70afc..0000000 --- a/src/main/java/edu/kit/datamanager/idoris/notification/EntityChangeSubscriber.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2025 Karlsruhe Institute of Technology - * - * 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 edu.kit.datamanager.idoris.notification; - -import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; - -/** - * Interface for subscribers that want to be notified of entity changes. - * Implementations of this interface can be registered with the EntityChangeNotifier - * to receive callbacks when entities are created, updated, or deleted. - */ -public interface EntityChangeSubscriber { - - /** - * Called when an entity is created. - * - * @param entity the created entity - */ - void onEntityCreated(AdministrativeMetadata entity); - - /** - * Called when an entity is updated. - * - * @param entity the updated entity - * @param previousVersion the version of the entity before the update - */ - void onEntityUpdated(AdministrativeMetadata entity, Long previousVersion); - - /** - * Called when an entity is deleted. - * - * @param entity the deleted entity - */ - void onEntityDeleted(AdministrativeMetadata entity); -} diff --git a/src/main/java/edu/kit/datamanager/idoris/notification/LoggingEntityChangeSubscriber.java b/src/main/java/edu/kit/datamanager/idoris/notification/LoggingEntityChangeSubscriber.java deleted file mode 100644 index a1c076c..0000000 --- a/src/main/java/edu/kit/datamanager/idoris/notification/LoggingEntityChangeSubscriber.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2025 Karlsruhe Institute of Technology - * - * 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 edu.kit.datamanager.idoris.notification; - -import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -/** - * A simple implementation of EntityChangeSubscriber that logs entity changes. - * This class is provided as an example of how to implement the EntityChangeSubscriber interface. - * In a real-world scenario, subscribers might send notifications via email, webhooks, or other channels. - */ -@Component -@Slf4j -public class LoggingEntityChangeSubscriber implements EntityChangeSubscriber { - - /** - * Called when an entity is created. - * Logs information about the created entity. - * - * @param entity the created entity - */ - @Override - public void onEntityCreated(AdministrativeMetadata entity) { - log.info("Entity created: type={}, pid={}, name={}", - entity.getClass().getSimpleName(), - entity.getPid(), - entity.getName()); - } - - /** - * Called when an entity is updated. - * Logs information about the updated entity and its previous version. - * - * @param entity the updated entity - * @param previousVersion the version of the entity before the update - */ - @Override - public void onEntityUpdated(AdministrativeMetadata entity, Long previousVersion) { - log.info("Entity updated: type={}, pid={}, name={}, previousVersion={}, newVersion={}", - entity.getClass().getSimpleName(), - entity.getPid(), - entity.getName(), - previousVersion, - entity.getVersion()); - } - - /** - * Called when an entity is deleted. - * Logs information about the deleted entity. - * - * @param entity the deleted entity - */ - @Override - public void onEntityDeleted(AdministrativeMetadata entity) { - log.info("Entity deleted: type={}, pid={}, name={}", - entity.getClass().getSimpleName(), - entity.getPid(), - entity.getName()); - } -} diff --git a/src/main/java/edu/kit/datamanager/idoris/notification/package-info.java b/src/main/java/edu/kit/datamanager/idoris/notification/package-info.java deleted file mode 100644 index 2faccd1..0000000 --- a/src/main/java/edu/kit/datamanager/idoris/notification/package-info.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2025 Karlsruhe Institute of Technology - * - * 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. - */ - -/** - * Notification module for IDORIS. - * This module is responsible for notifying subscribers about entity changes. - * It provides a callback mechanism for external systems to be notified when entities are created, updated, or deleted. - * - *

    The notification module depends on the core module for event infrastructure and the domain module for entity definitions. - * It listens for entity lifecycle events and notifies subscribers.

    - */ -@org.springframework.modulith.ApplicationModule( - displayName = "IDORIS Notification", - allowedDependencies = {"core", "domain"} -) -package edu.kit.datamanager.idoris.notification; \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/operations/dao/IAttributeMappingDao.java b/src/main/java/edu/kit/datamanager/idoris/operations/dao/IAttributeMappingDao.java index c7fd4a4..b4fa9c3 100644 --- a/src/main/java/edu/kit/datamanager/idoris/operations/dao/IAttributeMappingDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/operations/dao/IAttributeMappingDao.java @@ -34,6 +34,49 @@ public interface IAttributeMappingDao extends Neo4jRepository findByInputAttributePid(String pid); + /** + * Finds AttributeMapping entities by input attribute internal ID. + * + * @param internalId the internal ID of the input attribute + * @return an Iterable of AttributeMapping entities + */ + @Query("MATCH (a:Attribute {internalId: $internalId})<-[:input]-(m:AttributeMapping) RETURN m") + Iterable findByInputAttributeInternalId(String internalId); + + /** + * Finds AttributeMapping entities by input attribute ID (either PID or internal ID). + * This method tries to find mappings using both PID and internal ID. + * + * @param id the ID of the input attribute (either PID or internal ID) + * @return an Iterable of AttributeMapping entities + */ + default Iterable findByInputAttributeId(String id) { + // Try both PID and internal ID + Iterable byPid = findByInputAttributePid(id); + Iterable byInternalId = findByInputAttributeInternalId(id); + + // Combine the results + return () -> { + java.util.Iterator pidIterator = byPid.iterator(); + java.util.Iterator internalIdIterator = byInternalId.iterator(); + + return new java.util.Iterator() { + @Override + public boolean hasNext() { + return pidIterator.hasNext() || internalIdIterator.hasNext(); + } + + @Override + public AttributeMapping next() { + if (pidIterator.hasNext()) { + return pidIterator.next(); + } + return internalIdIterator.next(); + } + }; + }; + } + /** * Finds AttributeMapping entities by output attribute PID. * @@ -42,4 +85,47 @@ public interface IAttributeMappingDao extends Neo4jRepository findByOutputAttributePid(String pid); -} \ No newline at end of file + + /** + * Finds AttributeMapping entities by output attribute internal ID. + * + * @param internalId the internal ID of the output attribute + * @return an Iterable of AttributeMapping entities + */ + @Query("MATCH (a:Attribute {internalId: $internalId})<-[:output]-(m:AttributeMapping) RETURN m") + Iterable findByOutputAttributeInternalId(String internalId); + + /** + * Finds AttributeMapping entities by output attribute ID (either PID or internal ID). + * This method tries to find mappings using both PID and internal ID. + * + * @param id the ID of the output attribute (either PID or internal ID) + * @return an Iterable of AttributeMapping entities + */ + default Iterable findByOutputAttributeId(String id) { + // Try both PID and internal ID + Iterable byPid = findByOutputAttributePid(id); + Iterable byInternalId = findByOutputAttributeInternalId(id); + + // Combine the results + return () -> { + java.util.Iterator pidIterator = byPid.iterator(); + java.util.Iterator internalIdIterator = byInternalId.iterator(); + + return new java.util.Iterator() { + @Override + public boolean hasNext() { + return pidIterator.hasNext() || internalIdIterator.hasNext(); + } + + @Override + public AttributeMapping next() { + if (pidIterator.hasNext()) { + return pidIterator.next(); + } + return internalIdIterator.next(); + } + }; + }; + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/operations/dao/IOperationDao.java b/src/main/java/edu/kit/datamanager/idoris/operations/dao/IOperationDao.java index 4b283e9..9ca9d5b 100644 --- a/src/main/java/edu/kit/datamanager/idoris/operations/dao/IOperationDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/operations/dao/IOperationDao.java @@ -31,7 +31,6 @@ public interface IOperationDao extends IGenericRepo { * @param pid the PID of the data type * @return an Iterable of Operation entities */ - // @Query("optional MATCH (:DataType {pid: $pid})-[:attributes|inheritsFrom*]->(:DataType)<-[:dataType]-(:Attribute)<-[:executableOn]-(o:Operation) RETURN o") @Query(""" MATCH (d:DataType {pid: $pid})<-[:dataType]-(:Attribute)<-[:executableOn]-(o:Operation) RETURN o UNION @@ -42,5 +41,59 @@ public interface IOperationDao extends IGenericRepo { MATCH (d:DataType {pid: $pid})-[:inheritsFrom*]->(:DataType)-[:attributes]->(:Attribute)-[:dataType]->(:DataType)<-[:dataType]-(:Attribute)<-[:executableOn]-(o:Operation) RETURN o UNION MATCH (d:DataType {pid: $pid})-[:inheritsFrom*]->(:DataType)-[:attributes]->(:Attribute)-[:dataType]->(:DataType)-[:inheritsFrom*]->(:DataType)<-[:dataType]-(:Attribute)<-[:executableOn]-(o:Operation) RETURN o""") - Iterable getOperationsForDataType(String pid); -} \ No newline at end of file + Iterable getOperationsForDataTypeByPid(String pid); + + /** + * Gets operations that can be executed on a data type. + * This method finds operations that are executable on the given data type or its attributes. + * + * @param internalId the internal ID of the data type + * @return an Iterable of Operation entities + */ + @Query(""" + MATCH (d:DataType {internalId: $internalId})<-[:dataType]-(:Attribute)<-[:executableOn]-(o:Operation) RETURN o + UNION + MATCH (d:DataType {internalId: $internalId})-[:attributes]->(:Attribute)<-[:executableOn]-(o:Operation) RETURN o + UNION + MATCH (d:DataType {internalId: $internalId})-[:attributes]->(:Attribute)-[:dataType]->(:DataType)<-[:dataType]-(:Attribute)<-[:executableOn]-(o:Operation) RETURN o + UNION + MATCH (d:DataType {internalId: $internalId})-[:inheritsFrom*]->(:DataType)-[:attributes]->(:Attribute)-[:dataType]->(:DataType)<-[:dataType]-(:Attribute)<-[:executableOn]-(o:Operation) RETURN o + UNION + MATCH (d:DataType {internalId: $internalId})-[:inheritsFrom*]->(:DataType)-[:attributes]->(:Attribute)-[:dataType]->(:DataType)-[:inheritsFrom*]->(:DataType)<-[:dataType]-(:Attribute)<-[:executableOn]-(o:Operation) RETURN o""") + Iterable getOperationsForDataTypeByInternalId(String internalId); + + /** + * Gets operations that can be executed on a data type. + * This method finds operations that are executable on the given data type or its attributes. + * This method tries to find operations using both PID and internal ID. + * + * @param id the ID of the data type (either PID or internal ID) + * @return an Iterable of Operation entities + */ + default Iterable getOperationsForDataType(String id) { + // Try both PID and internal ID + Iterable byPid = getOperationsForDataTypeByPid(id); + Iterable byInternalId = getOperationsForDataTypeByInternalId(id); + + // Combine the results + return () -> { + java.util.Iterator pidIterator = byPid.iterator(); + java.util.Iterator internalIdIterator = byInternalId.iterator(); + + return new java.util.Iterator() { + @Override + public boolean hasNext() { + return pidIterator.hasNext() || internalIdIterator.hasNext(); + } + + @Override + public Operation next() { + if (pidIterator.hasNext()) { + return pidIterator.next(); + } + return internalIdIterator.next(); + } + }; + }; + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/operations/services/AttributeMappingService.java b/src/main/java/edu/kit/datamanager/idoris/operations/services/AttributeMappingService.java index 165f1ed..a842b15 100644 --- a/src/main/java/edu/kit/datamanager/idoris/operations/services/AttributeMappingService.java +++ b/src/main/java/edu/kit/datamanager/idoris/operations/services/AttributeMappingService.java @@ -135,22 +135,50 @@ public List getAllAttributeMappings() { * * @param pid the PID of the input attribute * @return a list of AttributeMapping entities + * @deprecated Use {@link #findByInputAttributeId(String)} instead */ @Transactional(readOnly = true) + @Deprecated public List findByInputAttributePid(String pid) { log.debug("Finding AttributeMappings by input attribute PID: {}", pid); return (List) attributeMappingDao.findByInputAttributePid(pid); } + /** + * Finds AttributeMapping entities by input attribute ID (either PID or internal ID). + * + * @param id the ID of the input attribute (either PID or internal ID) + * @return a list of AttributeMapping entities + */ + @Transactional(readOnly = true) + public List findByInputAttributeId(String id) { + log.debug("Finding AttributeMappings by input attribute ID: {}", id); + return (List) attributeMappingDao.findByInputAttributeId(id); + } + /** * Finds AttributeMapping entities by output attribute PID. * * @param pid the PID of the output attribute * @return a list of AttributeMapping entities + * @deprecated Use {@link #findByOutputAttributeId(String)} instead */ @Transactional(readOnly = true) + @Deprecated public List findByOutputAttributePid(String pid) { log.debug("Finding AttributeMappings by output attribute PID: {}", pid); return (List) attributeMappingDao.findByOutputAttributePid(pid); } -} \ No newline at end of file + + /** + * Finds AttributeMapping entities by output attribute ID (either PID or internal ID). + * + * @param id the ID of the output attribute (either PID or internal ID) + * @return a list of AttributeMapping entities + */ + @Transactional(readOnly = true) + public List findByOutputAttributeId(String id) { + log.debug("Finding AttributeMappings by output attribute ID: {}", id); + return (List) attributeMappingDao.findByOutputAttributeId(id); + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/operations/services/OperationService.java b/src/main/java/edu/kit/datamanager/idoris/operations/services/OperationService.java index e642436..1355b59 100644 --- a/src/main/java/edu/kit/datamanager/idoris/operations/services/OperationService.java +++ b/src/main/java/edu/kit/datamanager/idoris/operations/services/OperationService.java @@ -59,7 +59,7 @@ public Operation createOperation(Operation operation) { log.debug("Creating Operation: {}", operation); Operation saved = operationDao.save(operation); eventPublisher.publishEntityCreated(saved); - log.info("Created Operation with PID: {}", saved.getPid()); + log.info("Created Operation with PID: {}", saved.getId()); return saved; } @@ -74,50 +74,50 @@ public Operation createOperation(Operation operation) { public Operation updateOperation(Operation operation) { log.debug("Updating Operation: {}", operation); - if (operation.getPid() == null || operation.getPid().isEmpty()) { + if (operation.getId() == null || operation.getId().isEmpty()) { throw new IllegalArgumentException("Operation must have a PID to be updated"); } // Get the current version before updating - Operation existing = operationDao.findById(operation.getPid()) - .orElseThrow(() -> new IllegalArgumentException("Operation not found with PID: " + operation.getPid())); + Operation existing = operationDao.findById(operation.getId()) + .orElseThrow(() -> new IllegalArgumentException("Operation not found with PID: " + operation.getId())); Long previousVersion = existing.getVersion(); Operation saved = operationDao.save(operation); eventPublisher.publishEntityUpdated(saved, previousVersion); - log.info("Updated Operation with PID: {}", saved.getPid()); + log.info("Updated Operation with PID: {}", saved.getId()); return saved; } /** * Deletes an Operation entity. * - * @param pid the PID of the Operation to delete + * @param id the PID or internal ID of the Operation to delete * @throws IllegalArgumentException if the Operation does not exist */ @Transactional - public void deleteOperation(String pid) { - log.debug("Deleting Operation with PID: {}", pid); + public void deleteOperation(String id) { + log.debug("Deleting Operation with ID: {}", id); - Operation operation = operationDao.findById(pid) - .orElseThrow(() -> new IllegalArgumentException("Operation not found with PID: " + pid)); + Operation operation = operationDao.findById(id) + .orElseThrow(() -> new IllegalArgumentException("Operation not found with ID: " + id)); operationDao.delete(operation); eventPublisher.publishEntityDeleted(operation); - log.info("Deleted Operation with PID: {}", pid); + log.info("Deleted Operation with ID: {}", id); } /** - * Retrieves an Operation entity by its PID. + * Retrieves an Operation entity by its PID or internal ID. * - * @param pid the PID of the Operation to retrieve + * @param id the PID or internal ID of the Operation to retrieve * @return an Optional containing the Operation, or empty if not found */ @Transactional(readOnly = true) - public Optional getOperation(String pid) { - log.debug("Retrieving Operation with PID: {}", pid); - return operationDao.findById(pid); + public Optional getOperation(String id) { + log.debug("Retrieving Operation with ID: {}", id); + return operationDao.findById(id); } /** @@ -134,33 +134,33 @@ public List getAllOperations() { /** * Retrieves all Operations for a DataType. * - * @param dataTypePid the PID of the DataType + * @param dataTypeId the ID of the DataType (either PID or internal ID) * @return an iterable of Operations for the DataType */ @Transactional(readOnly = true) - public Iterable getOperationsForDataType(String dataTypePid) { - log.debug("Retrieving Operations for DataType with PID: {}", dataTypePid); - return operationDao.getOperationsForDataType(dataTypePid); + public Iterable getOperationsForDataType(String dataTypeId) { + log.debug("Retrieving Operations for DataType with ID: {}", dataTypeId); + return operationDao.getOperationsForDataType(dataTypeId); } /** * Partially updates an existing Operation entity. * - * @param pid the PID of the Operation to patch + * @param id the PID or internal ID of the Operation to patch * @param operationPatch the partial Operation entity with fields to update * @return the patched Operation entity * @throws IllegalArgumentException if the Operation does not exist */ @Transactional - public Operation patchOperation(String pid, Operation operationPatch) { - log.debug("Patching Operation with PID: {}, patch: {}", pid, operationPatch); - if (pid == null || pid.isEmpty()) { - throw new IllegalArgumentException("Operation PID cannot be null or empty"); + public Operation patchOperation(String id, Operation operationPatch) { + log.debug("Patching Operation with ID: {}, patch: {}", id, operationPatch); + if (id == null || id.isEmpty()) { + throw new IllegalArgumentException("Operation ID cannot be null or empty"); } // Get the current entity - Operation existing = operationDao.findById(pid) - .orElseThrow(() -> new IllegalArgumentException("Operation not found with PID: " + pid)); + Operation existing = operationDao.findById(id) + .orElseThrow(() -> new IllegalArgumentException("Operation not found with ID: " + id)); Long previousVersion = existing.getVersion(); // Apply non-null fields from the patch to the existing entity @@ -189,7 +189,7 @@ public Operation patchOperation(String pid, Operation operationPatch) { // Publish the patched event eventPublisher.publishEntityPatched(saved, previousVersion); - log.info("Patched Operation with PID: {}", saved.getPid()); + log.info("Patched Operation with PID: {}", saved.getId()); return saved; } } diff --git a/src/main/java/edu/kit/datamanager/idoris/operations/web/api/IOperationApi.java b/src/main/java/edu/kit/datamanager/idoris/operations/web/api/IOperationApi.java index 3d0cd11..20c547d 100644 --- a/src/main/java/edu/kit/datamanager/idoris/operations/web/api/IOperationApi.java +++ b/src/main/java/edu/kit/datamanager/idoris/operations/web/api/IOperationApi.java @@ -53,15 +53,15 @@ public interface IOperationApi { ResponseEntity>> getAllOperations(); /** - * Gets an Operation entity by its PID. + * Gets an Operation entity by its PID or internal ID. * - * @param pid the PID of the Operation to retrieve + * @param id the PID or internal ID of the Operation to retrieve * @return the Operation entity */ - @GetMapping("/{pid}") + @GetMapping("/{id}") @io.swagger.v3.oas.annotations.Operation( - summary = "Get an Operation by PID", - description = "Returns an Operation entity by its PID", + summary = "Get an Operation by PID or internal ID", + description = "Returns an Operation entity by its PID or internal ID", responses = { @ApiResponse(responseCode = "200", description = "Operation found", content = @Content(mediaType = "application/hal+json", @@ -70,8 +70,8 @@ public interface IOperationApi { } ) ResponseEntity> getOperation( - @Parameter(description = "PID of the Operation", required = true) - @PathVariable String pid); + @Parameter(description = "PID or internal ID of the Operation", required = true) + @PathVariable String id); /** * Creates a new Operation entity. @@ -99,11 +99,11 @@ ResponseEntity> createOperation( * Updates an existing Operation entity. * The entity is validated before saving. * - * @param pid the PID of the Operation to update + * @param id the PID or internal ID of the Operation to update * @param operation the updated Operation entity * @return the updated Operation entity */ - @PutMapping("/{pid}") + @PutMapping("/{id}") @io.swagger.v3.oas.annotations.Operation( summary = "Update an Operation", description = "Updates an existing Operation entity after validating it", @@ -116,18 +116,18 @@ ResponseEntity> createOperation( } ) ResponseEntity> updateOperation( - @Parameter(description = "PID of the Operation", required = true) - @PathVariable String pid, + @Parameter(description = "PID or internal ID of the Operation", required = true) + @PathVariable String id, @Parameter(description = "Updated Operation", required = true) @Valid @RequestBody Operation operation); /** * Deletes an Operation entity. * - * @param pid the PID of the Operation to delete + * @param id the PID or internal ID of the Operation to delete * @return no content */ - @DeleteMapping("/{pid}") + @DeleteMapping("/{id}") @io.swagger.v3.oas.annotations.Operation( summary = "Delete an Operation", description = "Deletes an Operation entity", @@ -137,16 +137,16 @@ ResponseEntity> updateOperation( } ) ResponseEntity deleteOperation( - @Parameter(description = "PID of the Operation", required = true) - @PathVariable String pid); + @Parameter(description = "PID or internal ID of the Operation", required = true) + @PathVariable String id); /** * Validates an Operation entity. * - * @param pid the PID of the Operation to validate + * @param id the PID or internal ID of the Operation to validate * @return the validation result */ - @GetMapping("/{pid}/validate") + @GetMapping("/{id}/validate") @io.swagger.v3.oas.annotations.Operation( summary = "Validate an Operation", description = "Validates an Operation entity and returns the validation result", @@ -157,13 +157,13 @@ ResponseEntity deleteOperation( } ) ResponseEntity validate( - @Parameter(description = "PID of the Operation", required = true) - @PathVariable String pid); + @Parameter(description = "PID or internal ID of the Operation", required = true) + @PathVariable String id); /** * Gets operations for a data type. * - * @param pid the PID of the data type + * @param id the PID or internal ID of the data type * @return a collection of operations for the data type */ @GetMapping("/search/getOperationsForDataType") @@ -177,17 +177,17 @@ ResponseEntity validate( } ) ResponseEntity>> getOperationsForDataType( - @Parameter(description = "PID of the data type", required = true) - @RequestParam String pid); + @Parameter(description = "PID or internal ID of the data type", required = true) + @RequestParam String id); /** * Partially updates an Operation entity. * - * @param pid the PID of the Operation to patch + * @param id the PID or internal ID of the Operation to patch * @param operationPatch the partial Operation entity with fields to update * @return the patched Operation entity */ - @PatchMapping("/{pid}") + @PatchMapping("/{id}") @io.swagger.v3.oas.annotations.Operation( summary = "Partially update an Operation", description = "Updates specific fields of an existing Operation entity", @@ -200,8 +200,8 @@ ResponseEntity>> getOperationsForDataType } ) ResponseEntity> patchOperation( - @Parameter(description = "PID of the Operation", required = true) - @PathVariable String pid, + @Parameter(description = "PID or internal ID of the Operation", required = true) + @PathVariable String id, @Parameter(description = "Partial Operation with fields to update", required = true) @RequestBody Operation operationPatch); } diff --git a/src/main/java/edu/kit/datamanager/idoris/operations/web/hateoas/OperationModelAssembler.java b/src/main/java/edu/kit/datamanager/idoris/operations/web/hateoas/OperationModelAssembler.java index fc29d21..67e5c04 100644 --- a/src/main/java/edu/kit/datamanager/idoris/operations/web/hateoas/OperationModelAssembler.java +++ b/src/main/java/edu/kit/datamanager/idoris/operations/web/hateoas/OperationModelAssembler.java @@ -42,14 +42,14 @@ public EntityModel toModel(Operation operation) { EntityModel entityModel = toModelWithoutLinks(operation); // Add self link - entityModel.add(linkTo(methodOn(OperationController.class).getOperation(operation.getPid())).withSelfRel()); + entityModel.add(linkTo(methodOn(OperationController.class).getOperation(operation.getId())).withSelfRel()); // Add link to all operations entityModel.add(linkTo(methodOn(OperationController.class).getAllOperations()).withRel("operations")); // Add link to executable on data type if (operation.getExecutableOn() != null && operation.getExecutableOn().getDataType() != null) { - entityModel.add(linkTo(methodOn(OperationController.class).getOperation(operation.getExecutableOn().getDataType().getPid())).withRel("executableOn")); + entityModel.add(linkTo(methodOn(OperationController.class).getOperation(operation.getExecutableOn().getDataType().getId())).withRel("executableOn")); } return entityModel; diff --git a/src/main/java/edu/kit/datamanager/idoris/operations/web/v1/OperationController.java b/src/main/java/edu/kit/datamanager/idoris/operations/web/v1/OperationController.java index f5c89d5..177b41b 100644 --- a/src/main/java/edu/kit/datamanager/idoris/operations/web/v1/OperationController.java +++ b/src/main/java/edu/kit/datamanager/idoris/operations/web/v1/OperationController.java @@ -89,10 +89,10 @@ public ResponseEntity>> getAllOperations( * {@inheritDoc} */ @Override - @GetMapping("/{pid}") + @GetMapping("/{id}") @io.swagger.v3.oas.annotations.Operation( - summary = "Get an Operation by PID", - description = "Returns an Operation entity by its PID", + summary = "Get an Operation by PID or internal ID", + description = "Returns an Operation entity by its PID or internal ID", responses = { @ApiResponse(responseCode = "200", description = "Operation found", content = @Content(mediaType = "application/hal+json", @@ -101,9 +101,9 @@ public ResponseEntity>> getAllOperations( } ) public ResponseEntity> getOperation( - @Parameter(description = "PID of the Operation", required = true) - @PathVariable String pid) { - return operationService.getOperation(pid) + @Parameter(description = "PID or internal ID of the Operation", required = true) + @PathVariable String id) { + return operationService.getOperation(id) .map(operationModelAssembler::toModel) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); @@ -146,7 +146,7 @@ public ResponseEntity> createOperation( * {@inheritDoc} */ @Override - @PutMapping("/{pid}") + @PutMapping("/{id}") @io.swagger.v3.oas.annotations.Operation( summary = "Update an Operation", description = "Updates an existing Operation entity after validating it", @@ -159,15 +159,23 @@ public ResponseEntity> createOperation( } ) public ResponseEntity> updateOperation( - @Parameter(description = "PID of the Operation", required = true) - @PathVariable String pid, + @Parameter(description = "PID or internal ID of the Operation", required = true) + @PathVariable String id, @Parameter(description = "Updated Operation", required = true) @Valid @RequestBody Operation operation) { - if (!operationService.getOperation(pid).isPresent()) { + // Check if the entity exists + if (!operationService.getOperation(id).isPresent()) { return ResponseEntity.notFound().build(); } - operation.setPid(pid); + // Get the existing entity to get its PID and internalId + Operation existing = operationService.getOperation(id).get(); + + // Set the PID from the existing entity + operation.setInternalId(existing.getId()); + + // Ensure internal ID is preserved + operation.setInternalId(existing.getInternalId()); // Validate the operation using the ValidationPolicyValidator ValidationPolicyValidator validator = new ValidationPolicyValidator(); @@ -188,7 +196,7 @@ public ResponseEntity> updateOperation( * {@inheritDoc} */ @Override - @DeleteMapping("/{pid}") + @DeleteMapping("/{id}") @io.swagger.v3.oas.annotations.Operation( summary = "Delete an Operation", description = "Deletes an Operation entity", @@ -198,13 +206,13 @@ public ResponseEntity> updateOperation( } ) public ResponseEntity deleteOperation( - @Parameter(description = "PID of the Operation", required = true) - @PathVariable String pid) { - if (!operationService.getOperation(pid).isPresent()) { + @Parameter(description = "PID or internal ID of the Operation", required = true) + @PathVariable String id) { + if (!operationService.getOperation(id).isPresent()) { return ResponseEntity.notFound().build(); } - operationService.deleteOperation(pid); + operationService.deleteOperation(id); return ResponseEntity.noContent().build(); } @@ -212,7 +220,7 @@ public ResponseEntity deleteOperation( * {@inheritDoc} */ @Override - @GetMapping("/{pid}/validate") + @GetMapping("/{id}/validate") @io.swagger.v3.oas.annotations.Operation( summary = "Validate an Operation", description = "Validates an Operation entity and returns the validation result", @@ -223,13 +231,13 @@ public ResponseEntity deleteOperation( } ) public ResponseEntity validate( - @Parameter(description = "PID of the Operation", required = true) - @PathVariable String pid) { - if (!operationService.getOperation(pid).isPresent()) { + @Parameter(description = "PID or internal ID of the Operation", required = true) + @PathVariable String id) { + if (!operationService.getOperation(id).isPresent()) { return ResponseEntity.notFound().build(); } - Operation operation = operationService.getOperation(pid).get(); + Operation operation = operationService.getOperation(id).get(); ValidationPolicyValidator validator = new ValidationPolicyValidator(); ValidationResult result = operation.execute(validator); @@ -255,15 +263,15 @@ public ResponseEntity validate( } ) public ResponseEntity>> getOperationsForDataType( - @Parameter(description = "PID of the data type", required = true) - @RequestParam String pid) { - List> operations = StreamSupport.stream(operationService.getOperationsForDataType(pid).spliterator(), false) + @Parameter(description = "PID or internal ID of the data type", required = true) + @RequestParam String id) { + List> operations = StreamSupport.stream(operationService.getOperationsForDataType(id).spliterator(), false) .map(operationModelAssembler::toModel) .collect(Collectors.toList()); CollectionModel> collectionModel = CollectionModel.of( operations, - linkTo(methodOn(OperationController.class).getOperationsForDataType(pid)).withSelfRel() + linkTo(methodOn(OperationController.class).getOperationsForDataType(id)).withSelfRel() ); return ResponseEntity.ok(collectionModel); @@ -273,7 +281,7 @@ public ResponseEntity>> getOperationsForD * {@inheritDoc} */ @Override - @PatchMapping("/{pid}") + @PatchMapping("/{id}") @io.swagger.v3.oas.annotations.Operation( summary = "Partially update an Operation", description = "Updates specific fields of an existing Operation entity", @@ -286,11 +294,11 @@ public ResponseEntity>> getOperationsForD } ) public ResponseEntity> patchOperation( - @Parameter(description = "PID of the Operation", required = true) - @PathVariable String pid, + @Parameter(description = "PID or internal ID of the Operation", required = true) + @PathVariable String id, @Parameter(description = "Partial Operation with fields to update", required = true) @RequestBody Operation operationPatch) { - if (!operationService.getOperation(pid).isPresent()) { + if (!operationService.getOperation(id).isPresent()) { return ResponseEntity.notFound().build(); } @@ -301,11 +309,11 @@ public ResponseEntity> patchOperation( operationPatch.getExecution() != null) { // Get the current entity - Operation existing = operationService.getOperation(pid).get(); + Operation existing = operationService.getOperation(id).get(); // Create a merged entity for validation Operation merged = new Operation(); - merged.setPid(existing.getPid()); + merged.setInternalId(existing.getId()); merged.setName(operationPatch.getName() != null ? operationPatch.getName() : existing.getName()); merged.setDescription(operationPatch.getDescription() != null ? operationPatch.getDescription() : existing.getDescription()); merged.setExecutableOn(operationPatch.getExecutableOn() != null ? operationPatch.getExecutableOn() : existing.getExecutableOn()); @@ -323,7 +331,7 @@ public ResponseEntity> patchOperation( } } - Operation patchedOperation = operationService.patchOperation(pid, operationPatch); + Operation patchedOperation = operationService.patchOperation(id, operationPatch); EntityModel entityModel = operationModelAssembler.toModel(patchedOperation); return ResponseEntity.ok(entityModel); } diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/ConfigurablePIDGenerator.java b/src/main/java/edu/kit/datamanager/idoris/pids/ConfigurablePIDGenerator.java index 3f567d9..6a2b849 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/ConfigurablePIDGenerator.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/ConfigurablePIDGenerator.java @@ -24,8 +24,6 @@ import org.springframework.data.neo4j.core.schema.IdGenerator; import org.springframework.stereotype.Component; -import java.util.UUID; - /** * A configurable PID generator that delegates to either TypedPIDMakerIDGenerator * or LocalUUIDPIDGenerator based on the 'idoris.pid-generation' application property. @@ -55,13 +53,6 @@ public ConfigurablePIDGenerator(ApplicationProperties applicationProperties, */ @Override public String generateId(String primaryLabel, Object entity) { - ApplicationProperties.PIDGeneration strategy = applicationProperties.getPidGeneration(); - log.debug("PID generation strategy determined as: {}", strategy); - - if (strategy == null) { - log.error("PID generation strategy not configured"); - throw new IllegalArgumentException("PID generation strategy is not set in application properties."); - } // Validate inputs if (primaryLabel.isEmpty()) { @@ -73,28 +64,14 @@ public String generateId(String primaryLabel, Object entity) { throw new IllegalArgumentException("Entity must be a non-null instance of AdministrativeMetadata."); } - switch (strategy) { - case TYPED_PID_MAKER -> { - TypedPIDMakerIDGenerator typedGenerator = typedPidMakerProvider.getIfAvailable(); + TypedPIDMakerIDGenerator typedGenerator = typedPidMakerProvider.getIfAvailable(); - if (typedGenerator != null) { - log.debug("Using TypedPIDMakerIDGenerator for entity labeled '{}'", primaryLabel); - return typedGenerator.generateId(primaryLabel, entity); - } else { - log.error("PID generation strategy is TYPED_PID_MAKER, but TypedPIDMakerIDGenerator bean is not available."); - throw new IllegalStateException("TypedPIDMakerIDGenerator bean is not available. Check your configuration."); - } - } - case LOCAL -> { - log.error("PID generation strategy is LOCAL, generating UUID."); - String pid = UUID.randomUUID().toString(); - log.debug("Generated UUID PID: {}", pid); - return pid; - } - default -> { - log.warn("Unsupported PID generation strategy: {}.", strategy); - throw new IllegalStateException("Unsupported PID generation strategy: " + strategy + ". Please check your configuration."); - } + if (typedGenerator != null) { + log.debug("Using TypedPIDMakerIDGenerator for entity labeled '{}'", primaryLabel); + return typedGenerator.generateId(primaryLabel, entity); + } else { + log.error("PID generation strategy is TYPED_PID_MAKER, but TypedPIDMakerIDGenerator bean is not available."); + throw new IllegalStateException("TypedPIDMakerIDGenerator bean is not available. Check your configuration."); } } } diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/MetadataEventListener.java b/src/main/java/edu/kit/datamanager/idoris/pids/MetadataEventListener.java index 1632f63..fc2de08 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/MetadataEventListener.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/MetadataEventListener.java @@ -31,7 +31,7 @@ import java.util.Optional; /** - * Event listener that generates PIDs for newly created entities. + * Event listener that generates IDs for newly created entities. * This listener subscribes to EntityCreatedEvent and uses the PersistentIdentifierService * to create PersistentIdentifier entities for newly created AdministrativeMetadata entities. */ @@ -54,7 +54,7 @@ public MetadataEventListener(PersistentIdentifierService pidService, EventPublis /** * Handles EntityCreatedEvent by creating a PersistentIdentifier for the entity. - * This method is executed in a new transaction to ensure that the PID creation is isolated + * This method is executed in a new transaction to ensure that the ID creation is isolated * from the transaction that created the entity. * * @param event the entity created event @@ -76,15 +76,15 @@ public void handleEntityCreatedEvent(EntityCreatedEvent // Create a new PersistentIdentifier for the entity log.info("Creating PersistentIdentifier for entity: {}", entity); PersistentIdentifier pid = pidService.createPersistentIdentifier(entity); - log.info("Created PersistentIdentifier with PID: {} for entity: {}", pid.getPid(), entity); + log.info("Created PersistentIdentifier with ID: {} for entity: {}", pid.getPid(), entity); - // Publish a PID generated event - eventPublisher.publishPIDGenerated(entity, pid.getPid()); + // Publish an ID generated event + eventPublisher.publishIDGenerated(entity, pid.getPid()); } /** * Handles EntityUpdatedEvent by updating the PersistentIdentifier for the entity. - * This method is executed in a new transaction to ensure that the PID update is isolated + * This method is executed in a new transaction to ensure that the ID update is isolated * from the transaction that updated the entity. * * @param event the entity updated event @@ -102,7 +102,7 @@ public void handleEntityUpdatedEvent(EntityUpdatedEvent PersistentIdentifier pid = existingPid.get(); log.info("Updating PersistentIdentifier for entity: {}", entity); pidService.updatePIDRecord(pid); - log.info("Updated PersistentIdentifier with PID: {} for entity: {}", pid.getPid(), entity); + log.info("Updated PersistentIdentifier with ID: {} for entity: {}", pid.getPid(), entity); } else { log.warn("No PersistentIdentifier found for entity, cannot update: {}", entity); } @@ -127,7 +127,7 @@ public void handleEntityDeletedEvent(EntityDeletedEvent if (optionalPid.isPresent()) { PersistentIdentifier pid = optionalPid.get(); - log.info("Created tombstone for entity with PID: {}", pid.getPid()); + log.info("Created tombstone for entity with ID: {}", pid.getPid()); } else { log.warn("No PersistentIdentifier found for entity, cannot create tombstone: {}", entity); } diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClient.java b/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClient.java index 58ae367..a90ab1f 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClient.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClient.java @@ -17,6 +17,9 @@ package edu.kit.datamanager.idoris.pids.client; import edu.kit.datamanager.idoris.pids.client.model.PIDRecord; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.service.annotation.GetExchange; import org.springframework.web.service.annotation.HttpExchange; import org.springframework.web.service.annotation.PostExchange; @@ -36,7 +39,8 @@ public interface TypedPIDMakerClient { * @return The created PID record */ @PostExchange(value = "/", accept = "application/vnd.datamanager.pid.simple+json", contentType = "application/vnd.datamanager.pid.simple+json") - PIDRecord createPIDRecord(PIDRecord record); + @ResponseBody + PIDRecord createPIDRecord(@RequestBody PIDRecord record); /** @@ -46,7 +50,8 @@ public interface TypedPIDMakerClient { * @return The PID record */ @GetExchange(value = "/{pid}", accept = "application/vnd.datamanager.pid.simple+json") - PIDRecord getPIDRecord(String pid); + @ResponseBody + PIDRecord getPIDRecord(@PathVariable String pid); /** * Updates an existing PID record using the SimplePidRecord format. @@ -56,5 +61,6 @@ public interface TypedPIDMakerClient { * @return The updated PID record */ @PutExchange(value = "/{pid}", accept = "application/vnd.datamanager.pid.simple+json", contentType = "application/vnd.datamanager.pid.simple+json") - PIDRecord updatePIDRecord(String pid, PIDRecord record); + @ResponseBody + PIDRecord updatePIDRecord(@PathVariable String pid, @RequestBody PIDRecord record); } diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClientConfig.java b/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClientConfig.java index 05b9214..70cb2f8 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClientConfig.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClientConfig.java @@ -16,10 +16,13 @@ package edu.kit.datamanager.idoris.pids.client; +import com.fasterxml.jackson.databind.ObjectMapper; import edu.kit.datamanager.idoris.configuration.TypedPIDMakerConfig; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.client.RestClient; import org.springframework.web.client.support.RestClientAdapter; import org.springframework.web.service.invoker.HttpServiceProxyFactory; @@ -39,7 +42,7 @@ public class TypedPIDMakerClientConfig { * @return The TypedPIDMakerClient */ @Bean - public TypedPIDMakerClient typedPIDMakerClient(TypedPIDMakerConfig config) { + public TypedPIDMakerClient typedPIDMakerClient(TypedPIDMakerConfig config, ObjectMapper objectMapper) { // Create a client HTTP request factory with the configured timeout org.springframework.http.client.ClientHttpRequestFactory requestFactory = new org.springframework.http.client.SimpleClientHttpRequestFactory(); @@ -49,7 +52,17 @@ public TypedPIDMakerClient typedPIDMakerClient(TypedPIDMakerConfig config) { .requestFactory(requestFactory) .defaultHeaders(headers -> headers.setAccept(java.util.List.of(org.springframework.http.MediaType.parseMediaType("application/vnd.datamanager.pid.simple+json")))) .defaultStatusHandler(org.springframework.http.HttpStatusCode::isError, (request, response) -> { - throw new org.springframework.web.client.RestClientException("Error response: " + response.getStatusCode()); + throw new org.springframework.web.client.RestClientException(String.format("Error response from Typed PID Maker: %s %s", response.getStatusCode(), response.getStatusText())); + }) + .messageConverters(converters -> { + // Add a custom converter for application/octet-stream to handle binary data + converters.add(new MappingJackson2HttpMessageConverter(objectMapper) { + @Override + protected boolean canRead(MediaType mediaType) { + return super.canRead(mediaType) || + MediaType.APPLICATION_OCTET_STREAM.isCompatibleWith(mediaType); + } + }); }) .build(); diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/client/model/PIDRecord.java b/src/main/java/edu/kit/datamanager/idoris/pids/client/model/PIDRecord.java index 3080d56..9d504a2 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/client/model/PIDRecord.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/client/model/PIDRecord.java @@ -17,13 +17,50 @@ package edu.kit.datamanager.idoris.pids.client.model; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.With; +import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * Represents a Simple PID Record in the Typed PID Maker service. * This follows the SimplePidRecord structure from the Typed PID Maker API. */ @JsonIgnoreProperties(ignoreUnknown = true) +@With public record PIDRecord(String pid, List record) { + + /** + * Constructs a PIDRecord with the given PID and an empty record. + * If the PID is null, it defaults to an empty string. + * + * @param pid The PID of the record + */ + public PIDRecord(String pid) { + this(pid, new ArrayList<>()); + } + + /** + * Constructs a PIDRecord with an empty PID and the given record entries. + * If the record is null, it initializes an empty list. + * + * @param record The list of PIDRecordEntry entries + */ + public PIDRecord(List record) { + this("", Objects.requireNonNullElseGet(record, ArrayList::new)); + } + + /** + * Constructs a PIDRecord with the given PID and record entries. + * If the PID is null, it defaults to an empty string. + * If the record is null, it initializes an empty list. + * + * @param pid The PID of the record + * @param record The list of PIDRecordEntry entries + */ + public PIDRecord(String pid, List record) { + this.pid = Objects.requireNonNullElse(pid, ""); + this.record = Objects.requireNonNullElseGet(record, ArrayList::new); + } } diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/entities/PersistentIdentifier.java b/src/main/java/edu/kit/datamanager/idoris/pids/entities/PersistentIdentifier.java index d9aba19..abbbd78 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/entities/PersistentIdentifier.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/entities/PersistentIdentifier.java @@ -26,8 +26,6 @@ import org.springframework.data.neo4j.core.schema.Relationship; import java.time.Instant; -import java.util.HashMap; -import java.util.Map; /** * Entity representing a Persistent Identifier (PID) in the system. @@ -93,55 +91,6 @@ public class PersistentIdentifier { @Relationship(value = "IDENTIFIES", direction = Relationship.Direction.OUTGOING) private AdministrativeMetadata entity; - /** - * Additional metadata stored in the PID record. - * This is a map of key-value pairs that can be used to store any additional information. - */ - private Map metadata = new HashMap<>(); - - /** - * Adds a metadata entry to this PID record. - * - * @param key The key of the metadata entry - * @param value The value of the metadata entry - * @return This PID record for method chaining - */ - public PersistentIdentifier addMetadata(String key, String value) { - metadata.put(key, value); - return this; - } - - /** - * Removes a metadata entry from this PID record. - * - * @param key The key of the metadata entry to remove - * @return This PID record for method chaining - */ - public PersistentIdentifier removeMetadata(String key) { - metadata.remove(key); - return this; - } - - /** - * Clears all metadata entries from this PID record. - * - * @return This PID record for method chaining - */ - public PersistentIdentifier clearMetadata() { - metadata.clear(); - return this; - } - - /** - * Gets the value of a metadata entry. - * - * @param key The key of the metadata entry - * @return The value of the metadata entry, or null if the key does not exist - */ - public String getMetadataValue(String key) { - return metadata.get(key); - } - /** * Marks this PID record as a tombstone, indicating that the entity it identifies has been deleted. * diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/repositories/PersistentIdentifierRepository.java b/src/main/java/edu/kit/datamanager/idoris/pids/repositories/PersistentIdentifierRepository.java index 9f9702b..55505e0 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/repositories/PersistentIdentifierRepository.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/repositories/PersistentIdentifierRepository.java @@ -19,8 +19,6 @@ import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; import edu.kit.datamanager.idoris.pids.entities.PersistentIdentifier; import org.springframework.data.neo4j.repository.Neo4jRepository; -import org.springframework.data.neo4j.repository.query.Query; -import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; @@ -70,14 +68,4 @@ public interface PersistentIdentifierRepository extends Neo4jRepository findByTombstoneFalse(); - - /** - * Finds all PersistentIdentifiers that have a metadata entry with the given key and value. - * - * @param key The key of the metadata entry - * @param value The value of the metadata entry - * @return A list of PersistentIdentifiers that have a metadata entry with the given key and value - */ - @Query("MATCH (p:PersistentIdentifier) WHERE p.metadata[$key] = $value RETURN p") - List findByMetadata(@Param("key") String key, @Param("value") String value); } \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/services/PersistentIdentifierService.java b/src/main/java/edu/kit/datamanager/idoris/pids/services/PersistentIdentifierService.java index 7c7ca9e..e406a9a 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/services/PersistentIdentifierService.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/services/PersistentIdentifierService.java @@ -20,6 +20,7 @@ import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; import edu.kit.datamanager.idoris.pids.client.TypedPIDMakerClient; import edu.kit.datamanager.idoris.pids.client.model.PIDRecord; +import edu.kit.datamanager.idoris.pids.client.model.PIDRecordEntry; import edu.kit.datamanager.idoris.pids.entities.PersistentIdentifier; import edu.kit.datamanager.idoris.pids.repositories.PersistentIdentifierRepository; import edu.kit.datamanager.idoris.pids.utils.PIDRecordMapper; @@ -30,6 +31,7 @@ import java.time.Instant; import java.util.List; +import java.util.Objects; import java.util.Optional; /** @@ -83,28 +85,46 @@ public PersistentIdentifier createPersistentIdentifier(AdministrativeMetadata en return existingPid.get(); } - // Create a new PID record in the Typed PID Maker service - PIDRecord record = mapper.createEmptyPIDRecord(); - PIDRecord createdRecord = client.createPIDRecord(record); - - // Create a new PersistentIdentifier entity - PersistentIdentifier pid = PersistentIdentifier.builder() - .pid(createdRecord.pid()) + // Create a temporary PID entity to use with the mapper + PersistentIdentifier tempPid = PersistentIdentifier.builder() + .pid(null) .entityType(entity.getClass().getSimpleName()) .entityInternalId(entity.getInternalId()) .entity(entity) .tombstone(false) .build(); - // Save the PersistentIdentifier entity - PersistentIdentifier savedPid = repository.save(pid); + // Use the mapper to create a PID record with administrative metadata + PIDRecord record = mapper.toPIDRecord(tempPid); - // Update the PID record with metadata if configured to do so - if (config.isMeaningfulPIDRecords()) { - updatePIDRecord(savedPid); - } + // Create the PID record in the Typed PID Maker service + PIDRecord createdRecord = client.createPIDRecord(record); - log.info("Created PersistentIdentifier: {}", savedPid); + log.debug("Created first PID record: {}", createdRecord); + + // Set the PID in the temporary PersistentIdentifier entity + tempPid.setPid(createdRecord.pid()); + + // Save the PersistentIdentifier entity + PersistentIdentifier savedPid = repository.save(tempPid); + + // Update the entity with the saved PersistentIdentifier + List entries = createdRecord.record().stream() + .map(entry -> { + if (Objects.equals(entry.key(), "21.T11148/b8457812905b83046284")) { + // Update the DO location to point to the saved PID + String doLocation = String.format("%s/pid/%s", config.getBaseUrl(), createdRecord.pid()); + return new PIDRecordEntry(entry.key(), doLocation); + } + return entry; + }) + .toList(); + PIDRecord updatedRecord = new PIDRecord(createdRecord.pid(), entries); + // Update the PID record in the Typed PID Maker service with the saved PID + log.debug("Updating PID record with saved PID: {}", updatedRecord); + client.updatePIDRecord(savedPid.getPid(), updatedRecord); + + log.info("Created PersistentIdentifier: {} with record", savedPid); return savedPid; } @@ -119,12 +139,6 @@ public PersistentIdentifier createPersistentIdentifier(AdministrativeMetadata en public PersistentIdentifier updatePIDRecord(PersistentIdentifier pid) { log.debug("Updating PID record for PersistentIdentifier: {}", pid); - // Skip update if not configured to do so - if (!config.isUpdatePIDRecords()) { - log.debug("Skipping PID record update because updatePIDRecords is false"); - return pid; - } - // Create a PID record with metadata from the entity PIDRecord record = mapper.toPIDRecord(pid); @@ -220,4 +234,4 @@ public List getTombstones() { log.debug("Getting tombstone PersistentIdentifiers"); return repository.findByTombstoneTrue(); } -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/utils/PIDRecordMapper.java b/src/main/java/edu/kit/datamanager/idoris/pids/utils/PIDRecordMapper.java index c9576ef..9994937 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/utils/PIDRecordMapper.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/utils/PIDRecordMapper.java @@ -28,10 +28,12 @@ import org.springframework.stereotype.Component; import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.time.Instant; import java.util.ArrayList; import java.util.List; -import java.util.Map; /** * Utility class for mapping between PersistentIdentifier entities and PIDRecord objects. @@ -56,16 +58,6 @@ public PIDRecordMapper(ApplicationProperties applicationProperties, TypedPIDMake this.config = config; } - /** - * Creates an empty PIDRecord with no entries. - * This is used when creating a new PID record in the Typed PID Maker service. - * - * @return An empty PIDRecord - */ - public PIDRecord createEmptyPIDRecord() { - return new PIDRecord("", new ArrayList<>()); - } - /** * Converts a PersistentIdentifier to a PIDRecord. * This method creates a PIDRecord with metadata from the PersistentIdentifier and its associated entity. @@ -75,6 +67,10 @@ public PIDRecord createEmptyPIDRecord() { */ public PIDRecord toPIDRecord(PersistentIdentifier pid) { List recordEntries = new ArrayList<>(); + AdministrativeMetadata entity = pid.getEntity(); + + // Helmholtz Kernel Information Profile + recordEntries.add(new PIDRecordEntry("21.T11148/076759916209e5d62bd5", "21.T11148/b9b76f887845e32d29f7")); // Always add a pointer to the entity String baseUrl = getBaseUrl(); @@ -83,45 +79,37 @@ public PIDRecord toPIDRecord(PersistentIdentifier pid) { if (pid.isTombstone()) { // For tombstones, use a special URL that indicates the entity has been deleted doLocation = String.format("%s/tombstone/%s", baseUrl, pid.getPid()); - recordEntries.add(new PIDRecordEntry("tombstone", "true")); - recordEntries.add(new PIDRecordEntry("deletedAt", pid.getDeletedAt().toString())); + recordEntries.add(new PIDRecordEntry("21.T11148/d1ec8ccbfa6de41da894", "TOMBSTONE")); //TODO: Add more tombstone information +// recordEntries.add(new PIDRecordEntry("deletedAt", pid.getDeletedAt().toString())); } else { // For active entities, use a URL that points to the entity doLocation = String.format("%s/pid/%s", baseUrl, pid.getPid()); } log.debug("Using DO location: {}", doLocation); - recordEntries.add(new PIDRecordEntry("digitalObjectLocation", doLocation)); - - // Add entity type information - recordEntries.add(new PIDRecordEntry("entityType", pid.getEntityType())); - - // Add custom metadata from the PersistentIdentifier - for (Map.Entry entry : pid.getMetadata().entrySet()) { - recordEntries.add(new PIDRecordEntry(entry.getKey(), entry.getValue())); - } - - // Only add additional metadata if configured to do so and the entity is not null (not a tombstone) - if (config.isMeaningfulPIDRecords() && pid.getEntity() != null) { - addAdministrativeMetadata(recordEntries, pid.getEntity()); + recordEntries.add(new PIDRecordEntry("21.T11148/b8457812905b83046284", doLocation)); + + // Add entity type information as digitalObjectType (currently hardcoded to "application/json") + recordEntries.add(new PIDRecordEntry("21.T11148/1c699a5d1b4ad3ba4956", "21.T11148/ca9fd0b2414177b79ac2")); + + // Add CC0 license information + recordEntries.add(new PIDRecordEntry("21.T11148/2f314c8fe5fb6a0063a8", "https://spdx.org/license/CC0-1.0/")); + + // Add nested SHA-256 hash for the doLocation + String sha256Hash = ""; + try { + // Calculate the SHA-256 hash of the doLocation as hex string + StringBuilder hexString = new StringBuilder(); + for (byte b : MessageDigest.getInstance("SHA-256").digest(doLocation.getBytes(StandardCharsets.UTF_8))) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) hexString.append('0'); + hexString.append(hex); + } + sha256Hash = hexString.toString(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); } - - // Create the PID record - PIDRecord pidRecord = new PIDRecord(pid.getPid(), recordEntries); - log.debug("Created PIDRecord: {}", pidRecord); - return pidRecord; - } - - /** - * Adds administrative metadata from an AdministrativeMetadata entity to a list of PIDRecordEntry objects. - * This method is used to add metadata to a PID record based on the Helmholtz Kernel Information Profile. - * - * @param recordEntries The list of PIDRecordEntry objects to add metadata to - * @param entity The AdministrativeMetadata entity to get metadata from - */ - private void addAdministrativeMetadata(List recordEntries, AdministrativeMetadata entity) { - // Helmholtz Kernel Information Profile - recordEntries.add(new PIDRecordEntry("21.T11148/076759916209e5d62bd5", "21.T11148/b9b76f887845e32d29f7")); + recordEntries.add(new PIDRecordEntry("21.T11148/82e2503c49209e987740", String.format("{\"sha256sum\": \"sha256 %s\"}", sha256Hash))); // Add basic metadata if (entity.getName() != null) { @@ -175,6 +163,11 @@ private void addAdministrativeMetadata(List recordEntries, Admin } }); } + + // Create the PID record + PIDRecord pidRecord = new PIDRecord(pid.getPid(), recordEntries); + log.debug("Created PIDRecord: {}", pidRecord); + return pidRecord; } /** diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/validation/InheritanceValidator.java b/src/main/java/edu/kit/datamanager/idoris/rules/validation/InheritanceValidator.java index fa17ec3..14e69d7 100644 --- a/src/main/java/edu/kit/datamanager/idoris/rules/validation/InheritanceValidator.java +++ b/src/main/java/edu/kit/datamanager/idoris/rules/validation/InheritanceValidator.java @@ -136,8 +136,8 @@ public ValidationResult visit(TypeProfile typeProfile, Object... args) { if (typeProfile.getInheritsFrom() != null && typeProfile.getInheritsFrom().size() > 0) { for (TypeProfile parent : typeProfile.getInheritsFrom()) { if (parent.isAbstract() && !typeProfile.isAbstract()) { - result.addMessage("TypeProfile " + typeProfile.getPid() + " is not abstract, but inherits from the TypeProfile " + - parent.getPid() + " that is abstract.", typeProfile, ERROR); + result.addMessage("TypeProfile " + typeProfile.getId() + " is not abstract, but inherits from the TypeProfile " + + parent.getId() + " that is abstract.", typeProfile, ERROR); } } } diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/validation/SyntaxValidator.java b/src/main/java/edu/kit/datamanager/idoris/rules/validation/SyntaxValidator.java index 2422f17..9e8e3c1 100644 --- a/src/main/java/edu/kit/datamanager/idoris/rules/validation/SyntaxValidator.java +++ b/src/main/java/edu/kit/datamanager/idoris/rules/validation/SyntaxValidator.java @@ -330,11 +330,6 @@ public ValidationResult visit(TechnologyInterface technologyInterface, Object... * @param result The validation result to add messages to */ private void validateDataType(DataType dataType, ValidationResult result) { - if (dataType.getType() == null) { - result.addMessage("You MUST provide a type for the data type. Please select from: " + - Arrays.toString(DataType.TYPES.values()), dataType, ERROR); - } - if (dataType.getName() == null || dataType.getName().isEmpty()) { result.addMessage("For better human readability and understanding, you MUST provide a name for the data type.", dataType, ERROR); diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationPolicyValidator.java b/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationPolicyValidator.java index f551f23..b40af5e 100644 --- a/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationPolicyValidator.java +++ b/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationPolicyValidator.java @@ -57,7 +57,7 @@ public ValidationResult visit(TypeProfile typeProfile, Object... args) { ValidationResult result = new ValidationResult(); if (typeProfile.getInheritsFrom() == null || typeProfile.getInheritsFrom().isEmpty()) { - log.debug("TypeProfile {} has no parent TypeProfiles. Skipping validation.", typeProfile.getPid()); + log.debug("TypeProfile {} has no parent TypeProfiles. Skipping validation.", typeProfile.getId()); return result; } @@ -66,11 +66,11 @@ public ValidationResult visit(TypeProfile typeProfile, Object... args) { log.debug("Parent TypeProfile is null or has no SubSchemaRelation defined. Skipping validation."); continue; } - log.debug("Validating TypeProfile {} against parent TypeProfile {}", typeProfile.getPid(), parent.getPid()); + log.debug("Validating TypeProfile {} against parent TypeProfile {}", typeProfile.getId(), parent.getId()); if (!parent.isAllowAdditionalAttributes() && !typeProfile.getAttributes().isEmpty()) - result.addMessage("TypeProfile " + typeProfile.getPid() + " defines additional properties, but inherits from the TypeProfile " + - parent.getPid() + " that denies additional properties.", + result.addMessage("TypeProfile " + typeProfile.getId() + " defines additional properties, but inherits from the TypeProfile " + + parent.getId() + " that denies additional properties.", getTypeProfileAndParentElementaryInformation(typeProfile, parent, Map.of("countOfAttributes", typeProfile.getAttributes().size())), ERROR); @@ -82,8 +82,8 @@ public ValidationResult visit(TypeProfile typeProfile, Object... args) { .toList(); if (!undefinedAttributes.isEmpty()) { - result.addMessage("TypeProfile " + typeProfile.getPid() + " does not define all properties defined in the TypeProfile " + - parent.getPid() + " that requires all properties.", + result.addMessage("TypeProfile " + typeProfile.getId() + " does not define all properties defined in the TypeProfile " + + parent.getId() + " that requires all properties.", getTypeProfileAndParentElementaryInformation(typeProfile, parent, Map.of("numberOfUndefinedAttributes", undefinedAttributes.size(), "undefinedAttributes", undefinedAttributes)), ERROR); @@ -93,16 +93,16 @@ public ValidationResult visit(TypeProfile typeProfile, Object... args) { case ANY -> { if (typeProfile.getAttributes().stream().noneMatch(a -> parent.getAttributes().stream() .anyMatch(pa -> pa.getDataType().equals(a.getDataType())))) { - result.addMessage("TypeProfile " + typeProfile.getPid() + " does not define any property defined in the TypeProfile " + - parent.getPid() + " that requires at least one property.", + result.addMessage("TypeProfile " + typeProfile.getId() + " does not define any property defined in the TypeProfile " + + parent.getId() + " that requires at least one property.", getTypeProfileAndParentElementaryInformation(typeProfile, parent, null), ERROR); } } case ONE -> { if (typeProfile.getAttributes().stream().filter(a -> parent.getAttributes().stream() .anyMatch(pa -> pa.getDataType().equals(a.getDataType()))).count() != 1) { - result.addMessage("TypeProfile " + typeProfile.getPid() + " does not define exactly one property defined in the TypeProfile " + - parent.getPid() + " that requires exactly one property.", + result.addMessage("TypeProfile " + typeProfile.getId() + " does not define exactly one property defined in the TypeProfile " + + parent.getId() + " that requires exactly one property.", getTypeProfileAndParentElementaryInformation(typeProfile, parent, null), ERROR); } } @@ -112,8 +112,8 @@ public ValidationResult visit(TypeProfile typeProfile, Object... args) { .toList(); if (!illegallyDefinedAttributes.isEmpty()) { - result.addMessage("TypeProfile " + typeProfile.getPid() + " defines a property defined in the TypeProfile " + - parent.getPid() + " that requires no property.", + result.addMessage("TypeProfile " + typeProfile.getId() + " defines a property defined in the TypeProfile " + + parent.getId() + " that requires no property.", getTypeProfileAndParentElementaryInformation(typeProfile, parent, Map.of("numberOfIllegallyDefinedAttributes", illegallyDefinedAttributes.size(), "illegallyDefinedAttributes", illegallyDefinedAttributes)), ERROR); @@ -122,7 +122,7 @@ public ValidationResult visit(TypeProfile typeProfile, Object... args) { default -> throw new IllegalStateException("Unknown ValidationPolicy " + parent.getValidationPolicy()); } } - log.debug("Validation of TypeProfile {} against parent TypeProfiles completed. result={}", typeProfile.getPid(), result); + log.debug("Validation of TypeProfile {} against parent TypeProfiles completed. result={}", typeProfile.getId(), result); return result; } @@ -136,8 +136,8 @@ public ValidationResult visit(TypeProfile typeProfile, Object... args) { */ private Object getTypeProfileAndParentElementaryInformation(TypeProfile typeProfile, TypeProfile parent, Map otherInformation) { Map result = new HashMap<>(); - result.put("this", new ElementaryInformation(typeProfile.getPid(), typeProfile.getName(), typeProfile.getValidationPolicy())); - result.put("parent", new ElementaryInformation(parent.getPid(), parent.getName(), parent.getValidationPolicy())); + result.put("this", new ElementaryInformation(typeProfile.getId(), typeProfile.getName(), typeProfile.getValidationPolicy())); + result.put("parent", new ElementaryInformation(parent.getId(), parent.getName(), parent.getValidationPolicy())); result.put("otherInformation", otherInformation); return result; } diff --git a/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/dao/ITechnologyInterfaceDao.java b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/dao/ITechnologyInterfaceDao.java index f140639..103c04d 100644 --- a/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/dao/ITechnologyInterfaceDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/dao/ITechnologyInterfaceDao.java @@ -20,4 +20,4 @@ import edu.kit.datamanager.idoris.technologyinterfaces.entities.TechnologyInterface; public interface ITechnologyInterfaceDao extends IGenericRepo { -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/services/TechnologyInterfaceService.java b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/services/TechnologyInterfaceService.java index 354eb1f..710bbe3 100644 --- a/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/services/TechnologyInterfaceService.java +++ b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/services/TechnologyInterfaceService.java @@ -59,7 +59,7 @@ public TechnologyInterface createTechnologyInterface(TechnologyInterface technol log.debug("Creating TechnologyInterface: {}", technologyInterface); TechnologyInterface saved = technologyInterfaceDao.save(technologyInterface); eventPublisher.publishEntityCreated(saved); - log.info("Created TechnologyInterface with PID: {}", saved.getPid()); + log.info("Created TechnologyInterface with PID: {}", saved.getId()); return saved; } @@ -74,50 +74,50 @@ public TechnologyInterface createTechnologyInterface(TechnologyInterface technol public TechnologyInterface updateTechnologyInterface(TechnologyInterface technologyInterface) { log.debug("Updating TechnologyInterface: {}", technologyInterface); - if (technologyInterface.getPid() == null || technologyInterface.getPid().isEmpty()) { + if (technologyInterface.getId() == null || technologyInterface.getId().isEmpty()) { throw new IllegalArgumentException("TechnologyInterface must have a PID to be updated"); } // Get the current version before updating - TechnologyInterface existing = technologyInterfaceDao.findById(technologyInterface.getPid()) - .orElseThrow(() -> new IllegalArgumentException("TechnologyInterface not found with PID: " + technologyInterface.getPid())); + TechnologyInterface existing = technologyInterfaceDao.findById(technologyInterface.getId()) + .orElseThrow(() -> new IllegalArgumentException("TechnologyInterface not found with PID: " + technologyInterface.getId())); Long previousVersion = existing.getVersion(); TechnologyInterface saved = technologyInterfaceDao.save(technologyInterface); eventPublisher.publishEntityUpdated(saved, previousVersion); - log.info("Updated TechnologyInterface with PID: {}", saved.getPid()); + log.info("Updated TechnologyInterface with PID: {}", saved.getId()); return saved; } /** * Deletes a TechnologyInterface entity. * - * @param pid the PID of the TechnologyInterface to delete + * @param id the PID or internal ID of the TechnologyInterface to delete * @throws IllegalArgumentException if the TechnologyInterface does not exist */ @Transactional - public void deleteTechnologyInterface(String pid) { - log.debug("Deleting TechnologyInterface with PID: {}", pid); + public void deleteTechnologyInterface(String id) { + log.debug("Deleting TechnologyInterface with ID: {}", id); - TechnologyInterface technologyInterface = technologyInterfaceDao.findById(pid) - .orElseThrow(() -> new IllegalArgumentException("TechnologyInterface not found with PID: " + pid)); + TechnologyInterface technologyInterface = technologyInterfaceDao.findById(id) + .orElseThrow(() -> new IllegalArgumentException("TechnologyInterface not found with ID: " + id)); technologyInterfaceDao.delete(technologyInterface); eventPublisher.publishEntityDeleted(technologyInterface); - log.info("Deleted TechnologyInterface with PID: {}", pid); + log.info("Deleted TechnologyInterface with ID: {}", id); } /** - * Retrieves a TechnologyInterface entity by its PID. + * Retrieves a TechnologyInterface entity by its PID or internal ID. * - * @param pid the PID of the TechnologyInterface to retrieve + * @param id the PID or internal ID of the TechnologyInterface to retrieve * @return an Optional containing the TechnologyInterface, or empty if not found */ @Transactional(readOnly = true) - public Optional getTechnologyInterface(String pid) { - log.debug("Retrieving TechnologyInterface with PID: {}", pid); - return technologyInterfaceDao.findById(pid); + public Optional getTechnologyInterface(String id) { + log.debug("Retrieving TechnologyInterface with ID: {}", id); + return technologyInterfaceDao.findById(id); } /** @@ -134,21 +134,21 @@ public List getAllTechnologyInterfaces() { /** * Partially updates an existing TechnologyInterface entity. * - * @param pid the PID of the TechnologyInterface to patch + * @param id the PID or internal ID of the TechnologyInterface to patch * @param technologyInterfacePatch the partial TechnologyInterface entity with fields to update * @return the patched TechnologyInterface entity * @throws IllegalArgumentException if the TechnologyInterface does not exist */ @Transactional - public TechnologyInterface patchTechnologyInterface(String pid, TechnologyInterface technologyInterfacePatch) { - log.debug("Patching TechnologyInterface with PID: {}, patch: {}", pid, technologyInterfacePatch); - if (pid == null || pid.isEmpty()) { - throw new IllegalArgumentException("TechnologyInterface PID cannot be null or empty"); + public TechnologyInterface patchTechnologyInterface(String id, TechnologyInterface technologyInterfacePatch) { + log.debug("Patching TechnologyInterface with ID: {}, patch: {}", id, technologyInterfacePatch); + if (id == null || id.isEmpty()) { + throw new IllegalArgumentException("TechnologyInterface ID cannot be null or empty"); } // Get the current entity - TechnologyInterface existing = technologyInterfaceDao.findById(pid) - .orElseThrow(() -> new IllegalArgumentException("TechnologyInterface not found with PID: " + pid)); + TechnologyInterface existing = technologyInterfaceDao.findById(id) + .orElseThrow(() -> new IllegalArgumentException("TechnologyInterface not found with ID: " + id)); Long previousVersion = existing.getVersion(); // Apply non-null fields from the patch to the existing entity @@ -174,7 +174,7 @@ public TechnologyInterface patchTechnologyInterface(String pid, TechnologyInterf // Publish the patched event eventPublisher.publishEntityPatched(saved, previousVersion); - log.info("Patched TechnologyInterface with PID: {}", saved.getPid()); + log.info("Patched TechnologyInterface with PID: {}", saved.getId()); return saved; } } diff --git a/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/web/api/ITechnologyInterfaceApi.java b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/web/api/ITechnologyInterfaceApi.java index 64627a4..093dadb 100644 --- a/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/web/api/ITechnologyInterfaceApi.java +++ b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/web/api/ITechnologyInterfaceApi.java @@ -55,15 +55,15 @@ public interface ITechnologyInterfaceApi { ResponseEntity>> getAllTechnologyInterfaces(); /** - * Gets a TechnologyInterface entity by its PID. + * Gets a TechnologyInterface entity by its PID or internal ID. * - * @param pid the PID of the TechnologyInterface to retrieve + * @param id the PID or internal ID of the TechnologyInterface to retrieve * @return the TechnologyInterface entity */ - @GetMapping("/{pid}") + @GetMapping("/{id}") @Operation( - summary = "Get a TechnologyInterface by PID", - description = "Returns a TechnologyInterface entity by its PID", + summary = "Get a TechnologyInterface by PID or internal ID", + description = "Returns a TechnologyInterface entity by its PID or internal ID", responses = { @ApiResponse(responseCode = "200", description = "TechnologyInterface found", content = @Content(mediaType = "application/hal+json", @@ -72,16 +72,16 @@ public interface ITechnologyInterfaceApi { } ) ResponseEntity> getTechnologyInterface( - @Parameter(description = "PID of the TechnologyInterface", required = true) - @PathVariable String pid); + @Parameter(description = "PID or internal ID of the TechnologyInterface", required = true) + @PathVariable String id); /** * Gets the attributes of a TechnologyInterface. * - * @param pid the PID of the TechnologyInterface + * @param id the PID or internal ID of the TechnologyInterface * @return a collection of attributes */ - @GetMapping("/{pid}/attributes") + @GetMapping("/{id}/attributes") @Operation( summary = "Get attributes of a TechnologyInterface", description = "Returns a collection of attributes of a TechnologyInterface", @@ -93,16 +93,16 @@ ResponseEntity> getTechnologyInterface( } ) ResponseEntity>> getAttributes( - @Parameter(description = "PID of the TechnologyInterface", required = true) - @PathVariable String pid); + @Parameter(description = "PID or internal ID of the TechnologyInterface", required = true) + @PathVariable String id); /** * Gets the outputs of a TechnologyInterface. * - * @param pid the PID of the TechnologyInterface + * @param id the PID or internal ID of the TechnologyInterface * @return a collection of outputs */ - @GetMapping("/{pid}/outputs") + @GetMapping("/{id}/outputs") @Operation( summary = "Get outputs of a TechnologyInterface", description = "Returns a collection of outputs of a TechnologyInterface", @@ -114,8 +114,8 @@ ResponseEntity>> getAttributes( } ) ResponseEntity>> getOutputs( - @Parameter(description = "PID of the TechnologyInterface", required = true) - @PathVariable String pid); + @Parameter(description = "PID or internal ID of the TechnologyInterface", required = true) + @PathVariable String id); /** * Creates a new TechnologyInterface entity. @@ -141,11 +141,11 @@ ResponseEntity> createTechnologyInterface( /** * Updates an existing TechnologyInterface entity. * - * @param pid the PID of the TechnologyInterface to update + * @param id the PID or internal ID of the TechnologyInterface to update * @param technologyInterface the updated TechnologyInterface entity * @return the updated TechnologyInterface entity */ - @PutMapping("/{pid}") + @PutMapping("/{id}") @Operation( summary = "Update a TechnologyInterface", description = "Updates an existing TechnologyInterface entity", @@ -158,18 +158,18 @@ ResponseEntity> createTechnologyInterface( } ) ResponseEntity> updateTechnologyInterface( - @Parameter(description = "PID of the TechnologyInterface", required = true) - @PathVariable String pid, + @Parameter(description = "PID or internal ID of the TechnologyInterface", required = true) + @PathVariable String id, @Parameter(description = "Updated TechnologyInterface", required = true) @Valid @RequestBody TechnologyInterface technologyInterface); /** * Deletes a TechnologyInterface entity. * - * @param pid the PID of the TechnologyInterface to delete + * @param id the PID or internal ID of the TechnologyInterface to delete * @return no content */ - @DeleteMapping("/{pid}") + @DeleteMapping("/{id}") @Operation( summary = "Delete a TechnologyInterface", description = "Deletes a TechnologyInterface entity", @@ -179,17 +179,17 @@ ResponseEntity> updateTechnologyInterface( } ) ResponseEntity deleteTechnologyInterface( - @Parameter(description = "PID of the TechnologyInterface", required = true) - @PathVariable String pid); + @Parameter(description = "PID or internal ID of the TechnologyInterface", required = true) + @PathVariable String id); /** * Partially updates a TechnologyInterface entity. * - * @param pid the PID of the TechnologyInterface to patch + * @param id the PID or internal ID of the TechnologyInterface to patch * @param technologyInterfacePatch the partial TechnologyInterface entity with fields to update * @return the patched TechnologyInterface entity */ - @PatchMapping("/{pid}") + @PatchMapping("/{id}") @Operation( summary = "Partially update a TechnologyInterface", description = "Updates specific fields of an existing TechnologyInterface entity", @@ -202,8 +202,8 @@ ResponseEntity deleteTechnologyInterface( } ) ResponseEntity> patchTechnologyInterface( - @Parameter(description = "PID of the TechnologyInterface", required = true) - @PathVariable String pid, + @Parameter(description = "PID or internal ID of the TechnologyInterface", required = true) + @PathVariable String id, @Parameter(description = "Partial TechnologyInterface with fields to update", required = true) @RequestBody TechnologyInterface technologyInterfacePatch); } diff --git a/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/web/hateoas/TechnologyInterfaceModelAssembler.java b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/web/hateoas/TechnologyInterfaceModelAssembler.java index c26b9b3..0451398 100644 --- a/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/web/hateoas/TechnologyInterfaceModelAssembler.java +++ b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/web/hateoas/TechnologyInterfaceModelAssembler.java @@ -42,13 +42,13 @@ public EntityModel toModel(TechnologyInterface technologyIn EntityModel entityModel = toModelWithoutLinks(technologyInterface); // Add self link - entityModel.add(linkTo(methodOn(TechnologyInterfaceController.class).getTechnologyInterface(technologyInterface.getPid())).withSelfRel()); + entityModel.add(linkTo(methodOn(TechnologyInterfaceController.class).getTechnologyInterface(technologyInterface.getId())).withSelfRel()); // Add link to attributes - entityModel.add(linkTo(methodOn(TechnologyInterfaceController.class).getAttributes(technologyInterface.getPid())).withRel("attributes")); + entityModel.add(linkTo(methodOn(TechnologyInterfaceController.class).getAttributes(technologyInterface.getId())).withRel("attributes")); // Add link to outputs - entityModel.add(linkTo(methodOn(TechnologyInterfaceController.class).getOutputs(technologyInterface.getPid())).withRel("outputs")); + entityModel.add(linkTo(methodOn(TechnologyInterfaceController.class).getOutputs(technologyInterface.getId())).withRel("outputs")); // Add link to all technology interfaces entityModel.add(linkTo(methodOn(TechnologyInterfaceController.class).getAllTechnologyInterfaces()).withRel("technologyInterfaces")); diff --git a/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/web/v1/TechnologyInterfaceController.java b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/web/v1/TechnologyInterfaceController.java index 7b9ef84..714c9be 100644 --- a/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/web/v1/TechnologyInterfaceController.java +++ b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/web/v1/TechnologyInterfaceController.java @@ -75,8 +75,8 @@ public ResponseEntity>> getAllT * {@inheritDoc} */ @Override - public ResponseEntity> getTechnologyInterface(String pid) { - return technologyInterfaceService.getTechnologyInterface(pid) + public ResponseEntity> getTechnologyInterface(String id) { + return technologyInterfaceService.getTechnologyInterface(id) .map(technologyInterfaceModelAssembler::toModel) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); @@ -86,8 +86,8 @@ public ResponseEntity> getTechnologyInterface(S * {@inheritDoc} */ @Override - public ResponseEntity>> getAttributes(String pid) { - return technologyInterfaceService.getTechnologyInterface(pid) + public ResponseEntity>> getAttributes(String id) { + return technologyInterfaceService.getTechnologyInterface(id) .map(technologyInterface -> { List> attributes = StreamSupport.stream(technologyInterface.getAttributes().spliterator(), false) .map(attributeModelAssembler::toModel) @@ -95,8 +95,8 @@ public ResponseEntity>> getAttributes(Str CollectionModel> collectionModel = CollectionModel.of( attributes, - linkTo(methodOn(TechnologyInterfaceController.class).getAttributes(pid)).withSelfRel(), - linkTo(methodOn(TechnologyInterfaceController.class).getTechnologyInterface(pid)).withRel("technologyInterface") + linkTo(methodOn(TechnologyInterfaceController.class).getAttributes(id)).withSelfRel(), + linkTo(methodOn(TechnologyInterfaceController.class).getTechnologyInterface(id)).withRel("technologyInterface") ); return ResponseEntity.ok(collectionModel); @@ -108,8 +108,8 @@ public ResponseEntity>> getAttributes(Str * {@inheritDoc} */ @Override - public ResponseEntity>> getOutputs(String pid) { - return technologyInterfaceService.getTechnologyInterface(pid) + public ResponseEntity>> getOutputs(String id) { + return technologyInterfaceService.getTechnologyInterface(id) .map(technologyInterface -> { List> outputs = StreamSupport.stream(technologyInterface.getOutputs().spliterator(), false) .map(attributeModelAssembler::toModel) @@ -117,8 +117,8 @@ public ResponseEntity>> getOutputs(String CollectionModel> collectionModel = CollectionModel.of( outputs, - linkTo(methodOn(TechnologyInterfaceController.class).getOutputs(pid)).withSelfRel(), - linkTo(methodOn(TechnologyInterfaceController.class).getTechnologyInterface(pid)).withRel("technologyInterface") + linkTo(methodOn(TechnologyInterfaceController.class).getOutputs(id)).withSelfRel(), + linkTo(methodOn(TechnologyInterfaceController.class).getTechnologyInterface(id)).withRel("technologyInterface") ); return ResponseEntity.ok(collectionModel); @@ -140,12 +140,21 @@ public ResponseEntity> createTechnologyInterfac * {@inheritDoc} */ @Override - public ResponseEntity> updateTechnologyInterface(String pid, TechnologyInterface technologyInterface) { - if (!technologyInterfaceService.getTechnologyInterface(pid).isPresent()) { + public ResponseEntity> updateTechnologyInterface(String id, TechnologyInterface technologyInterface) { + // Check if the entity exists + if (!technologyInterfaceService.getTechnologyInterface(id).isPresent()) { return ResponseEntity.notFound().build(); } - technologyInterface.setPid(pid); + // Get the existing entity to get its PID and internalId + TechnologyInterface existing = technologyInterfaceService.getTechnologyInterface(id).get(); + + // Set the PID from the existing entity + technologyInterface.setInternalId(existing.getId()); + + // Ensure internal ID is preserved + technologyInterface.setInternalId(existing.getInternalId()); + TechnologyInterface updatedTechnologyInterface = technologyInterfaceService.updateTechnologyInterface(technologyInterface); EntityModel entityModel = technologyInterfaceModelAssembler.toModel(updatedTechnologyInterface); return ResponseEntity.ok(entityModel); @@ -155,12 +164,12 @@ public ResponseEntity> updateTechnologyInterfac * {@inheritDoc} */ @Override - public ResponseEntity deleteTechnologyInterface(String pid) { - if (!technologyInterfaceService.getTechnologyInterface(pid).isPresent()) { + public ResponseEntity deleteTechnologyInterface(String id) { + if (!technologyInterfaceService.getTechnologyInterface(id).isPresent()) { return ResponseEntity.notFound().build(); } - technologyInterfaceService.deleteTechnologyInterface(pid); + technologyInterfaceService.deleteTechnologyInterface(id); return ResponseEntity.noContent().build(); } @@ -168,12 +177,12 @@ public ResponseEntity deleteTechnologyInterface(String pid) { * {@inheritDoc} */ @Override - public ResponseEntity> patchTechnologyInterface(String pid, TechnologyInterface technologyInterfacePatch) { - if (!technologyInterfaceService.getTechnologyInterface(pid).isPresent()) { + public ResponseEntity> patchTechnologyInterface(String id, TechnologyInterface technologyInterfacePatch) { + if (!technologyInterfaceService.getTechnologyInterface(id).isPresent()) { return ResponseEntity.notFound().build(); } - TechnologyInterface patchedTechnologyInterface = technologyInterfaceService.patchTechnologyInterface(pid, technologyInterfacePatch); + TechnologyInterface patchedTechnologyInterface = technologyInterfaceService.patchTechnologyInterface(id, technologyInterfacePatch); EntityModel entityModel = technologyInterfaceModelAssembler.toModel(patchedTechnologyInterface); return ResponseEntity.ok(entityModel); } diff --git a/src/main/java/edu/kit/datamanager/idoris/users/services/UserService.java b/src/main/java/edu/kit/datamanager/idoris/users/services/UserService.java index 63b5f91..d483e51 100644 --- a/src/main/java/edu/kit/datamanager/idoris/users/services/UserService.java +++ b/src/main/java/edu/kit/datamanager/idoris/users/services/UserService.java @@ -38,9 +38,9 @@ public interface UserService { List findAllUsers(); /** - * Find a user by their internal ID. + * Find a user by their PID or internal ID. * - * @param id The internal ID of the user + * @param id The PID or internal ID of the user * @return Optional containing the user if found, empty otherwise */ Optional findUserById(String id); @@ -94,7 +94,7 @@ public interface UserService { /** * Update an existing user. * - * @param id The internal ID of the user to update + * @param id The PID or internal ID of the user to update * @param user The updated user information * @return The updated user * @throws IllegalArgumentException if the user is not found @@ -102,10 +102,10 @@ public interface UserService { User updateUser(String id, User user); /** - * Delete a user by their internal ID. + * Delete a user by their PID or internal ID. * - * @param id The internal ID of the user to delete + * @param id The PID or internal ID of the user to delete * @throws IllegalArgumentException if the user is not found */ void deleteUser(String id); -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/idoris/users/web/api/IUserApi.java b/src/main/java/edu/kit/datamanager/idoris/users/web/api/IUserApi.java index b01ba09..1e0cfeb 100644 --- a/src/main/java/edu/kit/datamanager/idoris/users/web/api/IUserApi.java +++ b/src/main/java/edu/kit/datamanager/idoris/users/web/api/IUserApi.java @@ -57,13 +57,13 @@ public interface IUserApi { /** * Gets a User entity by its ID. * - * @param id the ID of the User to retrieve + * @param id the PID or internal ID of the User to retrieve * @return the User entity */ @GetMapping("/{id}") @Operation( summary = "Get user by ID", - description = "Retrieves a user by their internal ID", + description = "Retrieves a user by their PID or internal ID", responses = { @ApiResponse(responseCode = "200", description = "User retrieved successfully", content = @Content(mediaType = "application/hal+json", @@ -72,7 +72,7 @@ public interface IUserApi { } ) ResponseEntity> getUserById( - @Parameter(description = "ID of the User", required = true) + @Parameter(description = "PID or internal ID of the User", required = true) @PathVariable String id); /** @@ -194,14 +194,14 @@ ResponseEntity> createORCiDUser( /** * Updates an existing User entity. * - * @param id the ID of the User to update + * @param id the PID or internal ID of the User to update * @param user the updated User entity * @return the updated User entity */ @PutMapping("/{id}") @Operation( summary = "Update user", - description = "Updates an existing user", + description = "Updates an existing user by their PID or internal ID", responses = { @ApiResponse(responseCode = "200", description = "User updated successfully", content = @Content(mediaType = "application/hal+json", @@ -210,7 +210,7 @@ ResponseEntity> createORCiDUser( } ) ResponseEntity> updateUser( - @Parameter(description = "ID of the User", required = true) + @Parameter(description = "PID or internal ID of the User", required = true) @PathVariable String id, @Parameter(description = "Updated User", required = true) @RequestBody User user); @@ -218,19 +218,19 @@ ResponseEntity> updateUser( /** * Deletes a User entity. * - * @param id the ID of the User to delete + * @param id the PID or internal ID of the User to delete * @return no content */ @DeleteMapping("/{id}") @Operation( summary = "Delete user", - description = "Deletes a user by their internal ID", + description = "Deletes a user by their PID or internal ID", responses = { @ApiResponse(responseCode = "204", description = "User deleted successfully"), @ApiResponse(responseCode = "404", description = "User not found") } ) ResponseEntity deleteUser( - @Parameter(description = "ID of the User", required = true) + @Parameter(description = "PID or internal ID of the User", required = true) @PathVariable String id); } diff --git a/src/main/java/edu/kit/datamanager/idoris/users/web/v1/UserController.java b/src/main/java/edu/kit/datamanager/idoris/users/web/v1/UserController.java index c765303..a1d8dde 100644 --- a/src/main/java/edu/kit/datamanager/idoris/users/web/v1/UserController.java +++ b/src/main/java/edu/kit/datamanager/idoris/users/web/v1/UserController.java @@ -31,6 +31,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ResponseStatusException; +import java.net.URI; import java.net.URL; import java.util.List; import java.util.stream.Collectors; @@ -130,7 +131,7 @@ public ResponseEntity>> getAllORCiDUsers( public ResponseEntity> getORCiDUserByORCiD(String orcidStr) { try { // Convert ORCID string to URL - URL orcid = new URL("https://orcid.org/" + orcidStr); + URL orcid = URI.create("https://orcid.org/" + orcidStr).toURL(); return userService.findORCiDUserByORCiD(orcid) .map(user -> EntityModel.of(user, linkTo(methodOn(UserController.class).getORCiDUserByORCiD(orcidStr)).withSelfRel(), diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 614942a..604a11e 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -15,12 +15,13 @@ # spring.application.name=idoris logging.level.root=INFO -logging.level.org.springframework=INFO -logging.level.edu.kit.datamanager=DEBUG +logging.level.org.springframework=TRACE +logging.level.edu.kit.datamanager=TRACE spring.neo4j.uri=bolt://localhost:7687 spring.neo4j.authentication.username=neo4j spring.neo4j.authentication.password=superSecret management.endpoints.web.exposure.include=* +server.tomcat.relaxed-path-chars=/ # Spring Doc Settings for OpenAPI documentation springdoc.show-actuator=true springdoc.api-docs.path=/v1/api-docs @@ -30,10 +31,8 @@ spring.hateoas.use-hal-as-default-json-media-type=true server.servlet.context-path=/api # IDORIS Settings server.port=8095 +idoris.base-url=http://localhost:8095/api idoris.validation-level=info idoris.validation-policy=strict -idoris.pid-generation=TYPED_PID_MAKER idoris.typed-pid-maker.base-url=http://localhost:8090 idoris.typed-pid-maker.timeout=5000 -idoris.typed-pid-maker.meaningful-pid-records=true -idoris.typed-pid-maker.update-pid-records=true diff --git a/src/test/java/edu/kit/datamanager/idoris/core/domain/dao/IGenericRepoTest.java b/src/test/java/edu/kit/datamanager/idoris/core/domain/dao/IGenericRepoTest.java new file mode 100644 index 0000000..9388537 --- /dev/null +++ b/src/test/java/edu/kit/datamanager/idoris/core/domain/dao/IGenericRepoTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2024-2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.core.domain.dao; + +import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class IGenericRepoTest { + + @Mock + private IGenericRepo repository; + + @Mock + private AdministrativeMetadata entity; + + @BeforeEach + void setUp() { + // Set up the mock repository to return the entity when findByPid is called with "pid123" + when(repository.findByPid("pid123")).thenReturn(Optional.of(entity)); + + // Set up the mock repository to return the entity when findByInternalId is called with "internal456" + when(repository.findByInternalId("internal456")).thenReturn(Optional.of(entity)); + + // Set up the mock repository to delegate findById to the default implementation + when(repository.findById(anyString())).thenCallRealMethod(); + } + + @Test + void findById_WithPid_ShouldFindByPid() { + // When + Optional result = repository.findById("pid123"); + + // Then + assertTrue(result.isPresent()); + assertEquals(entity, result.get()); + verify(repository).findByPid("pid123"); + verify(repository, never()).findByInternalId(anyString()); + } + + @Test + void findById_WithInternalId_ShouldFindByInternalId() { + // Given + when(repository.findByPid("internal456")).thenReturn(Optional.empty()); + + // When + Optional result = repository.findById("internal456"); + + // Then + assertTrue(result.isPresent()); + assertEquals(entity, result.get()); + verify(repository).findByPid("internal456"); + verify(repository).findByInternalId("internal456"); + } + + @Test + void findById_WithNonExistentId_ShouldReturnEmpty() { + // Given + when(repository.findByPid("nonexistent")).thenReturn(Optional.empty()); + when(repository.findByInternalId("nonexistent")).thenReturn(Optional.empty()); + + // When + Optional result = repository.findById("nonexistent"); + + // Then + assertTrue(result.isEmpty()); + verify(repository).findByPid("nonexistent"); + verify(repository).findByInternalId("nonexistent"); + } +} \ No newline at end of file diff --git a/src/test/java/edu/kit/datamanager/idoris/pids/PIDTombstoneEventListenerTest.java b/src/test/java/edu/kit/datamanager/idoris/pids/PIDTombstoneEventListenerTest.java deleted file mode 100644 index 1f37d51..0000000 --- a/src/test/java/edu/kit/datamanager/idoris/pids/PIDTombstoneEventListenerTest.java +++ /dev/null @@ -1,221 +0,0 @@ -/* - * Copyright (c) 2025 Karlsruhe Institute of Technology - * - * 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 edu.kit.datamanager.idoris.pids; - -import edu.kit.datamanager.idoris.configuration.TypedPIDMakerConfig; -import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; -import edu.kit.datamanager.idoris.core.events.EntityDeletedEvent; -import edu.kit.datamanager.idoris.pids.client.TypedPIDMakerClient; -import edu.kit.datamanager.idoris.pids.client.model.PIDRecord; -import edu.kit.datamanager.idoris.pids.client.model.PIDRecordEntry; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.time.Instant; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class PIDTombstoneEventListenerTest { - - @Mock - private TypedPIDMakerClient client; - - @Mock - private TypedPIDMakerConfig config; - - private PIDTombstoneEventListener listener; - - @BeforeEach - void setUp() { - listener = new PIDTombstoneEventListener(client, config); - } - - @Test - void handleEntityDeletedEvent_withValidPid_createsTombstone() { - // Arrange - String pid = "test-pid"; - TestEntity entity = new TestEntity(); - entity.setPid(pid); - entity.setName("Test Entity"); - entity.setDescription("Test Description"); - entity.setCreatedAt(Instant.now()); - entity.setLastModifiedAt(Instant.now()); - entity.setVersion(1L); - - EntityDeletedEvent event = new EntityDeletedEvent<>(entity); - - PIDRecord existingRecord = new PIDRecord(pid, List.of( - new PIDRecordEntry("digitalObjectLocation", "http://example.com/pid/" + pid), - new PIDRecordEntry("name", "Test Entity") - )); - - when(client.getPIDRecord(pid)).thenReturn(existingRecord); - when(config.isMeaningfulPIDRecords()).thenReturn(true); - - // Act - listener.handleEntityDeletedEvent(event); - - // Assert - ArgumentCaptor recordCaptor = ArgumentCaptor.forClass(PIDRecord.class); - verify(client).updatePIDRecord(eq(pid), recordCaptor.capture()); - - PIDRecord updatedRecord = recordCaptor.getValue(); - assertEquals(pid, updatedRecord.pid()); - - // Verify tombstone marker - assertTrue(updatedRecord.record().stream() - .anyMatch(entry -> entry.key().equals("tombstone") && entry.value().equals("true"))); - - // Verify deletedAt timestamp - assertTrue(updatedRecord.record().stream() - .anyMatch(entry -> entry.key().equals("deletedAt"))); - - // Verify entity type - assertTrue(updatedRecord.record().stream() - .anyMatch(entry -> entry.key().equals("entityType") && entry.value().equals("TestEntity"))); - - // Verify Helmholtz Kernel Information Profile - assertTrue(updatedRecord.record().stream() - .anyMatch(entry -> entry.key().equals("21.T11148/076759916209e5d62bd5") && - entry.value().equals("21.T11148/b9b76f887845e32d29f7"))); - - // Verify basic metadata - assertTrue(updatedRecord.record().stream() - .anyMatch(entry -> entry.key().equals("name") && entry.value().equals("Test Entity"))); - assertTrue(updatedRecord.record().stream() - .anyMatch(entry -> entry.key().equals("description") && entry.value().equals("Test Description"))); - } - - @Test - void handleEntityDeletedEvent_withoutMeaningfulRecords_createsTombstoneWithMinimalInfo() { - // Arrange - String pid = "test-pid"; - TestEntity entity = new TestEntity(); - entity.setPid(pid); - entity.setName("Test Entity"); - entity.setDescription("Test Description"); - - EntityDeletedEvent event = new EntityDeletedEvent<>(entity); - - PIDRecord existingRecord = new PIDRecord(pid, List.of( - new PIDRecordEntry("digitalObjectLocation", "http://example.com/pid/" + pid) - )); - - when(client.getPIDRecord(pid)).thenReturn(existingRecord); - when(config.isMeaningfulPIDRecords()).thenReturn(false); - - // Act - listener.handleEntityDeletedEvent(event); - - // Assert - ArgumentCaptor recordCaptor = ArgumentCaptor.forClass(PIDRecord.class); - verify(client).updatePIDRecord(eq(pid), recordCaptor.capture()); - - PIDRecord updatedRecord = recordCaptor.getValue(); - assertEquals(pid, updatedRecord.pid()); - - // Verify tombstone marker - assertTrue(updatedRecord.record().stream() - .anyMatch(entry -> entry.key().equals("tombstone") && entry.value().equals("true"))); - - // Verify deletedAt timestamp - assertTrue(updatedRecord.record().stream() - .anyMatch(entry -> entry.key().equals("deletedAt"))); - - // Verify entity type - assertTrue(updatedRecord.record().stream() - .anyMatch(entry -> entry.key().equals("entityType") && entry.value().equals("TestEntity"))); - - // Verify no Helmholtz Kernel Information Profile - assertFalse(updatedRecord.record().stream() - .anyMatch(entry -> entry.key().equals("21.T11148/076759916209e5d62bd5"))); - - // Verify no basic metadata - assertFalse(updatedRecord.record().stream() - .anyMatch(entry -> entry.key().equals("name"))); - assertFalse(updatedRecord.record().stream() - .anyMatch(entry -> entry.key().equals("description"))); - } - - @Test - void handleEntityDeletedEvent_withNullPid_doesNothing() { - // Arrange - TestEntity entity = new TestEntity(); - entity.setPid(null); - - EntityDeletedEvent event = new EntityDeletedEvent<>(entity); - - // Act - listener.handleEntityDeletedEvent(event); - - // Assert - verify(client, never()).getPIDRecord(any()); - verify(client, never()).updatePIDRecord(any(), any()); - } - - @Test - void handleEntityDeletedEvent_withEmptyPid_doesNothing() { - // Arrange - TestEntity entity = new TestEntity(); - entity.setPid(""); - - EntityDeletedEvent event = new EntityDeletedEvent<>(entity); - - // Act - listener.handleEntityDeletedEvent(event); - - // Assert - verify(client, never()).getPIDRecord(any()); - verify(client, never()).updatePIDRecord(any(), any()); - } - - @Test - void handleEntityDeletedEvent_whenClientThrowsException_handlesGracefully() { - // Arrange - String pid = "test-pid"; - TestEntity entity = new TestEntity(); - entity.setPid(pid); - - EntityDeletedEvent event = new EntityDeletedEvent<>(entity); - - when(client.getPIDRecord(pid)).thenThrow(new RuntimeException("Test exception")); - - // Act & Assert - assertDoesNotThrow(() -> listener.handleEntityDeletedEvent(event)); - verify(client, never()).updatePIDRecord(any(), any()); - } - - // Test entity class - private static class TestEntity extends AdministrativeMetadata { - @Override - protected > T accept( - edu.kit.datamanager.idoris.rules.logic.Visitor visitor, Object... args) { - // Simple implementation for testing purposes - // Since this is just a test class, we return null - return null; - } - } -} diff --git a/src/test/java/edu/kit/datamanager/idoris/pids/web/v1/PersistentIdentifierControllerTest.java b/src/test/java/edu/kit/datamanager/idoris/pids/web/v1/PersistentIdentifierControllerTest.java deleted file mode 100644 index ceff516..0000000 --- a/src/test/java/edu/kit/datamanager/idoris/pids/web/v1/PersistentIdentifierControllerTest.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (c) 2025 Karlsruhe Institute of Technology - * - * 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 edu.kit.datamanager.idoris.pids.web.v1; - -import edu.kit.datamanager.idoris.pids.entities.PersistentIdentifier; -import edu.kit.datamanager.idoris.pids.services.PersistentIdentifierService; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; - -import java.time.Instant; -import java.util.HashMap; -import java.util.List; -import java.util.Optional; - -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.is; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@ExtendWith(MockitoExtension.class) -class PersistentIdentifierControllerTest { - - @Mock - private PersistentIdentifierService service; - - @InjectMocks - private PersistentIdentifierController controller; - - private MockMvc mockMvc; - - @BeforeEach - void setUp() { - mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); - } - - @Test - void getAllPersistentIdentifiers_shouldReturnAllPersistentIdentifiers() throws Exception { - // Arrange - PersistentIdentifier pid1 = createPersistentIdentifier("pid1", "TestEntity", "entity1", false); - PersistentIdentifier pid2 = createPersistentIdentifier("pid2", "TestEntity", "entity2", false); - List pids = List.of(pid1, pid2); - - when(service.getAllPersistentIdentifiers()).thenReturn(pids); - - // Act & Assert - mockMvc.perform(get("/api/v1/pids") - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(2))) - .andExpect(jsonPath("$[0].pid", is("pid1"))) - .andExpect(jsonPath("$[1].pid", is("pid2"))); - } - - @Test - void getPersistentIdentifier_withExistingPid_shouldReturnPersistentIdentifier() throws Exception { - // Arrange - String pid = "test-pid"; - PersistentIdentifier persistentIdentifier = createPersistentIdentifier(pid, "TestEntity", "entity1", false); - - when(service.getPersistentIdentifier(pid)).thenReturn(Optional.of(persistentIdentifier)); - - // Act & Assert - mockMvc.perform(get("/api/v1/pids/{pid}", pid) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.pid", is(pid))) - .andExpect(jsonPath("$.entityType", is("TestEntity"))) - .andExpect(jsonPath("$.entityInternalId", is("entity1"))) - .andExpect(jsonPath("$.tombstone", is(false))); - } - - @Test - void getPersistentIdentifier_withNonExistingPid_shouldReturnNotFound() throws Exception { - // Arrange - String pid = "non-existing-pid"; - - when(service.getPersistentIdentifier(pid)).thenReturn(Optional.empty()); - - // Act & Assert - mockMvc.perform(get("/api/v1/pids/{pid}", pid) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isNotFound()); - } - - @Test - void getPersistentIdentifiersByEntityType_shouldReturnPersistentIdentifiersForEntityType() throws Exception { - // Arrange - String entityType = "TestEntity"; - PersistentIdentifier pid1 = createPersistentIdentifier("pid1", entityType, "entity1", false); - PersistentIdentifier pid2 = createPersistentIdentifier("pid2", entityType, "entity2", false); - List pids = List.of(pid1, pid2); - - when(service.getPersistentIdentifiersByEntityType(entityType)).thenReturn(pids); - - // Act & Assert - mockMvc.perform(get("/api/v1/pids/byEntityType/{entityType}", entityType) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(2))) - .andExpect(jsonPath("$[0].pid", is("pid1"))) - .andExpect(jsonPath("$[1].pid", is("pid2"))) - .andExpect(jsonPath("$[0].entityType", is(entityType))) - .andExpect(jsonPath("$[1].entityType", is(entityType))); - } - - @Test - void getTombstones_shouldReturnTombstones() throws Exception { - // Arrange - PersistentIdentifier pid1 = createPersistentIdentifier("pid1", "TestEntity", "entity1", true); - PersistentIdentifier pid2 = createPersistentIdentifier("pid2", "TestEntity", "entity2", true); - List pids = List.of(pid1, pid2); - - when(service.getTombstones()).thenReturn(pids); - - // Act & Assert - mockMvc.perform(get("/api/v1/pids/tombstones") - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(2))) - .andExpect(jsonPath("$[0].pid", is("pid1"))) - .andExpect(jsonPath("$[1].pid", is("pid2"))) - .andExpect(jsonPath("$[0].tombstone", is(true))) - .andExpect(jsonPath("$[1].tombstone", is(true))); - } - - private PersistentIdentifier createPersistentIdentifier(String pid, String entityType, String entityInternalId, boolean tombstone) { - PersistentIdentifier persistentIdentifier = new PersistentIdentifier(); - persistentIdentifier.setPid(pid); - persistentIdentifier.setEntityType(entityType); - persistentIdentifier.setEntityInternalId(entityInternalId); - persistentIdentifier.setTombstone(tombstone); - if (tombstone) { - persistentIdentifier.setDeletedAt(Instant.now()); - } - persistentIdentifier.setCreatedAt(Instant.now()); - persistentIdentifier.setLastModifiedAt(Instant.now()); - persistentIdentifier.setVersion(1L); - persistentIdentifier.setMetadata(new HashMap<>()); - return persistentIdentifier; - } -} \ No newline at end of file diff --git a/src/test/java/edu/kit/datamanager/idoris/pids/web/v1/PidRedirectControllerV2Test.java b/src/test/java/edu/kit/datamanager/idoris/pids/web/v1/PidRedirectControllerV2Test.java deleted file mode 100644 index 9bfdf8e..0000000 --- a/src/test/java/edu/kit/datamanager/idoris/pids/web/v1/PidRedirectControllerV2Test.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright (c) 2025 Karlsruhe Institute of Technology - * - * 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 edu.kit.datamanager.idoris.pids.web.v1; - -import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; -import edu.kit.datamanager.idoris.pids.entities.PersistentIdentifier; -import edu.kit.datamanager.idoris.pids.services.PersistentIdentifierService; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; - -import java.time.Instant; -import java.util.HashMap; -import java.util.Optional; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@ExtendWith(MockitoExtension.class) -class PidRedirectControllerV2Test { - - @Mock - private PersistentIdentifierService pidService; - - @InjectMocks - private PidRedirectControllerV2 controller; - - private MockMvc mockMvc; - - @BeforeEach - void setUp() { - mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); - } - - @Test - void redirectToEntity_withExistingPid_shouldRedirectToEntity() throws Exception { - // Arrange - String pidValue = "test-pid"; - String entityType = "testentity"; - String entityId = "entity-id"; - - PersistentIdentifier pid = createPersistentIdentifier(pidValue, "TestEntity", entityId, false); - pid.setEntity(mock(AdministrativeMetadata.class)); - - when(pidService.getPersistentIdentifier(pidValue)).thenReturn(Optional.of(pid)); - - // Act & Assert - mockMvc.perform(get("/pid/{pidValue}", pidValue)) - .andExpect(status().isFound()) - .andExpect(header().string("Location", "/" + entityType + "s/" + entityId)); - } - - @Test - void redirectToEntity_withTombstonePid_shouldRedirectToTombstone() throws Exception { - // Arrange - String pidValue = "test-pid"; - - PersistentIdentifier pid = createPersistentIdentifier(pidValue, "TestEntity", "entity-id", true); - - when(pidService.getPersistentIdentifier(pidValue)).thenReturn(Optional.of(pid)); - - // Act & Assert - mockMvc.perform(get("/pid/{pidValue}", pidValue)) - .andExpect(status().isFound()) - .andExpect(header().string("Location", "/tombstone/" + pidValue)); - } - - @Test - void redirectToEntity_withNonExistingPid_shouldReturnNotFound() throws Exception { - // Arrange - String pidValue = "non-existing-pid"; - - when(pidService.getPersistentIdentifier(pidValue)).thenReturn(Optional.empty()); - - // Act & Assert - mockMvc.perform(get("/pid/{pidValue}", pidValue)) - .andExpect(status().isNotFound()); - } - - @Test - void redirectToEntity_withPidWithoutEntityAndNotTombstone_shouldReturnNotFound() throws Exception { - // Arrange - String pidValue = "test-pid"; - - PersistentIdentifier pid = createPersistentIdentifier(pidValue, "TestEntity", "entity-id", false); - pid.setEntity(null); - - when(pidService.getPersistentIdentifier(pidValue)).thenReturn(Optional.of(pid)); - - // Act & Assert - mockMvc.perform(get("/pid/{pidValue}", pidValue)) - .andExpect(status().isNotFound()); - } - - @Test - void handleTombstone_withTombstonePid_shouldReturnGoneWithMessage() throws Exception { - // Arrange - String pidValue = "test-pid"; - Instant deletedAt = Instant.parse("2023-01-01T00:00:00Z"); - - PersistentIdentifier pid = createPersistentIdentifier(pidValue, "TestEntity", "entity-id", true); - pid.setDeletedAt(deletedAt); - - when(pidService.getPersistentIdentifier(pidValue)).thenReturn(Optional.of(pid)); - - // Act & Assert - mockMvc.perform(get("/pid/tombstone/{pidValue}", pidValue)) - .andExpect(status().isGone()) - .andExpect(content().string("The entity with PID test-pid has been deleted at 2023-01-01T00:00:00Z. Entity type: TestEntity")); - } - - @Test - void handleTombstone_withNonTombstonePid_shouldRedirectToEntity() throws Exception { - // Arrange - String pidValue = "test-pid"; - - PersistentIdentifier pid = createPersistentIdentifier(pidValue, "TestEntity", "entity-id", false); - - when(pidService.getPersistentIdentifier(pidValue)).thenReturn(Optional.of(pid)); - - // Act & Assert - mockMvc.perform(get("/pid/tombstone/{pidValue}", pidValue)) - .andExpect(status().isFound()) - .andExpect(header().string("Location", "/pid/" + pidValue)); - } - - @Test - void handleTombstone_withNonExistingPid_shouldReturnNotFound() throws Exception { - // Arrange - String pidValue = "non-existing-pid"; - - when(pidService.getPersistentIdentifier(pidValue)).thenReturn(Optional.empty()); - - // Act & Assert - mockMvc.perform(get("/pid/tombstone/{pidValue}", pidValue)) - .andExpect(status().isNotFound()); - } - - private PersistentIdentifier createPersistentIdentifier(String pid, String entityType, String entityInternalId, boolean tombstone) { - PersistentIdentifier persistentIdentifier = new PersistentIdentifier(); - persistentIdentifier.setPid(pid); - persistentIdentifier.setEntityType(entityType); - persistentIdentifier.setEntityInternalId(entityInternalId); - persistentIdentifier.setTombstone(tombstone); - if (tombstone) { - persistentIdentifier.setDeletedAt(Instant.now()); - } - persistentIdentifier.setCreatedAt(Instant.now()); - persistentIdentifier.setLastModifiedAt(Instant.now()); - persistentIdentifier.setVersion(1L); - persistentIdentifier.setMetadata(new HashMap<>()); - return persistentIdentifier; - } -} \ No newline at end of file From bc2309ece266bb9c669c2d4e08260330fb882983 Mon Sep 17 00:00:00 2001 From: Maximilian Inckmann Date: Fri, 18 Jul 2025 17:15:31 +0200 Subject: [PATCH 07/19] added observability and formatter for TPM API ("%2F" -> "/") Signed-off-by: Maximilian Inckmann --- .gitignore | 3 + build.gradle | 21 ++- docker-compose-observability.yml | 108 +++++++++++++ observability/alloy/alloy-config.alloy | 127 +++++++++++++++ .../provisioning/datasources/datasources.yml | 41 +++++ observability/loki/loki-config.yml | 33 ++++ observability/prometheus/prometheus.yml | 16 ++ observability/tempo/tempo-config.yml | 44 +++++ .../datamanager/idoris/IdorisApplication.java | 4 +- .../idoris/pids/MetadataEventListener.java | 7 + .../pids/client/TypedPIDMakerClient.java | 14 +- .../client/TypedPIDMakerClientConfig.java | 151 +++++++++++++++++- .../services/PersistentIdentifierService.java | 6 + src/main/resources/application.properties | 58 ++++++- src/main/resources/logback-spring.xml | 41 +++++ src/test/resources/application.properties | 3 - 16 files changed, 660 insertions(+), 17 deletions(-) create mode 100644 docker-compose-observability.yml create mode 100644 observability/alloy/alloy-config.alloy create mode 100644 observability/grafana/provisioning/datasources/datasources.yml create mode 100644 observability/loki/loki-config.yml create mode 100644 observability/prometheus/prometheus.yml create mode 100644 observability/tempo/tempo-config.yml create mode 100644 src/main/resources/logback-spring.xml diff --git a/.gitignore b/.gitignore index d1a70ec..f52e9aa 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ db/ neo4j-data/ neo4j-data-new/ +observability/alloy_data/ +logs/ # Ignore Gradle project-specific cache directory nbproject @@ -258,3 +260,4 @@ gradle-app.setting *.hprof # End of https://www.toptal.com/developers/gitignore/api/intellij+all,macos,java,gradle,webstorm+all,git!/.idea/!/db/ +!/observability/alloy_data/ diff --git a/build.gradle b/build.gradle index d41463a..df43656 100644 --- a/build.gradle +++ b/build.gradle @@ -54,6 +54,14 @@ repositories { maven { url = uri("https://repo.spring.io/milestone") } + // Add Sonatype snapshots repository for alpha versions + maven { + url = uri("https://oss.sonatype.org/content/repositories/snapshots") + } + // Add OpenTelemetry repository + maven { + url = uri("https://oss.jfrog.org/artifactory/oss-snapshot-local") + } } ext { @@ -63,6 +71,10 @@ ext { errorproneJavacVersion = "9+181-r4173-1" httpClientVersion = "5.5" javersVersion = "7.3.7" + micrometerVersion = "1.12.5" + openTelemetryVersion = "1.49.0" + openTelemetryInstrumentationVersion = "2.16.0" + logbackVersion = "1.5.13" set("snippetsDir", file("build/generated-snippets")) set('springModulithVersion', "1.4.1") } @@ -88,7 +100,7 @@ dependencies { runtimeOnly 'org.springframework.modulith:spring-modulith-runtime' runtimeOnly "org.springframework.modulith:spring-modulith-observability:${springModulithVersion}" runtimeOnly "org.springframework.modulith:spring-modulith-actuator:${springModulithVersion}" -// runtimeOnly "org.springframework.modulith:spring-modulith-starter-insights:${springModulithVersion}" + runtimeOnly "org.springframework.modulith:spring-modulith-starter-insight:${springModulithVersion}" /* OpenAPI */ implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${springDocVersion}" @@ -98,6 +110,12 @@ dependencies { /* HTTP client */ implementation "org.apache.httpcomponents.client5:httpclient5:${httpClientVersion}" + + /* Observability */ + implementation "io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter" + implementation 'io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations' + implementation "org.springframework.boot:spring-boot-starter-aop" + /* Development helpers */ implementation "org.springframework.boot:spring-boot-configuration-processor" developmentOnly "org.springframework.boot:spring-boot-devtools" @@ -129,6 +147,7 @@ dependencies { dependencyManagement { imports { mavenBom "org.springframework.modulith:spring-modulith-bom:${springModulithVersion}" + mavenBom("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom:2.17.1") } } diff --git a/docker-compose-observability.yml b/docker-compose-observability.yml new file mode 100644 index 0000000..035ead2 --- /dev/null +++ b/docker-compose-observability.yml @@ -0,0 +1,108 @@ +version: '3.8' + +services: + # Prometheus for metrics collection + prometheus: + image: prom/prometheus:latest + container_name: prometheus + ports: + - "9090:9090" + volumes: + - ./observability/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus_data:/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--web.console.libraries=/etc/prometheus/console_libraries' + - '--web.console.templates=/etc/prometheus/consoles' + - '--web.enable-lifecycle' + - '--web.enable-remote-write-receiver' + restart: unless-stopped + networks: + - observability-network + + # Loki for log aggregation + loki: + image: grafana/loki:latest + container_name: loki + ports: + - "3100:3100" + volumes: + - ./observability/loki/loki-config.yml:/etc/loki/local-config.yaml + - loki_data:/loki + command: -config.file=/etc/loki/local-config.yaml + restart: unless-stopped + networks: + - observability-network + + # Grafana Alloy for unified observability data collection + alloy: + image: grafana/alloy:latest + container_name: alloy + volumes: + - ./observability/alloy/alloy-config.alloy:/etc/alloy/config.alloy + - /var/log:/var/log + - ./logs:/logs + - ./observability/alloy_data:/var/lib/alloy/data + command: [ "run", "--storage.path=/var/lib/alloy/data", "--server.http.listen-addr=0.0.0.0:12345", "/etc/alloy/config.alloy" ] + restart: unless-stopped + ports: + - "4317:4317" # OTLP gRPC + - "4318:4318" # OTLP HTTP + - "12345:12345" # Alloy UI + depends_on: + - loki + - prometheus + networks: + - observability-network + + # Tempo for distributed tracing + tempo: + image: grafana/tempo:latest + container_name: tempo + command: [ "-config.file=/etc/tempo/tempo-config.yml" ] + user: root # Run as root to avoid permission issues + volumes: + - ./observability/tempo/tempo-config.yml:/etc/tempo/tempo-config.yml + - tempo_data:/tmp/tempo + ports: + - "3200:3200" # tempo + - "4319:4319" # OTLP gRPC (changed from 4317 to avoid conflict with Alloy) + - "4320:4320" # OTLP HTTP (changed from 4318 to avoid conflict with Alloy) + - "9411:9411" # Zipkin + restart: unless-stopped + networks: + - observability-network + + # Grafana for visualization + grafana: + image: grafana/grafana:latest + container_name: grafana + ports: + - "3000:3000" + volumes: + - ./observability/grafana/provisioning:/etc/grafana/provisioning + - ./observability/grafana/dashboards:/var/lib/grafana/dashboards + - grafana_data:/var/lib/grafana + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_USERS_ALLOW_SIGN_UP=false + - GF_INSTALL_PLUGINS=grafana-piechart-panel + restart: unless-stopped + depends_on: + - prometheus + - loki + - tempo + networks: + - observability-network + +volumes: + prometheus_data: + loki_data: + tempo_data: + grafana_data: + +networks: + observability-network: + driver: bridge \ No newline at end of file diff --git a/observability/alloy/alloy-config.alloy b/observability/alloy/alloy-config.alloy new file mode 100644 index 0000000..51e639e --- /dev/null +++ b/observability/alloy/alloy-config.alloy @@ -0,0 +1,127 @@ +// Grafana Alloy Configuration +// This uses Alloy's native configuration format, not OpenTelemetry Collector format + +// File log collection for typedpidmaker +loki.source.file "tpm" { + targets = [ + {__path__ = "/var/log/*log"}, + {__path__ = "/logs/*.log"}, + {__path__ = "/logs/typedpidmaker*.log"}, + ] + forward_to = [loki.process.tpm.receiver] +} + +// Add labels to logs +loki.process "tpm" { + forward_to = [loki.write.loki.receiver] + + stage.static_labels { + values = { + service_name = "typed-pid-maker", + environment = "default", + log_source = "alloy", + } + } +} + +// File log collection for idoris +loki.source.file "idoris" { + targets = [ + {__path__ = "/var/log/*log"}, + {__path__ = "/logs/*.log"}, + {__path__ = "/logs/idoris*.log"}, + ] + forward_to = [loki.process.idoris.receiver] +} + +// Add labels to logs +loki.process "idoris" { + forward_to = [loki.write.loki.receiver] + + stage.static_labels { + values = { + service_name = "idoris", + environment = "default", + log_source = "alloy", + } + } +} + +// Export logs to Loki +loki.write "loki" { + endpoint { + url = "http://loki:3100/loki/api/v1/push" + } +} + +prometheus.scrape "prometheus_metrics" { + targets = [ + {"__address__" = "prometheus:9090"}, + ] + forward_to = [prometheus.remote_write.prometheus.receiver] + scrape_interval = "15s" + scrape_timeout = "10s" // Must be less than scrape_interval + job_name = "prometheus" +} + +prometheus.scrape "tempo_metrics" { + targets = [ + {"__address__" = "tempo:3200"}, + ] + forward_to = [prometheus.remote_write.prometheus.receiver] + scrape_interval = "15s" + scrape_timeout = "10s" // Must be less than scrape_interval + job_name = "tempo" +} + +// Export metrics to Prometheus +prometheus.remote_write "prometheus" { + endpoint { + url = "http://prometheus:9090/api/v1/write" + } +} + +// OTLP receiver for traces and metrics +otelcol.receiver.otlp "default" { + grpc { + endpoint = "0.0.0.0:4317" + } + http { + endpoint = "0.0.0.0:4318" + } + + output { + metrics = [otelcol.processor.batch.default.input] + traces = [otelcol.processor.batch.default.input] + logs = [otelcol.processor.batch.default.input] + } +} + +// Batch processor +otelcol.processor.batch "default" { + output { + metrics = [otelcol.exporter.prometheus.default.input] + traces = [otelcol.exporter.otlp.tempo.input] + logs = [otelcol.exporter.loki.default.input] + } +} + +// Export metrics via Prometheus exporter +otelcol.exporter.prometheus "default" { + forward_to = [prometheus.remote_write.prometheus.receiver] +} + +// Export traces to Tempo +otelcol.exporter.otlp "tempo" { + client { + endpoint = "tempo:4319" + tls { + insecure = true + } + } +} + +// Export OTLP logs to Loki +otelcol.exporter.loki "default" { + forward_to = [loki.write.loki.receiver] +} \ No newline at end of file diff --git a/observability/grafana/provisioning/datasources/datasources.yml b/observability/grafana/provisioning/datasources/datasources.yml new file mode 100644 index 0000000..53ae7f7 --- /dev/null +++ b/observability/grafana/provisioning/datasources/datasources.yml @@ -0,0 +1,41 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: true + + - name: Loki + type: loki + access: proxy + url: http://loki:3100 + editable: true + jsonData: + derivedFields: + - name: "traceID" + matcherRegex: "traceID=(\\w+)" + url: "${__value.raw}" + datasourceUid: "tempo" + + - name: Tempo + type: tempo + access: proxy + url: http://tempo:3200 + editable: true + uid: tempo + jsonData: + httpMethod: GET + tracesToLogs: + datasourceUid: 'Loki' + tags: [ 'job', 'instance', 'pod', 'namespace' ] + mappedTags: [ { key: 'service.name', value: 'service' } ] + mapTagNamesEnabled: false + spanStartTimeShift: '1h' + spanEndTimeShift: '1h' + filterByTraceID: true + filterBySpanID: false + serviceMap: + datasourceUid: 'Prometheus' \ No newline at end of file diff --git a/observability/loki/loki-config.yml b/observability/loki/loki-config.yml new file mode 100644 index 0000000..0be43e2 --- /dev/null +++ b/observability/loki/loki-config.yml @@ -0,0 +1,33 @@ +auth_enabled: false + +server: + http_listen_port: 3100 + grpc_listen_port: 9096 + +common: + path_prefix: /loki + storage: + filesystem: + chunks_directory: /loki/chunks + rules_directory: /loki/rules + replication_factor: 1 + ring: + instance_addr: 127.0.0.1 + kvstore: + store: inmemory + +schema_config: + configs: + - from: 2020-10-24 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + +ruler: + alertmanager_url: http://localhost:9093 + +analytics: + reporting_enabled: false \ No newline at end of file diff --git a/observability/prometheus/prometheus.yml b/observability/prometheus/prometheus.yml new file mode 100644 index 0000000..f606125 --- /dev/null +++ b/observability/prometheus/prometheus.yml @@ -0,0 +1,16 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: [ 'localhost:9090' ] + + - job_name: 'tempo' + static_configs: + - targets: [ 'tempo:3200' ] + +rule_files: +# - "first_rules.yml" +# - "second_rules.yml" \ No newline at end of file diff --git a/observability/tempo/tempo-config.yml b/observability/tempo/tempo-config.yml new file mode 100644 index 0000000..77cea6d --- /dev/null +++ b/observability/tempo/tempo-config.yml @@ -0,0 +1,44 @@ +server: + http_listen_port: 3200 + +distributor: + receivers: + otlp: + protocols: + http: + endpoint: "0.0.0.0:4320" + grpc: + endpoint: "0.0.0.0:4319" + jaeger: + protocols: + thrift_http: + endpoint: "0.0.0.0:14268" + zipkin: + endpoint: "0.0.0.0:9411" + +storage: + trace: + backend: local + block: + bloom_filter_false_positive: .05 + wal: + path: /tmp/tempo/wal + local: + path: /tmp/tempo/blocks + pool: + max_workers: 100 + queue_depth: 10000 + +metrics_generator: + registry: + external_labels: + source: tempo + cluster: docker-compose + storage: + path: /tmp/tempo/generator/wal + remote_write: + - url: http://prometheus:9090/api/v1/write + send_exemplars: true + +overrides: + metrics_generator_processors: [ service-graphs, span-metrics ] \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/IdorisApplication.java b/src/main/java/edu/kit/datamanager/idoris/IdorisApplication.java index b442ae6..370b7d7 100644 --- a/src/main/java/edu/kit/datamanager/idoris/IdorisApplication.java +++ b/src/main/java/edu/kit/datamanager/idoris/IdorisApplication.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Karlsruhe Institute of Technology + * Copyright (c) 2024-2025 Karlsruhe Institute of Technology * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.data.neo4j.config.EnableNeo4jAuditing; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; +import org.springframework.modulith.Modulith; import org.springframework.transaction.annotation.EnableTransactionManagement; @SpringBootApplication @@ -36,6 +37,7 @@ @EntityScan("edu.kit.datamanager") @org.springframework.context.annotation.Configuration @Log +@Modulith public class IdorisApplication { public static void main(String[] args) { SpringApplication.run(IdorisApplication.class, args); diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/MetadataEventListener.java b/src/main/java/edu/kit/datamanager/idoris/pids/MetadataEventListener.java index fc2de08..ec5f7d8 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/MetadataEventListener.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/MetadataEventListener.java @@ -23,6 +23,9 @@ import edu.kit.datamanager.idoris.core.events.EventPublisherService; import edu.kit.datamanager.idoris.pids.entities.PersistentIdentifier; import edu.kit.datamanager.idoris.pids.services.PersistentIdentifierService; +import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.annotations.WithSpan; import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @@ -37,6 +40,7 @@ */ @Component @Slf4j +@Observed public class MetadataEventListener { private final PersistentIdentifierService pidService; private final EventPublisherService eventPublisher; @@ -61,6 +65,7 @@ public MetadataEventListener(PersistentIdentifierService pidService, EventPublis */ @EventListener(classes = {EntityCreatedEvent.class}) @Transactional + @WithSpan(kind = SpanKind.CONSUMER) public void handleEntityCreatedEvent(EntityCreatedEvent event) { AdministrativeMetadata entity = event.getEntity(); log.debug("Handling EntityCreatedEvent for entity: {}", entity); @@ -91,6 +96,7 @@ public void handleEntityCreatedEvent(EntityCreatedEvent */ @EventListener(classes = {EntityUpdatedEvent.class}) @Transactional + @WithSpan(kind = SpanKind.CONSUMER) public void handleEntityUpdatedEvent(EntityUpdatedEvent event) { AdministrativeMetadata entity = event.getEntity(); log.debug("Handling EntityUpdatedEvent for entity: {}", entity); @@ -118,6 +124,7 @@ public void handleEntityUpdatedEvent(EntityUpdatedEvent */ @EventListener(classes = {EntityDeletedEvent.class}) @Transactional + @WithSpan(kind = SpanKind.CONSUMER) public void handleEntityDeletedEvent(EntityDeletedEvent event) { AdministrativeMetadata entity = event.getEntity(); log.debug("Handling EntityDeletedEvent for entity: {}", entity); diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClient.java b/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClient.java index a90ab1f..099c19d 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClient.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClient.java @@ -17,6 +17,10 @@ package edu.kit.datamanager.idoris.pids.client; import edu.kit.datamanager.idoris.pids.client.model.PIDRecord; +import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.annotations.WithSpan; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.ResponseBody; @@ -30,6 +34,7 @@ * This interface defines the operations for interacting with the service. */ @HttpExchange("/api/v1/pit/pid") +@Observed public interface TypedPIDMakerClient { /** @@ -40,7 +45,8 @@ public interface TypedPIDMakerClient { */ @PostExchange(value = "/", accept = "application/vnd.datamanager.pid.simple+json", contentType = "application/vnd.datamanager.pid.simple+json") @ResponseBody - PIDRecord createPIDRecord(@RequestBody PIDRecord record); + @WithSpan(kind = SpanKind.CLIENT) + PIDRecord createPIDRecord(@SpanAttribute @RequestBody PIDRecord record); /** @@ -51,7 +57,8 @@ public interface TypedPIDMakerClient { */ @GetExchange(value = "/{pid}", accept = "application/vnd.datamanager.pid.simple+json") @ResponseBody - PIDRecord getPIDRecord(@PathVariable String pid); + @WithSpan(kind = SpanKind.CLIENT) + PIDRecord getPIDRecord(@SpanAttribute @PathVariable String pid); /** * Updates an existing PID record using the SimplePidRecord format. @@ -62,5 +69,6 @@ public interface TypedPIDMakerClient { */ @PutExchange(value = "/{pid}", accept = "application/vnd.datamanager.pid.simple+json", contentType = "application/vnd.datamanager.pid.simple+json") @ResponseBody - PIDRecord updatePIDRecord(@PathVariable String pid, @RequestBody PIDRecord record); + @WithSpan(kind = SpanKind.CLIENT) + PIDRecord updatePIDRecord(@SpanAttribute @PathVariable String pid, @SpanAttribute @RequestBody PIDRecord record); } diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClientConfig.java b/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClientConfig.java index 70cb2f8..18751ad 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClientConfig.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClientConfig.java @@ -18,21 +18,41 @@ import com.fasterxml.jackson.databind.ObjectMapper; import edu.kit.datamanager.idoris.configuration.TypedPIDMakerConfig; +import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.annotations.WithSpan; +import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClientException; import org.springframework.web.client.support.RestClientAdapter; import org.springframework.web.service.invoker.HttpServiceProxyFactory; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; + /** * Configuration class for the TypedPIDMakerClient. * This class registers the TypedPIDMakerClient as a bean in the Spring application context. */ @Configuration @ConditionalOnBean(TypedPIDMakerConfig.class) +@Observed +@Slf4j public class TypedPIDMakerClientConfig { /** @@ -42,17 +62,74 @@ public class TypedPIDMakerClientConfig { * @return The TypedPIDMakerClient */ @Bean + @WithSpan(kind = SpanKind.CLIENT) public TypedPIDMakerClient typedPIDMakerClient(TypedPIDMakerConfig config, ObjectMapper objectMapper) { // Create a client HTTP request factory with the configured timeout - org.springframework.http.client.ClientHttpRequestFactory requestFactory = - new org.springframework.http.client.SimpleClientHttpRequestFactory(); + ClientHttpRequestFactory requestFactory = new DecodingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()); RestClient restClient = RestClient.builder() .baseUrl(config.getBaseUrl()) + .requestInterceptor((request, body, execution) -> { + // Manual trace header injection + Span currentSpan = Span.current(); + if (currentSpan != null && currentSpan.getSpanContext().isValid()) { + SpanContext spanContext = currentSpan.getSpanContext(); + String traceParent = String.format("00-%s-%s-%s", + spanContext.getTraceId(), + spanContext.getSpanId(), + spanContext.getTraceFlags().asHex()); + request.getHeaders().add("traceparent", traceParent); + + // Add tracestate if available + if (!spanContext.getTraceState().isEmpty()) { + request.getHeaders().add("tracestate", spanContext.getTraceState().toString()); + } + + currentSpan.setAttribute("request.method", request.getMethod().toString()); + currentSpan.setAttribute("request.url", request.getURI().toString()); + currentSpan.setAttribute("request.headers", request.getHeaders().toString()); + currentSpan.setAttribute("request.body", new String(body)); + } + + log.debug("Outgoing request headers: {}", request.getHeaders()); + + // Execute the request and capture the response + ClientHttpResponse response = execution.execute(request, body); + + // Add response attributes to the span + if (currentSpan != null && currentSpan.getSpanContext().isValid()) { + try { + currentSpan.setAttribute("response.status_code", response.getStatusCode().value()); + currentSpan.setAttribute("response.status_text", response.getStatusText()); + currentSpan.setAttribute("response.headers", response.getHeaders().toString()); + + // Read the response body for span attributes + // Note: This creates a buffered response to avoid consuming the stream + byte[] bodyBytes = response.getBody().readAllBytes(); + String responseBody = new String(bodyBytes, StandardCharsets.UTF_8); + currentSpan.setAttribute("response.body", responseBody); + + log.debug("Response status: {}, headers: {}, body: {}", + response.getStatusCode(), response.getHeaders(), responseBody); + + // Return a new response with the buffered body + return new BufferedClientHttpResponse(response, bodyBytes); + } catch (IOException e) { + log.warn("Failed to read response body for span attributes", e); + currentSpan.setAttribute("response.status_code", response.getStatusCode().value()); + currentSpan.setAttribute("response.status_text", response.getStatusText()); + currentSpan.setAttribute("response.headers", response.getHeaders().toString()); + currentSpan.setAttribute("response.body.error", "Failed to read response body: " + e.getMessage()); + } + } + + return response; + + }) .requestFactory(requestFactory) - .defaultHeaders(headers -> headers.setAccept(java.util.List.of(org.springframework.http.MediaType.parseMediaType("application/vnd.datamanager.pid.simple+json")))) - .defaultStatusHandler(org.springframework.http.HttpStatusCode::isError, (request, response) -> { - throw new org.springframework.web.client.RestClientException(String.format("Error response from Typed PID Maker: %s %s", response.getStatusCode(), response.getStatusText())); + .defaultHeaders(headers -> headers.setAccept(java.util.List.of(MediaType.parseMediaType("application/vnd.datamanager.pid.simple+json")))) + .defaultStatusHandler(HttpStatusCode::isError, (request, response) -> { + throw new RestClientException(String.format("Error response from Typed PID Maker: %s %s %s", response.getStatusCode(), response.getStatusText(), response)); }) .messageConverters(converters -> { // Add a custom converter for application/octet-stream to handle binary data @@ -72,4 +149,66 @@ protected boolean canRead(MediaType mediaType) { return factory.createClient(TypedPIDMakerClient.class); } -} \ No newline at end of file + + /** + * This record wraps a ClientHttpRequestFactory to decode slashes in URIs. + * This is necessary because some PID services may encode slashes in URIs as %2F. + * + * @param delegate the original ClientHttpRequestFactory to delegate to. + */ + private record DecodingClientHttpRequestFactory( + ClientHttpRequestFactory delegate) implements ClientHttpRequestFactory { + + @Override + public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException { + URI decodedUri = decodeSlashes(uri); + return delegate.createRequest(decodedUri, httpMethod); + } + + private URI decodeSlashes(URI uri) { + String uriString = uri.toString(); + if (uriString.contains("%2F")) { + String decodedUriString = uriString.replace("%2F", "/"); + try { + return new URI(decodedUriString); + } catch (URISyntaxException e) { + // If decoding fails, return the original URI + return uri; + } + } + return uri; + } + } + + /** + * A wrapper for ClientHttpResponse that allows the body to be read multiple times. + */ + private record BufferedClientHttpResponse(ClientHttpResponse delegate, + byte[] bufferedBody) implements ClientHttpResponse { + + @Override + public HttpStatusCode getStatusCode() throws IOException { + return delegate.getStatusCode(); + } + + @Override + public String getStatusText() throws IOException { + return delegate.getStatusText(); + } + + @Override + public void close() { + delegate.close(); + } + + @Override + public java.io.InputStream getBody() throws IOException { + return new java.io.ByteArrayInputStream(bufferedBody); + } + + @Override + public org.springframework.http.HttpHeaders getHeaders() { + return delegate.getHeaders(); + } + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/services/PersistentIdentifierService.java b/src/main/java/edu/kit/datamanager/idoris/pids/services/PersistentIdentifierService.java index e406a9a..40b0073 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/services/PersistentIdentifierService.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/services/PersistentIdentifierService.java @@ -24,6 +24,8 @@ import edu.kit.datamanager.idoris.pids.entities.PersistentIdentifier; import edu.kit.datamanager.idoris.pids.repositories.PersistentIdentifierRepository; import edu.kit.datamanager.idoris.pids.utils.PIDRecordMapper; +import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.instrumentation.annotations.WithSpan; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -40,6 +42,7 @@ */ @Service @Slf4j +@Observed public class PersistentIdentifierService { private final PersistentIdentifierRepository repository; @@ -75,6 +78,7 @@ public PersistentIdentifierService(PersistentIdentifierRepository repository, * @return The created PersistentIdentifier */ @Transactional + @WithSpan public PersistentIdentifier createPersistentIdentifier(AdministrativeMetadata entity) { log.debug("Creating PersistentIdentifier for entity: {}", entity); @@ -136,6 +140,7 @@ public PersistentIdentifier createPersistentIdentifier(AdministrativeMetadata en * @return The updated PersistentIdentifier */ @Transactional + @WithSpan public PersistentIdentifier updatePIDRecord(PersistentIdentifier pid) { log.debug("Updating PID record for PersistentIdentifier: {}", pid); @@ -157,6 +162,7 @@ public PersistentIdentifier updatePIDRecord(PersistentIdentifier pid) { * @return The updated PersistentIdentifier, or empty if no PID exists for the entity */ @Transactional + @WithSpan public Optional markAsTombstone(AdministrativeMetadata entity) { log.debug("Marking PersistentIdentifier as tombstone for entity: {}", entity); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 604a11e..43e7b72 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -14,14 +14,66 @@ # limitations under the License. # spring.application.name=idoris +spring.profiles.active=default + +# Logging Configuration logging.level.root=INFO -logging.level.org.springframework=TRACE -logging.level.edu.kit.datamanager=TRACE +logging.level.org.springframework=DEBUG +logging.level.edu.kit.datamanager=DEBUG +#logging.config=classpath:logback-spring.xml + +# Database Configuration spring.neo4j.uri=bolt://localhost:7687 spring.neo4j.authentication.username=neo4j spring.neo4j.authentication.password=superSecret + +# Actuator and Metrics Configuration +# Expose only necessary endpoints for security +#management.endpoints.web.exposure.include=health,info,metrics management.endpoints.web.exposure.include=* -server.tomcat.relaxed-path-chars=/ +management.endpoint.health.show-details=always +# Disable Prometheus endpoint as we're using OTLP for metrics +management.endpoint.prometheus.enabled=false +management.metrics.distribution.percentiles-histogram.http.server.requests=true +management.metrics.tags.application=${spring.application.name} +management.metrics.tags.environment=${spring.profiles.active} + +# OpenTelemetry Logging Configuration +management.otlp.logging.export.enabled=true +management.otlp.logging.endpoint=http://localhost:4318/v1/logs +otel.instrumentation.logback-appender.experimental.capture-mdc-attributes=* +otel.instrumentation.logback-appender.experimental-log-attributes=true +otel.instrumentation.logback-appender.experimental.capture-code-attributes=true +otel.instrumentation.logback-appender.experimental.capture-marker-attribute=true + +# OpenTelemetry Metrics Configuration +management.otlp.metrics.export.enabled=true +management.otlp.metrics.export.step=10s +management.otlp.metrics.export.url=http://localhost:4318/v1/metrics + +# Tracing Configuration +management.tracing.sampling.probability=1.0 +management.otlp.tracing.endpoint=http://localhost:4318/v1/traces +management.httpexchanges.recording.enabled=true +management.tracing.baggage.correlation.enabled=true +management.tracing.opentelemetry.export.include-unsampled=true +management.observations.annotations.enabled=true +otel.instrumentation.http.client.emit-experimental-telemetry=true +otel.instrumentation.runtime-telemetry-java17.enabled=true +otel.instrumentation.spring-webmvc.enabled=true +otel.instrumentation.annotations.enabled=true +otel.instrumentation.http.client.capture-request-headers=true +otel.instrumentation.http.client.capture-response-headers=true +otel.instrumentation.http.client.experimental.redact-query-parameters=false +otel.instrumentation.jdbc.experimental.transaction.enabled=true +otel.propagators=tracecontext,baggage +otel.traces.sampler=parentbased_traceidratio +otel.traces.sampler.arg=1 + + +# Spring Modulith Observability Configuration +spring.modulith.events.externalization.enabled=true + # Spring Doc Settings for OpenAPI documentation springdoc.show-actuator=true springdoc.api-docs.path=/v1/api-docs diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..484785a --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + true + * + true + true + + + + + + + \ No newline at end of file diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 071ba03..e0168a8 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -25,8 +25,5 @@ server.servlet.context-path=/api server.port=8095 idoris.validation-level=info idoris.validation-policy=strict -idoris.pid-generation=TYPED_PID_MAKER idoris.typed-pid-maker.base-url=http://localhost:8090 idoris.typed-pid-maker.timeout=5000 -idoris.typed-pid-maker.meaningful-pid-records=true -idoris.typed-pid-maker.update-pid-records=true From 9ec2d1490d0ce52c4206aa78c680ec2b2e2f8340 Mon Sep 17 00:00:00 2001 From: Maximilian Inckmann Date: Thu, 7 Aug 2025 10:42:32 +0200 Subject: [PATCH 08/19] added AcyclicityValidator Signed-off-by: Maximilian Inckmann --- .../datatypes/rules/AcyclicityValidator.java | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 src/main/java/edu/kit/datamanager/idoris/datatypes/rules/AcyclicityValidator.java diff --git a/src/main/java/edu/kit/datamanager/idoris/datatypes/rules/AcyclicityValidator.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/rules/AcyclicityValidator.java new file mode 100644 index 0000000..798cee5 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/rules/AcyclicityValidator.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * 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 edu.kit.datamanager.idoris.datatypes.rules; + +import edu.kit.datamanager.idoris.datatypes.entities.AtomicDataType; +import edu.kit.datamanager.idoris.datatypes.entities.DataType; +import edu.kit.datamanager.idoris.datatypes.entities.TypeProfile; +import edu.kit.datamanager.idoris.rules.logic.Rule; +import edu.kit.datamanager.idoris.rules.validation.SyntaxValidator; +import edu.kit.datamanager.idoris.rules.validation.ValidationResult; +import edu.kit.datamanager.idoris.rules.validation.ValidationVisitor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.neo4j.core.Neo4jClient; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@Slf4j +@Component +@Rule( + appliesTo = { + edu.kit.datamanager.idoris.datatypes.entities.AtomicDataType.class, + edu.kit.datamanager.idoris.datatypes.entities.TypeProfile.class + }, + name = "AcyclicityValidationRule", + description = "Validates that entities do not form cycles in their inheritance structure", + tasks = {edu.kit.datamanager.idoris.rules.logic.RuleTask.VALIDATE}, + dependsOn = {SyntaxValidator.class} +) +public class AcyclicityValidator extends ValidationVisitor { + @Autowired + private Neo4jClient neo4jClient; + + @Override + public ValidationResult visit(AtomicDataType atomicDataType, Object... args) { + return doesNotInheritItself(atomicDataType); + } + + @Override + public ValidationResult visit(TypeProfile profile, Object... args) { + return ValidationResult.combine( + doesNotInheritItself(profile), + doesNotUseItselfAsAttribute(profile) + ); + } + + /** + * Validates that a DataType (TypeProfile or AtomicDataType) does not inherit from itself, preventing circular inheritance. + * + * @param dataType The DataType to validate + * @return ValidationResult containing any validation errors + */ + private ValidationResult doesNotInheritItself(DataType dataType) { + String query = "MATCH path = (n:DataType {id: $nodeID})-[:inheritsFrom*1..]->(n) RETURN path"; + + // Query the path from the Neo4j database + var path = neo4jClient.query(query) + .bind(dataType.getId()).to("nodeID") + .fetch() + .all(); + + if (!path.isEmpty()) { + return ValidationResult.error("Circular inheritance detected", Map.of("element", dataType, "path", path)); + } else { + return ValidationResult.ok(); + } + } + + /** + * Validates that a TypeProfile does not use itself as an attribute type, either directly or through overrides. + * + * @param profile The TypeProfile to validate + * @return ValidationResult containing any validation errors + */ + private ValidationResult doesNotUseItselfAsAttribute(TypeProfile profile) { + // Optimized single unified query to check for all types of self-reference cycles + // Using MATCH...WHERE pattern for better readability and performance + String query = "// Direct cycle check" + + "MATCH path = (n:TypeProfile)-[:attributes]->(a:Attribute)-[:dataType]->(dt:DataType)" + + "WHERE n.id = $nodeID AND dt.id = $nodeID " + + "RETURN path, a.id AS attributeId, 'direct' AS cycleType, NULL AS baseAttributeId" + + "UNION" + + "// Attribute override cycle check" + + "MATCH path = (n:TypeProfile)-[:attributes]->(a:Attribute)-[:override*1..]->(b:Attribute)-[:dataType]->(dt:DataType)" + + "WHERE n.id = $nodeID AND dt.id = $nodeID " + + "RETURN path, a.id AS attributeId, 'override' AS cycleType, b.id AS baseAttributeId" + + "UNION" + + "// Data type inheritance cycle check" + + "MATCH path = (n:TypeProfile)-[:attributes]->(a:Attribute)-[:dataType]->(dt1:DataType)-[:inheritsFrom*1..]->(dt2:DataType)" + + "WHERE n.id = $nodeID AND dt2.id = $nodeID " + + "RETURN path, a.id AS attributeId, 'inheritance' AS cycleType, NULL AS baseAttributeId" + + "LIMIT 1"; + + // Execute the query - use fetchAs(Map.class) to get proper type-safe access to results + var result = neo4jClient.query(query) + .bind(profile.getId()).to("nodeID") + .fetchAs(java.util.Map.class) + .one(); + + if (result.isPresent()) { + var map = result.get(); + String cycleType = String.valueOf(map.get("cycleType")); + + String errorMessage = switch (cycleType) { + case "direct" -> "TypeProfile directly uses itself as an attribute type."; + case "override" -> { + String baseAttributeId = map.get("baseAttributeId") != null ? + String.valueOf(map.get("baseAttributeId")) : "Unknown"; + yield "TypeProfile indirectly uses itself through attribute override. Base attribute ID: " + baseAttributeId; + } + case "inheritance" -> "TypeProfile indirectly uses itself through data type inheritance."; + default -> "TypeProfile has a cyclic reference in its attributes."; + }; + + return ValidationResult.error("Illegal path: " + errorMessage, Map.of( + "element", profile, + "path", map.get("path"), + "cycleType", cycleType + )); + } + + return ValidationResult.ok(); + } +} From ef39e9363d96079563a4a6b9dbe8e70334e3dff3 Mon Sep 17 00:00:00 2001 From: Maximilian Inckmann Date: Fri, 15 Aug 2025 13:24:38 +0200 Subject: [PATCH 09/19] fixed observability Signed-off-by: Maximilian Inckmann --- .gitignore | 2 +- build.gradle | 5 + docker-compose-observability.yml | 36 +- observability/alloy/alloy-config.alloy | 17 +- .../grafana/dashboards/idoris-overview.json | 791 ++++++++++++++++++ .../grafana/dashboards/idoris-profiling.json | 429 ++++++++++ .../dashboards/spring-boot-metrics.json | 718 ++++++++++++++++ .../dashboards/typedpidmaker-client.json | 656 +++++++++++++++ .../provisioning/dashboards/dashboards.yml | 13 + .../provisioning/dashboards/spring-boot.yaml | 13 + .../provisioning/datasources/datasources.yml | 34 +- observability/mimir/mimir-config.yaml | 80 ++ observability/prometheus/prometheus.yml | 31 +- .../datamanager/idoris/IdorisApplication.java | 16 + .../attributes/services/AttributeService.java | 34 +- .../web/v1/AttributeController.java | 43 +- .../core/events/EventPublisherService.java | 63 +- .../services/AtomicDataTypeService.java | 31 +- .../services/TypeProfileService.java | 45 +- .../datatypes/web/api/IAtomicDataTypeApi.java | 18 + .../datatypes/web/api/ITypeProfileApi.java | 2 + .../web/v1/AtomicDataTypeController.java | 116 ++- .../web/v1/TypeProfileController.java | 133 ++- .../operations/services/OperationService.java | 40 +- .../web/v1/OperationController.java | 47 +- .../idoris/pids/MetadataEventListener.java | 10 +- .../pids/client/TypedPIDMakerClient.java | 48 +- .../services/PersistentIdentifierService.java | 81 +- .../idoris/pids/utils/PIDRecordMapper.java | 2 +- .../rules/validation/SyntaxValidator.java | 3 +- .../rules/validation/ValidationResult.java | 28 + src/main/resources/application.properties | 111 ++- src/main/resources/log4j2.xml | 27 + src/main/resources/logback-spring.xml | 7 +- 34 files changed, 3514 insertions(+), 216 deletions(-) create mode 100644 observability/grafana/dashboards/idoris-overview.json create mode 100644 observability/grafana/dashboards/idoris-profiling.json create mode 100644 observability/grafana/dashboards/spring-boot-metrics.json create mode 100644 observability/grafana/dashboards/typedpidmaker-client.json create mode 100644 observability/grafana/provisioning/dashboards/dashboards.yml create mode 100644 observability/grafana/provisioning/dashboards/spring-boot.yaml create mode 100644 observability/mimir/mimir-config.yaml create mode 100644 src/main/resources/log4j2.xml diff --git a/.gitignore b/.gitignore index f52e9aa..de27c60 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ db/ neo4j-data/ neo4j-data-new/ -observability/alloy_data/ +observability/ logs/ # Ignore Gradle project-specific cache directory diff --git a/build.gradle b/build.gradle index df43656..ade8e7a 100644 --- a/build.gradle +++ b/build.gradle @@ -75,6 +75,7 @@ ext { openTelemetryVersion = "1.49.0" openTelemetryInstrumentationVersion = "2.16.0" logbackVersion = "1.5.13" + pyroscopeVersion = "0.13.0" set("snippetsDir", file("build/generated-snippets")) set('springModulithVersion', "1.4.1") } @@ -112,9 +113,13 @@ dependencies { /* Observability */ + implementation(platform("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom:2.18.0")) implementation "io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter" implementation 'io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations' + implementation "io.opentelemetry.contrib:opentelemetry-samplers:1.47.0-alpha" implementation "org.springframework.boot:spring-boot-starter-aop" + implementation 'io.micrometer:micrometer-tracing-bridge-otel' + implementation 'io.opentelemetry:opentelemetry-exporter-otlp' /* Development helpers */ implementation "org.springframework.boot:spring-boot-configuration-processor" diff --git a/docker-compose-observability.yml b/docker-compose-observability.yml index 035ead2..4814e99 100644 --- a/docker-compose-observability.yml +++ b/docker-compose-observability.yml @@ -21,6 +21,22 @@ services: networks: - observability-network + # Mimir for scalable metrics storage + mimir: + image: grafana/mimir:latest + container_name: mimir + ports: + - "9009:9009" # HTTP API + - "9095:9095" # gRPC API + - "9093:9093" # Alertmanager API + volumes: + # - ./observability/mimir/mimir-config.yaml:/etc/mimir/config.yaml + - mimir_data:/data + # command: [ "-config.file=/etc/mimir/config.yaml" ] + restart: unless-stopped + networks: + - observability-network + # Loki for log aggregation loki: image: grafana/loki:latest @@ -43,7 +59,7 @@ services: - ./observability/alloy/alloy-config.alloy:/etc/alloy/config.alloy - /var/log:/var/log - ./logs:/logs - - ./observability/alloy_data:/var/lib/alloy/data + - ./observability/alloy/alloy_data:/var/lib/alloy/data command: [ "run", "--storage.path=/var/lib/alloy/data", "--server.http.listen-addr=0.0.0.0:12345", "/etc/alloy/config.alloy" ] restart: unless-stopped ports: @@ -74,6 +90,20 @@ services: networks: - observability-network + # Pyroscope for continuous profiling + pyroscope: + image: grafana/pyroscope:latest + container_name: pyroscope + ports: + - "4040:4040" # HTTP API and UI + volumes: + # - ./observability/pyroscope/pyroscope-config.yaml:/etc/pyroscope/config.yaml + - pyroscope_data:/data + # command: [ "-config.file=/etc/pyroscope/config.yaml" ] + restart: unless-stopped + networks: + - observability-network + # Grafana for visualization grafana: image: grafana/grafana:latest @@ -92,15 +122,19 @@ services: restart: unless-stopped depends_on: - prometheus + - mimir - loki - tempo + - pyroscope networks: - observability-network volumes: prometheus_data: + mimir_data: loki_data: tempo_data: + pyroscope_data: grafana_data: networks: diff --git a/observability/alloy/alloy-config.alloy b/observability/alloy/alloy-config.alloy index 51e639e..39713a8 100644 --- a/observability/alloy/alloy-config.alloy +++ b/observability/alloy/alloy-config.alloy @@ -54,6 +54,8 @@ loki.write "loki" { } } + +// Prometheus scrape configuration for metrics collection prometheus.scrape "prometheus_metrics" { targets = [ {"__address__" = "prometheus:9090"}, @@ -74,14 +76,23 @@ prometheus.scrape "tempo_metrics" { job_name = "tempo" } -// Export metrics to Prometheus +// Export metrics to Mimir +prometheus.remote_write "mimir" { + endpoint { + url = "http://mimir:9009/api/v1/push" + } +} + +// Export metrics to Prometheus (for backward compatibility) prometheus.remote_write "prometheus" { endpoint { url = "http://prometheus:9090/api/v1/write" } } -// OTLP receiver for traces and metrics + + +// OTLP receiver for traces, metrics, and logs otelcol.receiver.otlp "default" { grpc { endpoint = "0.0.0.0:4317" @@ -108,7 +119,7 @@ otelcol.processor.batch "default" { // Export metrics via Prometheus exporter otelcol.exporter.prometheus "default" { - forward_to = [prometheus.remote_write.prometheus.receiver] + forward_to = [prometheus.remote_write.prometheus.receiver, prometheus.remote_write.mimir.receiver] } // Export traces to Tempo diff --git a/observability/grafana/dashboards/idoris-overview.json b/observability/grafana/dashboards/idoris-overview.json new file mode 100644 index 0000000..20f6ed8 --- /dev/null +++ b/observability/grafana/dashboards/idoris-overview.json @@ -0,0 +1,791 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "panels": [], + "title": "IDORIS Overview", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 1 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(http_server_requests_seconds_count{application=\"idoris\"}[5m])) by (uri)", + "legendFormat": "{{uri}}", + "range": true, + "refId": "A" + } + ], + "title": "HTTP Request Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket{application=\"idoris\"}[5m])) by (uri, le))", + "legendFormat": "{{uri}}", + "range": true, + "refId": "A" + } + ], + "title": "HTTP Request Duration (p95)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 9 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(http_server_requests_seconds_count{application=\"idoris\", status=~\"5..\"}[5m])) by (uri)", + "legendFormat": "{{uri}}", + "range": true, + "refId": "A" + } + ], + "title": "HTTP 5xx Error Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 9 + }, + "id": 5, + "options": { + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(jvm_memory_used_bytes{application=\"idoris\", area=\"heap\"}) / sum(jvm_memory_max_bytes{application=\"idoris\", area=\"heap\"}) * 100", + "legendFormat": "Heap Usage", + "range": true, + "refId": "A" + } + ], + "title": "JVM Heap Usage (%)", + "type": "gauge" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 17 + }, + "id": 6, + "panels": [], + "title": "PID Maker Client", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 18 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(pidMakerClient_createPIDRecord_count_total[5m]))", + "legendFormat": "Create PID", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(pidMakerClient_getPIDRecord_count_total[5m]))", + "hide": false, + "legendFormat": "Get PID", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(pidMakerClient_updatePIDRecord_count_total[5m]))", + "hide": false, + "legendFormat": "Update PID", + "range": true, + "refId": "C" + } + ], + "title": "PID Maker Operations Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 18 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum(rate(pidMakerClient_createPIDRecord_seconds_bucket[5m])) by (le))", + "legendFormat": "Create PID (p95)", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum(rate(pidMakerClient_getPIDRecord_seconds_bucket[5m])) by (le))", + "hide": false, + "legendFormat": "Get PID (p95)", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum(rate(pidMakerClient_updatePIDRecord_seconds_bucket[5m])) by (le))", + "hide": false, + "legendFormat": "Update PID (p95)", + "range": true, + "refId": "C" + } + ], + "title": "PID Maker Operation Duration", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 26 + }, + "id": 9, + "panels": [], + "title": "Logs & Traces", + "type": "row" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 27 + }, + "id": 10, + "options": { + "dedupStrategy": "none", + "enableLogDetails": true, + "prettifyLogMessage": false, + "showCommonLabels": false, + "showLabels": false, + "showTime": true, + "sortOrder": "Descending", + "wrapLogMessage": false + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "editorMode": "builder", + "expr": "{service_name=\"idoris\"}", + "queryType": "range", + "refId": "A" + } + ], + "title": "IDORIS Logs", + "type": "logs" + }, + { + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 35 + }, + "id": 11, + "targets": [ + { + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "limit": 20, + "query": "service.name=\"idoris\"", + "refId": "A" + } + ], + "title": "IDORIS Traces", + "type": "traces" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 43 + }, + "id": 12, + "panels": [], + "title": "Profiling", + "type": "row" + }, + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "pyroscope" + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 44 + }, + "id": 13, + "targets": [ + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "pyroscope" + }, + "groupBy": [], + "labelSelector": "{service=\"idoris\"}", + "profileTypeId": "cpu:sample:nanoseconds:cpu:nanoseconds", + "refId": "A" + } + ], + "title": "CPU Profile", + "type": "flamegraph" + }, + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "pyroscope" + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 52 + }, + "id": 14, + "targets": [ + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "pyroscope" + }, + "groupBy": [], + "labelSelector": "{service=\"idoris\"}", + "profileTypeId": "memory:alloc:bytes:objects:bytes", + "refId": "A" + } + ], + "title": "Memory Profile", + "type": "flamegraph" + } + ], + "refresh": "10s", + "schemaVersion": 38, + "style": "dark", + "tags": [ + "idoris", + "overview" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "IDORIS Overview", + "uid": "idoris-overview", + "version": 1, + "weekStart": "" +} \ No newline at end of file diff --git a/observability/grafana/dashboards/idoris-profiling.json b/observability/grafana/dashboards/idoris-profiling.json new file mode 100644 index 0000000..97094d1 --- /dev/null +++ b/observability/grafana/dashboards/idoris-profiling.json @@ -0,0 +1,429 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "panels": [], + "title": "IDORIS Profiling Overview", + "type": "row" + }, + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "pyroscope" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ns" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 1 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "pyroscope" + }, + "groupBy": [ + "service" + ], + "labelSelector": "{service=\"idoris\"}", + "profileTypeId": "cpu:sample:nanoseconds:cpu:nanoseconds", + "refId": "A" + } + ], + "title": "CPU Usage Over Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "pyroscope" + }, + "description": "CPU profile showing where the application is spending CPU time", + "gridPos": { + "h": 12, + "w": 24, + "x": 0, + "y": 9 + }, + "id": 3, + "targets": [ + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "pyroscope" + }, + "groupBy": [], + "labelSelector": "{service=\"idoris\"}", + "profileTypeId": "cpu:sample:nanoseconds:cpu:nanoseconds", + "refId": "A" + } + ], + "title": "CPU Profile", + "type": "flamegraph" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 21 + }, + "id": 4, + "panels": [], + "title": "Memory Profiling", + "type": "row" + }, + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "pyroscope" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 22 + }, + "id": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "pyroscope" + }, + "groupBy": [ + "service" + ], + "labelSelector": "{service=\"idoris\"}", + "profileTypeId": "memory:alloc:bytes:objects:bytes", + "refId": "A" + } + ], + "title": "Memory Allocation Over Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "pyroscope" + }, + "description": "Memory profile showing where the application is allocating memory", + "gridPos": { + "h": 12, + "w": 24, + "x": 0, + "y": 30 + }, + "id": 6, + "targets": [ + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "pyroscope" + }, + "groupBy": [], + "labelSelector": "{service=\"idoris\"}", + "profileTypeId": "memory:alloc:bytes:objects:bytes", + "refId": "A" + } + ], + "title": "Memory Allocation Profile", + "type": "flamegraph" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 42 + }, + "id": 7, + "panels": [], + "title": "Lock Contention", + "type": "row" + }, + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "pyroscope" + }, + "description": "Lock contention profile showing where the application is experiencing synchronization bottlenecks", + "gridPos": { + "h": 12, + "w": 24, + "x": 0, + "y": 43 + }, + "id": 8, + "targets": [ + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "pyroscope" + }, + "groupBy": [], + "labelSelector": "{service=\"idoris\"}", + "profileTypeId": "lock:contentions:count:contentions:count", + "refId": "A" + } + ], + "title": "Lock Contention Profile", + "type": "flamegraph" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 55 + }, + "id": 9, + "panels": [], + "title": "TypedPIDMakerClient Profiling", + "type": "row" + }, + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "pyroscope" + }, + "description": "CPU profile filtered to show only TypedPIDMakerClient methods", + "gridPos": { + "h": 12, + "w": 24, + "x": 0, + "y": 56 + }, + "id": 10, + "targets": [ + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "pyroscope" + }, + "groupBy": [], + "labelSelector": "{service=\"idoris\"}", + "profileTypeId": "cpu:sample:nanoseconds:cpu:nanoseconds", + "query": "edu.kit.datamanager.idoris.pids.client.TypedPIDMakerClient", + "refId": "A" + } + ], + "title": "TypedPIDMakerClient CPU Profile", + "type": "flamegraph" + }, + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "pyroscope" + }, + "description": "Memory profile filtered to show only TypedPIDMakerClient methods", + "gridPos": { + "h": 12, + "w": 24, + "x": 0, + "y": 68 + }, + "id": 11, + "targets": [ + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "pyroscope" + }, + "groupBy": [], + "labelSelector": "{service=\"idoris\"}", + "profileTypeId": "memory:alloc:bytes:objects:bytes", + "query": "edu.kit.datamanager.idoris.pids.client.TypedPIDMakerClient", + "refId": "A" + } + ], + "title": "TypedPIDMakerClient Memory Profile", + "type": "flamegraph" + } + ], + "refresh": "10s", + "schemaVersion": 38, + "style": "dark", + "tags": [ + "idoris", + "profiling", + "pyroscope" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "IDORIS Profiling", + "uid": "idoris-profiling", + "version": 1, + "weekStart": "" +} \ No newline at end of file diff --git a/observability/grafana/dashboards/spring-boot-metrics.json b/observability/grafana/dashboards/spring-boot-metrics.json new file mode 100644 index 0000000..eeb73d1 --- /dev/null +++ b/observability/grafana/dashboards/spring-boot-metrics.json @@ -0,0 +1,718 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "title": "Operation Controller Metrics", + "type": "timeseries", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "operationController_getAllOperations_count_total", + "legendFormat": "GetAll Operations", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "operationController_getOperation_count_total", + "hide": false, + "legendFormat": "Get Operation", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "operationController_createOperation_count_total", + "hide": false, + "legendFormat": "Create Operation", + "range": true, + "refId": "C" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "title": "Atomic Data Type Service Metrics", + "type": "timeseries", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "atomicDataTypeService_createAtomicDataType_count_total", + "legendFormat": "Create Atomic Data Type", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "atomicDataTypeService_getAtomicDataType_count_total", + "hide": false, + "legendFormat": "Get Atomic Data Type", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "atomicDataTypeService_updateAtomicDataType_count_total", + "hide": false, + "legendFormat": "Update Atomic Data Type", + "range": true, + "refId": "C" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "title": "PID Maker Client Metrics", + "type": "timeseries", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "pidMakerClient_createPIDRecord_count_total", + "legendFormat": "Create PID Record", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "pidMakerClient_getPIDRecord_count_total", + "hide": false, + "legendFormat": "Get PID Record", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "pidMakerClient_updatePIDRecord_count_total", + "hide": false, + "legendFormat": "Update PID Record", + "range": true, + "refId": "C" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "title": "Event Publisher Service Metrics", + "type": "timeseries", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "eventPublisherService_publishEntityCreated_count_total", + "legendFormat": "Entity Created Events", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "eventPublisherService_publishEntityUpdated_count_total", + "hide": false, + "legendFormat": "Entity Updated Events", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "eventPublisherService_publishEntityDeleted_count_total", + "hide": false, + "legendFormat": "Entity Deleted Events", + "range": true, + "refId": "C" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "title": "JVM Metrics", + "type": "timeseries", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "jvm_memory_used_bytes{area=\"heap\"}", + "legendFormat": "Heap Memory Used", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "jvm_memory_used_bytes{area=\"nonheap\"}", + "hide": false, + "legendFormat": "Non-Heap Memory Used", + "range": true, + "refId": "B" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 6, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "title": "HTTP Request Metrics", + "type": "timeseries", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "http_server_requests_seconds_count", + "legendFormat": "HTTP Requests", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "http_server_requests_seconds_sum / http_server_requests_seconds_count", + "hide": false, + "legendFormat": "HTTP Request Duration (avg)", + "range": true, + "refId": "B" + } + ] + } + ], + "refresh": "5s", + "schemaVersion": 38, + "style": "dark", + "tags": [ + "spring-boot", + "idoris" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "IDORIS Application Metrics", + "uid": "idoris-metrics", + "version": 1, + "weekStart": "" +} diff --git a/observability/grafana/dashboards/typedpidmaker-client.json b/observability/grafana/dashboards/typedpidmaker-client.json new file mode 100644 index 0000000..151708d --- /dev/null +++ b/observability/grafana/dashboards/typedpidmaker-client.json @@ -0,0 +1,656 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "panels": [], + "title": "TypedPIDMakerClient Metrics", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Total number of PID operations performed by type", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Operations per second", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 1 + }, + "id": 2, + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "sum" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(pidMakerClient_createPIDRecord_count_total[5m]))", + "legendFormat": "Create PID", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(pidMakerClient_getPIDRecord_count_total[5m]))", + "hide": false, + "legendFormat": "Get PID", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(pidMakerClient_updatePIDRecord_count_total[5m]))", + "hide": false, + "legendFormat": "Update PID", + "range": true, + "refId": "C" + } + ], + "title": "PID Operations Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Total number of PID operations over time", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Total Operations", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 3, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(pidMakerClient_createPIDRecord_count_total)", + "legendFormat": "Create PID", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(pidMakerClient_getPIDRecord_count_total)", + "hide": false, + "legendFormat": "Get PID", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(pidMakerClient_updatePIDRecord_count_total)", + "hide": false, + "legendFormat": "Update PID", + "range": true, + "refId": "C" + } + ], + "title": "PID Operations Total", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Duration of PID operations (95th percentile)", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Duration (seconds)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 9 + }, + "id": 4, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum(rate(pidMakerClient_createPIDRecord_seconds_bucket[5m])) by (le))", + "legendFormat": "Create PID (p95)", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum(rate(pidMakerClient_getPIDRecord_seconds_bucket[5m])) by (le))", + "hide": false, + "legendFormat": "Get PID (p95)", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum(rate(pidMakerClient_updatePIDRecord_seconds_bucket[5m])) by (le))", + "hide": false, + "legendFormat": "Update PID (p95)", + "range": true, + "refId": "C" + } + ], + "title": "PID Operations Duration (p95)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Duration of PID operations (50th percentile)", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Duration (seconds)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 9 + }, + "id": 5, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.5, sum(rate(pidMakerClient_createPIDRecord_seconds_bucket[5m])) by (le))", + "legendFormat": "Create PID (p50)", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.5, sum(rate(pidMakerClient_getPIDRecord_seconds_bucket[5m])) by (le))", + "hide": false, + "legendFormat": "Get PID (p50)", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.5, sum(rate(pidMakerClient_updatePIDRecord_seconds_bucket[5m])) by (le))", + "hide": false, + "legendFormat": "Update PID (p50)", + "range": true, + "refId": "C" + } + ], + "title": "PID Operations Duration (p50)", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 17 + }, + "id": 6, + "panels": [], + "title": "Traces & Spans", + "type": "row" + }, + { + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "description": "Recent traces containing PID Maker client operations", + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 18 + }, + "id": 7, + "targets": [ + { + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "limit": 20, + "query": "service.name=\"idoris\" span.name=~\"TypedPIDMakerClient.*\"", + "refId": "A" + } + ], + "title": "PID Maker Client Traces", + "type": "traces" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Distribution of PID operation durations", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1 + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 26 + }, + "id": 8, + "options": { + "bucketOffset": 0, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(pidMakerClient_createPIDRecord_seconds_bucket[5m])) by (le)", + "format": "heatmap", + "legendFormat": "{{le}}", + "range": true, + "refId": "A" + } + ], + "title": "Create PID Duration Distribution", + "type": "histogram" + } + ], + "refresh": "10s", + "schemaVersion": 38, + "style": "dark", + "tags": [ + "idoris", + "pidmaker", + "client" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "TypedPIDMaker Client", + "uid": "typedpidmaker-client", + "version": 1, + "weekStart": "" +} \ No newline at end of file diff --git a/observability/grafana/provisioning/dashboards/dashboards.yml b/observability/grafana/provisioning/dashboards/dashboards.yml new file mode 100644 index 0000000..4665343 --- /dev/null +++ b/observability/grafana/provisioning/dashboards/dashboards.yml @@ -0,0 +1,13 @@ +apiVersion: 1 + +providers: + - name: 'IDORIS Dashboards' + orgId: 1 + folder: 'IDORIS' + type: file + disableDeletion: false + updateIntervalSeconds: 10 + allowUiUpdates: true + options: + path: /var/lib/grafana/dashboards + foldersFromFilesStructure: true \ No newline at end of file diff --git a/observability/grafana/provisioning/dashboards/spring-boot.yaml b/observability/grafana/provisioning/dashboards/spring-boot.yaml new file mode 100644 index 0000000..3250793 --- /dev/null +++ b/observability/grafana/provisioning/dashboards/spring-boot.yaml @@ -0,0 +1,13 @@ +apiVersion: 1 + +providers: + - name: 'Spring Boot' + orgId: 1 + folder: 'Spring Boot' + type: file + disableDeletion: false + updateIntervalSeconds: 10 + allowUiUpdates: true + options: + path: /var/lib/grafana/dashboards + foldersFromFilesStructure: true diff --git a/observability/grafana/provisioning/datasources/datasources.yml b/observability/grafana/provisioning/datasources/datasources.yml index 53ae7f7..268ac8b 100644 --- a/observability/grafana/provisioning/datasources/datasources.yml +++ b/observability/grafana/provisioning/datasources/datasources.yml @@ -7,12 +7,23 @@ datasources: url: http://prometheus:9090 isDefault: true editable: true + uid: prometheus + + - name: Mimir + type: prometheus + access: proxy + url: http://mimir:9009 + editable: true + uid: mimir + jsonData: + timeInterval: "15s" - name: Loki type: loki access: proxy url: http://loki:3100 editable: true + uid: loki jsonData: derivedFields: - name: "traceID" @@ -28,14 +39,27 @@ datasources: uid: tempo jsonData: httpMethod: GET + tracesToMetrics: + enabled: true + datasourceUid: 'prometheus' + tracesToProfiles: + enabled: true + datasourceUid: 'pyroscope' tracesToLogs: - datasourceUid: 'Loki' - tags: [ 'job', 'instance', 'pod', 'namespace' ] + datasourceUid: 'loki' + # tags: [ 'job', 'instance', 'pod', 'namespace' ] mappedTags: [ { key: 'service.name', value: 'service' } ] mapTagNamesEnabled: false spanStartTimeShift: '1h' spanEndTimeShift: '1h' - filterByTraceID: true - filterBySpanID: false serviceMap: - datasourceUid: 'Prometheus' \ No newline at end of file + datasourceUid: 'prometheus' + + - name: Pyroscope + type: grafana-pyroscope-datasource + access: proxy + url: http://pyroscope:4040 + editable: true + uid: pyroscope + jsonData: + httpMethod: GET \ No newline at end of file diff --git a/observability/mimir/mimir-config.yaml b/observability/mimir/mimir-config.yaml new file mode 100644 index 0000000..c15ba0b --- /dev/null +++ b/observability/mimir/mimir-config.yaml @@ -0,0 +1,80 @@ +# Mimir configuration file + +# Common configuration +common: + storage: + backend: filesystem + filesystem: + dir: /data/common + +# Server configuration +server: + http_listen_port: 9009 + grpc_listen_port: 9095 + +# Distributor configuration +distributor: + ring: + kvstore: + store: inmemory + +# Ingester configuration +ingester: + ring: + kvstore: + store: inmemory + replication_factor: 1 + final_sleep: 0s + instance_limits: + max_series: 1000000 + max_tenants: 1000 + +# Block storage configuration +blocks_storage: + backend: filesystem + filesystem: + dir: /data/blocks + tsdb: + dir: /data/tsdb + bucket_store: + sync_dir: /data/tsdb-sync + +# Compactor configuration +compactor: + data_dir: /data/compactor + sharding_ring: + kvstore: + store: inmemory + +# Query frontend configuration +frontend: + cache_results: false + +# Query scheduler configuration +query_scheduler: + max_outstanding_requests_per_tenant: 100 + +# Store gateway configuration +store_gateway: + sharding_ring: + replication_factor: 1 + kvstore: + store: inmemory + +# Limits configuration +limits: + max_label_names_per_series: 30 + max_label_name_length: 1024 + max_label_value_length: 2048 + out_of_order_time_window: 0s + ingestion_rate: 10000 + ingestion_burst_size: 20000 + +# Ruler configuration +ruler: + rule_path: /data/rules + alertmanager_url: http://localhost:9093 + ring: + kvstore: + store: inmemory + enable_api: true \ No newline at end of file diff --git a/observability/prometheus/prometheus.yml b/observability/prometheus/prometheus.yml index f606125..9215b2a 100644 --- a/observability/prometheus/prometheus.yml +++ b/observability/prometheus/prometheus.yml @@ -2,15 +2,30 @@ global: scrape_interval: 15s evaluation_interval: 15s +# Remote write configuration for long-term storage in Mimir +remote_write: + - url: "http://mimir:9009/api/v1/push" + name: mimir + remote_timeout: 30s + queue_config: + capacity: 10000 + max_shards: 200 + min_shards: 1 + max_samples_per_send: 2000 + batch_send_deadline: 5s + min_backoff: 30ms + max_backoff: 100ms + scrape_configs: - job_name: 'prometheus' static_configs: - targets: [ 'localhost:9090' ] - - - job_name: 'tempo' - static_configs: - - targets: [ 'tempo:3200' ] - -rule_files: -# - "first_rules.yml" -# - "second_rules.yml" \ No newline at end of file +# - job_name: 'tempo' +# static_configs: +# - targets: [ 'tempo:3200' ] +# - job_name: 'loki' +# static_configs: +# - targets: [ 'loki:3100' ] +# - job_name: 'alloy' +# static_configs: +# - targets: [ 'alloy:12345' ] # Ensure Alloy exposes metrics on this port \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/IdorisApplication.java b/src/main/java/edu/kit/datamanager/idoris/IdorisApplication.java index 370b7d7..8a96d6e 100644 --- a/src/main/java/edu/kit/datamanager/idoris/IdorisApplication.java +++ b/src/main/java/edu/kit/datamanager/idoris/IdorisApplication.java @@ -60,4 +60,20 @@ Configuration cypherDslConfiguration() { .withDialect(Dialect.NEO4J_5) .build(); } + + // Unregister the OpenTelemetryMeterRegistry from Metrics.globalRegistry and make it available + // as a Spring bean instead. +// @Bean +// @ConditionalOnClass(name = "io.opentelemetry.javaagent.OpenTelemetryAgent") +// public MeterRegistry otelRegistry() { +// Optional otelRegistry = Metrics.globalRegistry.getRegistries().stream() +// .filter(r -> r.getClass().getName().contains("OpenTelemetryMeterRegistry")) +// .findAny(); +// otelRegistry.ifPresent(Metrics.globalRegistry::remove); +// return otelRegistry.orElse(null); +// } +// @Bean +// public MeterRegistry getMeterRegistry() { +// return new CompositeMeterRegistry(); +// } } diff --git a/src/main/java/edu/kit/datamanager/idoris/attributes/services/AttributeService.java b/src/main/java/edu/kit/datamanager/idoris/attributes/services/AttributeService.java index 05a6f24..1d0725c 100644 --- a/src/main/java/edu/kit/datamanager/idoris/attributes/services/AttributeService.java +++ b/src/main/java/edu/kit/datamanager/idoris/attributes/services/AttributeService.java @@ -19,6 +19,12 @@ import edu.kit.datamanager.idoris.attributes.dao.IAttributeDao; import edu.kit.datamanager.idoris.attributes.entities.Attribute; import edu.kit.datamanager.idoris.core.events.EventPublisherService; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.annotation.Timed; +import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.annotations.WithSpan; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -33,6 +39,7 @@ */ @Service @Slf4j +@Observed(contextualName = "attributeService") public class AttributeService { private final IAttributeDao attributeDao; private final EventPublisherService eventPublisher; @@ -55,6 +62,9 @@ public AttributeService(IAttributeDao attributeDao, EventPublisherService eventP * @return the created Attribute entity */ @Transactional + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "attributeService.createAttribute", description = "Time taken to create an attribute", histogram = true) + @Counted(value = "attributeService.createAttribute.count", description = "Number of attribute creations") public Attribute createAttribute(Attribute attribute) { log.debug("Creating Attribute: {}", attribute); Attribute saved = attributeDao.save(attribute); @@ -71,6 +81,9 @@ public Attribute createAttribute(Attribute attribute) { * @throws IllegalArgumentException if the Attribute does not exist */ @Transactional + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "attributeService.updateAttribute", description = "Time taken to update an attribute", histogram = true) + @Counted(value = "attributeService.updateAttribute.count", description = "Number of attribute updates") public Attribute updateAttribute(Attribute attribute) { log.debug("Updating Attribute: {}", attribute); @@ -97,7 +110,10 @@ public Attribute updateAttribute(Attribute attribute) { * @throws IllegalArgumentException if the Attribute does not exist */ @Transactional - public void deleteAttribute(String id) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "attributeService.deleteAttribute", description = "Time taken to delete an attribute", histogram = true) + @Counted(value = "attributeService.deleteAttribute.count", description = "Number of attribute deletions") + public void deleteAttribute(@SpanAttribute("attribute.id") String id) { log.debug("Deleting Attribute with ID: {}", id); Attribute attribute = attributeDao.findById(id) @@ -115,7 +131,10 @@ public void deleteAttribute(String id) { * @return an Optional containing the Attribute, or empty if not found */ @Transactional(readOnly = true) - public Optional getAttribute(String id) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "attributeService.getAttribute", description = "Time taken to get an attribute", histogram = true) + @Counted(value = "attributeService.getAttribute.count", description = "Number of attribute retrievals") + public Optional getAttribute(@SpanAttribute("attribute.id") String id) { log.debug("Retrieving Attribute with ID: {}", id); return attributeDao.findById(id); } @@ -126,6 +145,9 @@ public Optional getAttribute(String id) { * @return a list of all Attribute entities */ @Transactional(readOnly = true) + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "attributeService.getAllAttributes", description = "Time taken to get all attributes", histogram = true) + @Counted(value = "attributeService.getAllAttributes.count", description = "Number of get all attributes requests") public List getAllAttributes() { log.debug("Retrieving all Attributes"); return attributeDao.findAll(); @@ -136,6 +158,9 @@ public List getAllAttributes() { * An orphaned Attribute is one that has a dataType relationship but is not referenced by any other node. */ @Transactional + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "attributeService.deleteOrphanedAttributes", description = "Time taken to delete orphaned attributes", histogram = true) + @Counted(value = "attributeService.deleteOrphanedAttributes.count", description = "Number of delete orphaned attributes requests") public void deleteOrphanedAttributes() { log.debug("Deleting orphaned Attributes"); attributeDao.deleteOrphanedAttributes(); @@ -151,7 +176,10 @@ public void deleteOrphanedAttributes() { * @throws IllegalArgumentException if the Attribute does not exist */ @Transactional - public Attribute patchAttribute(String id, Attribute attributePatch) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "attributeService.patchAttribute", description = "Time taken to patch an attribute", histogram = true) + @Counted(value = "attributeService.patchAttribute.count", description = "Number of attribute patches") + public Attribute patchAttribute(@SpanAttribute("attribute.id") String id, Attribute attributePatch) { log.debug("Patching Attribute with ID: {}, patch: {}", id, attributePatch); if (id == null || id.isEmpty()) { throw new IllegalArgumentException("Attribute ID cannot be null or empty"); diff --git a/src/main/java/edu/kit/datamanager/idoris/attributes/web/v1/AttributeController.java b/src/main/java/edu/kit/datamanager/idoris/attributes/web/v1/AttributeController.java index 2f53a56..f0b261a 100644 --- a/src/main/java/edu/kit/datamanager/idoris/attributes/web/v1/AttributeController.java +++ b/src/main/java/edu/kit/datamanager/idoris/attributes/web/v1/AttributeController.java @@ -22,6 +22,12 @@ import edu.kit.datamanager.idoris.attributes.web.hateoas.AttributeModelAssembler; import edu.kit.datamanager.idoris.datatypes.entities.DataType; import edu.kit.datamanager.idoris.datatypes.web.hateoas.DataTypeModelAssembler; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.annotation.Timed; +import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.annotations.WithSpan; import org.springframework.hateoas.CollectionModel; import org.springframework.hateoas.EntityModel; import org.springframework.http.HttpStatus; @@ -41,6 +47,7 @@ */ @RestController @RequestMapping("/v1/attributes") +@Observed(contextualName = "attributeController") public class AttributeController implements IAttributeApi { private final AttributeService attributeService; @@ -57,6 +64,9 @@ public AttributeController(AttributeService attributeService, AttributeModelAsse * {@inheritDoc} */ @Override + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "attributeController.getAllAttributes", description = "Time taken to get all attributes", histogram = true) + @Counted(value = "attributeController.getAllAttributes.count", description = "Number of get all attributes requests") public ResponseEntity>> getAllAttributes() { List> attributes = attributeService.getAllAttributes().stream() .map(attributeModelAssembler::toModel) @@ -74,7 +84,10 @@ public ResponseEntity>> getAllAttributes( * {@inheritDoc} */ @Override - public ResponseEntity> getAttribute(String pid) { + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "attributeController.getAttribute", description = "Time taken to get an attribute", histogram = true) + @Counted(value = "attributeController.getAttribute.count", description = "Number of get attribute requests") + public ResponseEntity> getAttribute(@SpanAttribute String pid) { return attributeService.getAttribute(pid) .map(attributeModelAssembler::toModel) .map(ResponseEntity::ok) @@ -85,7 +98,10 @@ public ResponseEntity> getAttribute(String pid) { * {@inheritDoc} */ @Override - public ResponseEntity> getDataType(String pid) { + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "attributeController.getDataType", description = "Time taken to get a data type", histogram = true) + @Counted(value = "attributeController.getDataType.count", description = "Number of get data type requests") + public ResponseEntity> getDataType(@SpanAttribute String pid) { return attributeService.getAttribute(pid) .map(Attribute::getDataType) .map(dataTypeModelAssembler::toModel) @@ -97,7 +113,10 @@ public ResponseEntity> getDataType(String pid) { * {@inheritDoc} */ @Override - public ResponseEntity> createAttribute(Attribute attribute) { + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "attributeController.createAttribute", description = "Time taken to create an attribute", histogram = true) + @Counted(value = "attributeController.createAttribute.count", description = "Number of create attribute requests") + public ResponseEntity> createAttribute(@SpanAttribute Attribute attribute) { Attribute createdAttribute = attributeService.createAttribute(attribute); EntityModel entityModel = attributeModelAssembler.toModel(createdAttribute); return ResponseEntity.status(HttpStatus.CREATED).body(entityModel); @@ -107,7 +126,10 @@ public ResponseEntity> createAttribute(Attribute attribut * {@inheritDoc} */ @Override - public ResponseEntity> updateAttribute(String id, Attribute attribute) { + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "attributeController.updateAttribute", description = "Time taken to update an attribute", histogram = true) + @Counted(value = "attributeController.updateAttribute.count", description = "Number of update attribute requests") + public ResponseEntity> updateAttribute(@SpanAttribute String id, @SpanAttribute Attribute attribute) { // Check if the entity exists if (attributeService.getAttribute(id).isEmpty()) { return ResponseEntity.notFound().build(); @@ -131,7 +153,10 @@ public ResponseEntity> updateAttribute(String id, Attribu * {@inheritDoc} */ @Override - public ResponseEntity deleteAttribute(String pid) { + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "attributeController.deleteAttribute", description = "Time taken to delete an attribute", histogram = true) + @Counted(value = "attributeController.deleteAttribute.count", description = "Number of delete attribute requests") + public ResponseEntity deleteAttribute(@SpanAttribute String pid) { if (attributeService.getAttribute(pid).isEmpty()) { return ResponseEntity.notFound().build(); } @@ -144,6 +169,9 @@ public ResponseEntity deleteAttribute(String pid) { * {@inheritDoc} */ @Override + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "attributeController.deleteOrphanedAttributes", description = "Time taken to delete orphaned attributes", histogram = true) + @Counted(value = "attributeController.deleteOrphanedAttributes.count", description = "Number of delete orphaned attributes requests") public ResponseEntity deleteOrphanedAttributes() { attributeService.deleteOrphanedAttributes(); return ResponseEntity.noContent().build(); @@ -153,7 +181,10 @@ public ResponseEntity deleteOrphanedAttributes() { * {@inheritDoc} */ @Override - public ResponseEntity> patchAttribute(String pid, Attribute attributePatch) { + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "attributeController.patchAttribute", description = "Time taken to patch an attribute", histogram = true) + @Counted(value = "attributeController.patchAttribute.count", description = "Number of patch attribute requests") + public ResponseEntity> patchAttribute(@SpanAttribute String pid, @SpanAttribute Attribute attributePatch) { if (attributeService.getAttribute(pid).isEmpty()) { return ResponseEntity.notFound().build(); } diff --git a/src/main/java/edu/kit/datamanager/idoris/core/events/EventPublisherService.java b/src/main/java/edu/kit/datamanager/idoris/core/events/EventPublisherService.java index 1edf865..e758bb0 100644 --- a/src/main/java/edu/kit/datamanager/idoris/core/events/EventPublisherService.java +++ b/src/main/java/edu/kit/datamanager/idoris/core/events/EventPublisherService.java @@ -17,6 +17,12 @@ package edu.kit.datamanager.idoris.core.events; import edu.kit.datamanager.idoris.core.domain.entities.AdministrativeMetadata; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.annotation.Timed; +import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.annotations.WithSpan; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; @@ -27,6 +33,7 @@ */ @Service @Slf4j +@Observed(contextualName = "eventPublisherService") public class EventPublisherService { private final ApplicationEventPublisher eventPublisher; @@ -45,6 +52,9 @@ public EventPublisherService(ApplicationEventPublisher eventPublisher) { * @param entity the newly created entity * @param the type of entity */ + @WithSpan(kind = SpanKind.PRODUCER) + @Timed(value = "eventPublisherService.publishEntityCreated", description = "Time taken to publish entity created event", histogram = true) + @Counted(value = "eventPublisherService.publishEntityCreated.count", description = "Number of entity created events published") public void publishEntityCreated(T entity) { log.debug("Publishing EntityCreatedEvent for entity: {}", entity); eventPublisher.publishEvent(new EntityCreatedEvent<>(entity)); @@ -68,7 +78,10 @@ public void publishEntityCreated(Object entity, String entityType) { * @param previousVersion the version of the entity before the update * @param the type of entity */ - public void publishEntityUpdated(T entity, Long previousVersion) { + @WithSpan(kind = SpanKind.PRODUCER) + @Timed(value = "eventPublisherService.publishEntityUpdated", description = "Time taken to publish entity updated event", histogram = true) + @Counted(value = "eventPublisherService.publishEntityUpdated.count", description = "Number of entity updated events published") + public void publishEntityUpdated(T entity, @SpanAttribute("entity.previousVersion") Long previousVersion) { log.debug("Publishing EntityUpdatedEvent for entity: {}, previous version: {}", entity, previousVersion); eventPublisher.publishEvent(new EntityUpdatedEvent<>(entity, previousVersion)); } @@ -90,6 +103,9 @@ public void publishEntityUpdated(Object entity, String entityType) { * @param entity the deleted entity * @param the type of entity */ + @WithSpan(kind = SpanKind.PRODUCER) + @Timed(value = "eventPublisherService.publishEntityDeleted", description = "Time taken to publish entity deleted event", histogram = true) + @Counted(value = "eventPublisherService.publishEntityDeleted.count", description = "Number of entity deleted events published") public void publishEntityDeleted(T entity) { log.debug("Publishing EntityDeletedEvent for entity: {}", entity); eventPublisher.publishEvent(new EntityDeletedEvent<>(entity)); @@ -108,27 +124,33 @@ public void publishEntityDeleted(Object entity, String entityType) { /** * Publishes an ID generated event. + * Assumes that the ID is newly generated. * - * @param entity the entity for which the ID was generated - * @param id the generated ID - * @param isNewID indicates whether this is a newly generated ID or an existing one - * @param the type of entity + * @param entity the entity for which the ID was generated + * @param id the generated ID + * @param the type of entity */ - public void publishIDGenerated(T entity, String id, boolean isNewID) { - log.debug("Publishing IDGeneratedEvent for entity: {}, ID: {}, isNewID: {}", entity, id, isNewID); - eventPublisher.publishEvent(new PIDGeneratedEvent<>(entity, id, isNewID)); + @WithSpan(kind = SpanKind.PRODUCER) + @Timed(value = "eventPublisherService.publishIDGeneratedShort", description = "Time taken to publish ID generated event (short form)", histogram = true) + @Counted(value = "eventPublisherService.publishIDGeneratedShort.count", description = "Number of ID generated events published (short form)") + public void publishIDGenerated(T entity, @SpanAttribute("entity.id") String id) { + publishIDGenerated(entity, id, true); } /** * Publishes an ID generated event. - * Assumes that the ID is newly generated. * - * @param entity the entity for which the ID was generated - * @param id the generated ID - * @param the type of entity + * @param entity the entity for which the ID was generated + * @param id the generated ID + * @param isNewID indicates whether this is a newly generated ID or an existing one + * @param the type of entity */ - public void publishIDGenerated(T entity, String id) { - publishIDGenerated(entity, id, true); + @WithSpan(kind = SpanKind.PRODUCER) + @Timed(value = "eventPublisherService.publishIDGenerated", description = "Time taken to publish ID generated event", histogram = true) + @Counted(value = "eventPublisherService.publishIDGenerated.count", description = "Number of ID generated events published") + public void publishIDGenerated(T entity, @SpanAttribute("entity.id") String id, @SpanAttribute("entity.isNewID") boolean isNewID) { + log.debug("Publishing IDGeneratedEvent for entity: {}, ID: {}, isNewID: {}", entity, id, isNewID); + eventPublisher.publishEvent(new PIDGeneratedEvent<>(entity, id, isNewID)); } /** @@ -138,7 +160,10 @@ public void publishIDGenerated(T entity, Stri * @param previousVersion the version of the entity before the patch * @param the type of entity */ - public void publishEntityPatched(T entity, Long previousVersion) { + @WithSpan(kind = SpanKind.PRODUCER) + @Timed(value = "eventPublisherService.publishEntityPatched", description = "Time taken to publish entity patched event", histogram = true) + @Counted(value = "eventPublisherService.publishEntityPatched.count", description = "Number of entity patched events published") + public void publishEntityPatched(T entity, @SpanAttribute("entity.previousVersion") Long previousVersion) { log.debug("Publishing EntityPatchedEvent for entity: {}, previous version: {}", entity, previousVersion); eventPublisher.publishEvent(new EntityPatchedEvent<>(entity, previousVersion)); } @@ -149,7 +174,10 @@ public void publishEntityPatched(T entity, Lo * @param entity the patched entity * @param entityType the type identifier for the entity */ - public void publishEntityPatched(Object entity, String entityType) { + @WithSpan(kind = SpanKind.PRODUCER) + @Timed(value = "eventPublisherService.publishEntityPatchedGeneric", description = "Time taken to publish generic entity patched event", histogram = true) + @Counted(value = "eventPublisherService.publishEntityPatchedGeneric.count", description = "Number of generic entity patched events published") + public void publishEntityPatched(Object entity, @SpanAttribute("entity.type") String entityType) { log.debug("Publishing EntityPatchedEvent for entity: {}, type: {}", entity, entityType); eventPublisher.publishEvent(new GenericEntityPatchedEvent(entity, entityType)); } @@ -159,6 +187,9 @@ public void publishEntityPatched(Object entity, String entityType) { * * @param event the event to publish */ + @WithSpan(kind = SpanKind.PRODUCER) + @Timed(value = "eventPublisherService.publishEvent", description = "Time taken to publish generic domain event", histogram = true) + @Counted(value = "eventPublisherService.publishEvent.count", description = "Number of generic domain events published") public void publishEvent(DomainEvent event) { log.debug("Publishing event: {}", event); eventPublisher.publishEvent(event); diff --git a/src/main/java/edu/kit/datamanager/idoris/datatypes/services/AtomicDataTypeService.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/services/AtomicDataTypeService.java index f2a8cd5..06d9e3c 100644 --- a/src/main/java/edu/kit/datamanager/idoris/datatypes/services/AtomicDataTypeService.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/services/AtomicDataTypeService.java @@ -19,6 +19,12 @@ import edu.kit.datamanager.idoris.core.events.EventPublisherService; import edu.kit.datamanager.idoris.datatypes.dao.IAtomicDataTypeDao; import edu.kit.datamanager.idoris.datatypes.entities.AtomicDataType; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.annotation.Timed; +import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.annotations.WithSpan; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -33,6 +39,7 @@ */ @Service @Slf4j +@Observed(contextualName = "atomicDataTypeService") public class AtomicDataTypeService { private final IAtomicDataTypeDao atomicDataTypeDao; private final EventPublisherService eventPublisher; @@ -55,6 +62,9 @@ public AtomicDataTypeService(IAtomicDataTypeDao atomicDataTypeDao, EventPublishe * @return the created AtomicDataType entity */ @Transactional + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "atomicDataTypeService.createAtomicDataType", description = "Time taken to create an atomic data type", histogram = true) + @Counted(value = "atomicDataTypeService.createAtomicDataType.count", description = "Number of atomic data type creations") public AtomicDataType createAtomicDataType(AtomicDataType atomicDataType) { log.debug("Creating AtomicDataType: {}", atomicDataType); atomicDataType.setInternalId(null); @@ -73,6 +83,9 @@ public AtomicDataType createAtomicDataType(AtomicDataType atomicDataType) { * @throws IllegalArgumentException if the AtomicDataType does not exist */ @Transactional + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "atomicDataTypeService.updateAtomicDataType", description = "Time taken to update an atomic data type", histogram = true) + @Counted(value = "atomicDataTypeService.updateAtomicDataType.count", description = "Number of atomic data type updates") public AtomicDataType updateAtomicDataType(AtomicDataType atomicDataType) { log.debug("Updating AtomicDataType: {}", atomicDataType); @@ -99,7 +112,10 @@ public AtomicDataType updateAtomicDataType(AtomicDataType atomicDataType) { * @throws IllegalArgumentException if the AtomicDataType does not exist */ @Transactional - public void deleteAtomicDataType(String id) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "atomicDataTypeService.deleteAtomicDataType", description = "Time taken to delete an atomic data type", histogram = true) + @Counted(value = "atomicDataTypeService.deleteAtomicDataType.count", description = "Number of atomic data type deletions") + public void deleteAtomicDataType(@SpanAttribute("atomicDataType.id") String id) { log.debug("Deleting AtomicDataType with ID: {}", id); AtomicDataType atomicDataType = atomicDataTypeDao.findById(id) @@ -117,7 +133,10 @@ public void deleteAtomicDataType(String id) { * @return an Optional containing the AtomicDataType, or empty if not found */ @Transactional(readOnly = true) - public Optional getAtomicDataType(String id) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "atomicDataTypeService.getAtomicDataType", description = "Time taken to get an atomic data type", histogram = true) + @Counted(value = "atomicDataTypeService.getAtomicDataType.count", description = "Number of atomic data type retrievals") + public Optional getAtomicDataType(@SpanAttribute("atomicDataType.id") String id) { log.debug("Retrieving AtomicDataType with ID: {}", id); return atomicDataTypeDao.findById(id); } @@ -128,6 +147,9 @@ public Optional getAtomicDataType(String id) { * @return a list of all AtomicDataType entities */ @Transactional(readOnly = true) + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "atomicDataTypeService.getAllAtomicDataTypes", description = "Time taken to get all atomic data types", histogram = true) + @Counted(value = "atomicDataTypeService.getAllAtomicDataTypes.count", description = "Number of get all atomic data types requests") public List getAllAtomicDataTypes() { log.debug("Retrieving all AtomicDataTypes"); return atomicDataTypeDao.findAll(); @@ -142,7 +164,10 @@ public List getAllAtomicDataTypes() { * @throws IllegalArgumentException if the AtomicDataType does not exist */ @Transactional - public AtomicDataType patchAtomicDataType(String id, AtomicDataType atomicDataTypePatch) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "atomicDataTypeService.patchAtomicDataType", description = "Time taken to patch an atomic data type", histogram = true) + @Counted(value = "atomicDataTypeService.patchAtomicDataType.count", description = "Number of atomic data type patches") + public AtomicDataType patchAtomicDataType(@SpanAttribute("atomicDataType.id") String id, AtomicDataType atomicDataTypePatch) { log.debug("Patching AtomicDataType with ID: {}, patch: {}", id, atomicDataTypePatch); if (id == null || id.isEmpty()) { throw new IllegalArgumentException("AtomicDataType ID cannot be null or empty"); diff --git a/src/main/java/edu/kit/datamanager/idoris/datatypes/services/TypeProfileService.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/services/TypeProfileService.java index bd78ad5..c7038d0 100644 --- a/src/main/java/edu/kit/datamanager/idoris/datatypes/services/TypeProfileService.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/services/TypeProfileService.java @@ -20,6 +20,12 @@ import edu.kit.datamanager.idoris.datatypes.entities.TypeProfile; import edu.kit.datamanager.idoris.rules.validation.ValidationPolicyValidator; import edu.kit.datamanager.idoris.rules.validation.ValidationResult; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.annotation.Timed; +import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.annotations.WithSpan; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -34,6 +40,7 @@ */ @Service @Slf4j +@Observed(contextualName = "typeProfileService") public class TypeProfileService { private final ITypeProfileDao typeProfileDao; private final EventPublisherService eventPublisher; @@ -56,7 +63,10 @@ public TypeProfileService(ITypeProfileDao typeProfileDao, EventPublisherService * @return the created TypeProfile entity */ @Transactional - public TypeProfile createTypeProfile(TypeProfile typeProfile) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "typeProfileService.createTypeProfile", description = "Time taken to create a type profile", histogram = true) + @Counted(value = "typeProfileService.createTypeProfile.count", description = "Number of type profile creations") + public TypeProfile createTypeProfile(@SpanAttribute TypeProfile typeProfile) { log.debug("Creating TypeProfile: {}", typeProfile); TypeProfile saved = typeProfileDao.save(typeProfile); eventPublisher.publishEntityCreated(saved); @@ -72,7 +82,10 @@ public TypeProfile createTypeProfile(TypeProfile typeProfile) { * @throws IllegalArgumentException if the TypeProfile does not exist */ @Transactional - public TypeProfile updateTypeProfile(TypeProfile typeProfile) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "typeProfileService.updateTypeProfile", description = "Time taken to update a type profile", histogram = true) + @Counted(value = "typeProfileService.updateTypeProfile.count", description = "Number of type profile updates") + public TypeProfile updateTypeProfile(@SpanAttribute TypeProfile typeProfile) { log.debug("Updating TypeProfile: {}", typeProfile); if (typeProfile.getId() == null || typeProfile.getId().isEmpty()) { throw new IllegalArgumentException("TypeProfile must have a PID to be updated"); @@ -94,7 +107,10 @@ public TypeProfile updateTypeProfile(TypeProfile typeProfile) { * @throws IllegalArgumentException if the TypeProfile does not exist */ @Transactional - public void deleteTypeProfile(String id) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "typeProfileService.deleteTypeProfile", description = "Time taken to delete a type profile", histogram = true) + @Counted(value = "typeProfileService.deleteTypeProfile.count", description = "Number of type profile deletions") + public void deleteTypeProfile(@SpanAttribute String id) { log.debug("Deleting TypeProfile with ID: {}", id); TypeProfile typeProfile = typeProfileDao.findById(id) @@ -112,7 +128,10 @@ public void deleteTypeProfile(String id) { * @return an Optional containing the TypeProfile, or empty if not found */ @Transactional(readOnly = true) - public Optional getTypeProfile(String id) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "typeProfileService.getTypeProfile", description = "Time taken to get a type profile", histogram = true) + @Counted(value = "typeProfileService.getTypeProfile.count", description = "Number of type profile retrievals") + public Optional getTypeProfile(@SpanAttribute String id) { log.debug("Retrieving TypeProfile with ID: {}", id); return typeProfileDao.findById(id); } @@ -123,6 +142,9 @@ public Optional getTypeProfile(String id) { * @return a list of all TypeProfile entities */ @Transactional(readOnly = true) + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "typeProfileService.getAllTypeProfiles", description = "Time taken to get all type profiles", histogram = true) + @Counted(value = "typeProfileService.getAllTypeProfiles.count", description = "Number of get all type profiles requests") public List getAllTypeProfiles() { log.debug("Retrieving all TypeProfiles"); return typeProfileDao.findAll(); @@ -136,7 +158,10 @@ public List getAllTypeProfiles() { * @throws IllegalArgumentException if the TypeProfile does not exist */ @Transactional(readOnly = true) - public ValidationResult validateTypeProfile(String id) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "typeProfileService.validateTypeProfile", description = "Time taken to validate a type profile", histogram = true) + @Counted(value = "typeProfileService.validateTypeProfile.count", description = "Number of type profile validations") + public ValidationResult validateTypeProfile(@SpanAttribute String id) { log.debug("Validating TypeProfile with ID: {}", id); TypeProfile typeProfile = typeProfileDao.findById(id) @@ -154,7 +179,10 @@ public ValidationResult validateTypeProfile(String id) { * @throws IllegalArgumentException if the TypeProfile does not exist */ @Transactional(readOnly = true) - public Iterable getInheritanceChain(String id) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "typeProfileService.getInheritanceChain", description = "Time taken to get inheritance chain", histogram = true) + @Counted(value = "typeProfileService.getInheritanceChain.count", description = "Number of inheritance chain retrievals") + public Iterable getInheritanceChain(@SpanAttribute String id) { log.debug("Retrieving inheritance chain for TypeProfile with ID: {}", id); TypeProfile typeProfile = typeProfileDao.findById(id) @@ -172,7 +200,10 @@ public Iterable getInheritanceChain(String id) { * @throws IllegalArgumentException if the TypeProfile does not exist */ @Transactional - public TypeProfile patchTypeProfile(String id, TypeProfile typeProfilePatch) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "typeProfileService.patchTypeProfile", description = "Time taken to patch a type profile", histogram = true) + @Counted(value = "typeProfileService.patchTypeProfile.count", description = "Number of type profile patches") + public TypeProfile patchTypeProfile(@SpanAttribute String id, @SpanAttribute TypeProfile typeProfilePatch) { log.debug("Patching TypeProfile with ID: {}, patch: {}", id, typeProfilePatch); if (id == null || id.isEmpty()) { throw new IllegalArgumentException("TypeProfile ID cannot be null or empty"); diff --git a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/api/IAtomicDataTypeApi.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/api/IAtomicDataTypeApi.java index 7db9e7f..5e31197 100644 --- a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/api/IAtomicDataTypeApi.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/api/IAtomicDataTypeApi.java @@ -17,6 +17,9 @@ package edu.kit.datamanager.idoris.datatypes.web.api; import edu.kit.datamanager.idoris.datatypes.entities.AtomicDataType; +import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.annotations.WithSpan; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -33,6 +36,7 @@ * This interface defines the REST API for managing AtomicDataType entities. */ @Tag(name = "AtomicDataType", description = "API for managing AtomicDataTypes") +@Observed public interface IAtomicDataTypeApi { /** @@ -50,6 +54,7 @@ public interface IAtomicDataTypeApi { schema = @Schema(implementation = AtomicDataType.class))) } ) + @WithSpan ResponseEntity>> getAllAtomicDataTypes(); /** @@ -69,7 +74,9 @@ public interface IAtomicDataTypeApi { @ApiResponse(responseCode = "404", description = "AtomicDataType not found") } ) + @WithSpan ResponseEntity> getAtomicDataType( + @SpanAttribute("atomicDataType.id") @Parameter(description = "PID or internal ID of the AtomicDataType", required = true) @PathVariable String id); @@ -91,7 +98,9 @@ ResponseEntity> getAtomicDataType( @ApiResponse(responseCode = "400", description = "Invalid input or validation failed") } ) + @WithSpan ResponseEntity> createAtomicDataType( + @Parameter(description = "AtomicDataType to create", required = true) @Valid @RequestBody AtomicDataType atomicDataType); @@ -115,9 +124,12 @@ ResponseEntity> createAtomicDataType( @ApiResponse(responseCode = "404", description = "AtomicDataType not found") } ) + @WithSpan ResponseEntity> updateAtomicDataType( + @SpanAttribute("atomicDataType.id") @Parameter(description = "PID or internal ID of the AtomicDataType", required = true) @PathVariable String id, + @Parameter(description = "Updated AtomicDataType", required = true) @Valid @RequestBody AtomicDataType atomicDataType); @@ -136,7 +148,9 @@ ResponseEntity> updateAtomicDataType( @ApiResponse(responseCode = "404", description = "AtomicDataType not found") } ) + @WithSpan ResponseEntity deleteAtomicDataType( + @SpanAttribute("atomicDataType.id") @Parameter(description = "PID or internal ID of the AtomicDataType", required = true) @PathVariable String id); @@ -157,7 +171,9 @@ ResponseEntity deleteAtomicDataType( @ApiResponse(responseCode = "404", description = "AtomicDataType not found") } ) + @WithSpan ResponseEntity>> getOperationsForAtomicDataType( + @SpanAttribute("atomicDataType.id") @Parameter(description = "PID or internal ID of the AtomicDataType", required = true) @PathVariable String id); @@ -180,7 +196,9 @@ ResponseEntity> patchAtomicDataType( + @SpanAttribute @Parameter(description = "PID or internal ID of the AtomicDataType", required = true) @PathVariable String id, @Parameter(description = "Partial AtomicDataType with fields to update", required = true) diff --git a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/api/ITypeProfileApi.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/api/ITypeProfileApi.java index bbdf315..8e739b1 100644 --- a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/api/ITypeProfileApi.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/api/ITypeProfileApi.java @@ -20,6 +20,7 @@ import edu.kit.datamanager.idoris.datatypes.entities.TypeProfile; import edu.kit.datamanager.idoris.datatypes.web.v1.TypeProfileController.TypeProfileInheritance; import edu.kit.datamanager.idoris.operations.entities.Operation; +import io.micrometer.observation.annotation.Observed; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -37,6 +38,7 @@ * This interface defines the REST API for managing TypeProfile entities. */ @Tag(name = "TypeProfile", description = "API for managing TypeProfiles") +@Observed public interface ITypeProfileApi { /** diff --git a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/v1/AtomicDataTypeController.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/v1/AtomicDataTypeController.java index c891831..34805a5 100644 --- a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/v1/AtomicDataTypeController.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/v1/AtomicDataTypeController.java @@ -27,6 +27,12 @@ import edu.kit.datamanager.idoris.rules.logic.RuleService; import edu.kit.datamanager.idoris.rules.logic.RuleTask; import edu.kit.datamanager.idoris.rules.validation.ValidationResult; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.annotation.Timed; +import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.annotations.WithSpan; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -55,6 +61,7 @@ @RequestMapping("/v1/atomicDataTypes") @Tag(name = "AtomicDataType", description = "API for managing AtomicDataTypes") @Slf4j +@Observed(contextualName = "atomicDataTypeController") public class AtomicDataTypeController implements IAtomicDataTypeApi { private final AtomicDataTypeService atomicDataTypeService; @@ -85,6 +92,9 @@ public AtomicDataTypeController(AtomicDataTypeService atomicDataTypeService, Ope schema = @Schema(implementation = AtomicDataType.class))) } ) + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "atomicDataTypeController.getAllAtomicDataTypes", description = "Time taken to get all atomic data types", histogram = true) + @Counted(value = "atomicDataTypeController.getAllAtomicDataTypes.count", description = "Number of get all atomic data types requests") public ResponseEntity>> getAllAtomicDataTypes() { List> atomicDataTypes = atomicDataTypeService.getAllAtomicDataTypes().stream() .map(atomicDataTypeModelAssembler::toModel) @@ -113,9 +123,12 @@ public ResponseEntity>> getAllAtomic @ApiResponse(responseCode = "404", description = "AtomicDataType not found") } ) + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "atomicDataTypeController.getAtomicDataType", description = "Time taken to get an atomic data type", histogram = true) + @Counted(value = "atomicDataTypeController.getAtomicDataType.count", description = "Number of get atomic data type requests") public ResponseEntity> getAtomicDataType( @Parameter(description = "PID or internal ID of the AtomicDataType", required = true) - @PathVariable String id) { + @SpanAttribute @PathVariable String id) { return atomicDataTypeService.getAtomicDataType(id) .map(atomicDataTypeModelAssembler::toModel) .map(ResponseEntity::ok) @@ -137,9 +150,12 @@ public ResponseEntity> getAtomicDataType( @ApiResponse(responseCode = "400", description = "Invalid input or validation failed") } ) + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "atomicDataTypeController.createAtomicDataType", description = "Time taken to create an atomic data type", histogram = true) + @Counted(value = "atomicDataTypeController.createAtomicDataType.count", description = "Number of create atomic data type requests") public ResponseEntity> createAtomicDataType( @Parameter(description = "AtomicDataType to create", required = true) - @Valid @RequestBody AtomicDataType atomicDataType) { + @SpanAttribute @Valid @RequestBody AtomicDataType atomicDataType) { // Validate BEFORE saving ValidationResult validationResult = ruleService.executeRules( @@ -176,11 +192,14 @@ public ResponseEntity> createAtomicDataType( @ApiResponse(responseCode = "404", description = "AtomicDataType not found") } ) + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "atomicDataTypeController.updateAtomicDataType", description = "Time taken to update an atomic data type", histogram = true) + @Counted(value = "atomicDataTypeController.updateAtomicDataType.count", description = "Number of update atomic data type requests") public ResponseEntity> updateAtomicDataType( @Parameter(description = "PID or internal ID of the AtomicDataType", required = true) - @PathVariable String id, + @SpanAttribute @PathVariable String id, @Parameter(description = "Updated AtomicDataType", required = true) - @Valid @RequestBody AtomicDataType atomicDataType) { + @SpanAttribute @Valid @RequestBody AtomicDataType atomicDataType) { // Check if the entity exists if (!atomicDataTypeService.getAtomicDataType(id).isPresent()) { return ResponseEntity.notFound().build(); @@ -227,9 +246,12 @@ public ResponseEntity> updateAtomicDataType( @ApiResponse(responseCode = "404", description = "AtomicDataType not found") } ) + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "atomicDataTypeController.deleteAtomicDataType", description = "Time taken to delete an atomic data type", histogram = true) + @Counted(value = "atomicDataTypeController.deleteAtomicDataType.count", description = "Number of delete atomic data type requests") public ResponseEntity deleteAtomicDataType( @Parameter(description = "PID or internal ID of the AtomicDataType", required = true) - @PathVariable String id) { + @SpanAttribute @PathVariable String id) { if (!atomicDataTypeService.getAtomicDataType(id).isPresent()) { return ResponseEntity.notFound().build(); } @@ -238,6 +260,46 @@ public ResponseEntity deleteAtomicDataType( return ResponseEntity.noContent().build(); } + /** + * {@inheritDoc} + */ + @Override + @GetMapping("/{id}/operations") + @io.swagger.v3.oas.annotations.Operation( + summary = "Get operations for an AtomicDataType", + description = "Returns a collection of operations that can be executed on an AtomicDataType", + responses = { + @ApiResponse(responseCode = "200", description = "Operations found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = Operation.class))), + @ApiResponse(responseCode = "404", description = "AtomicDataType not found") + } + ) + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "atomicDataTypeController.getOperationsForAtomicDataType", description = "Time taken to get operations for an atomic data type", histogram = true) + @Counted(value = "atomicDataTypeController.getOperationsForAtomicDataType.count", description = "Number of get operations for atomic data type requests") + public ResponseEntity>> getOperationsForAtomicDataType( + @Parameter(description = "PID or internal ID of the AtomicDataType", required = true) + @SpanAttribute @PathVariable String id) { + if (!atomicDataTypeService.getAtomicDataType(id).isPresent()) { + return ResponseEntity.notFound().build(); + } + + List> operations = StreamSupport.stream(operationService.getOperationsForDataType(id).spliterator(), false) + .map(operation -> EntityModel.of(operation, + linkTo(methodOn(AtomicDataTypeController.class).getOperationsForAtomicDataType(id)).withSelfRel(), + linkTo(methodOn(AtomicDataTypeController.class).getAtomicDataType(id)).withRel("atomicDataType"))) + .collect(Collectors.toList()); + + CollectionModel> collectionModel = CollectionModel.of( + operations, + linkTo(methodOn(AtomicDataTypeController.class).getOperationsForAtomicDataType(id)).withSelfRel(), + linkTo(methodOn(AtomicDataTypeController.class).getAtomicDataType(id)).withRel("atomicDataType") + ); + + return ResponseEntity.ok(collectionModel); + } + /** * {@inheritDoc} */ @@ -254,11 +316,14 @@ public ResponseEntity deleteAtomicDataType( @ApiResponse(responseCode = "404", description = "AtomicDataType not found") } ) + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "atomicDataTypeController.patchAtomicDataType", description = "Time taken to patch an atomic data type", histogram = true) + @Counted(value = "atomicDataTypeController.patchAtomicDataType.count", description = "Number of patch atomic data type requests") public ResponseEntity> patchAtomicDataType( @Parameter(description = "PID or internal ID of the AtomicDataType", required = true) - @PathVariable String id, + @SpanAttribute @PathVariable String id, @Parameter(description = "Partial AtomicDataType with fields to update", required = true) - @RequestBody AtomicDataType atomicDataTypePatch) { + @SpanAttribute @RequestBody AtomicDataType atomicDataTypePatch) { if (!atomicDataTypeService.getAtomicDataType(id).isPresent()) { return ResponseEntity.notFound().build(); } @@ -307,43 +372,6 @@ public ResponseEntity> patchAtomicDataType( return ResponseEntity.ok(entityModel); } - /** - * {@inheritDoc} - */ - @Override - @GetMapping("/{id}/operations") - @io.swagger.v3.oas.annotations.Operation( - summary = "Get operations for an AtomicDataType", - description = "Returns a collection of operations that can be executed on an AtomicDataType", - responses = { - @ApiResponse(responseCode = "200", description = "Operations found", - content = @Content(mediaType = "application/hal+json", - schema = @Schema(implementation = Operation.class))), - @ApiResponse(responseCode = "404", description = "AtomicDataType not found") - } - ) - public ResponseEntity>> getOperationsForAtomicDataType( - @Parameter(description = "PID or internal ID of the AtomicDataType", required = true) - @PathVariable String id) { - if (!atomicDataTypeService.getAtomicDataType(id).isPresent()) { - return ResponseEntity.notFound().build(); - } - - List> operations = StreamSupport.stream(operationService.getOperationsForDataType(id).spliterator(), false) - .map(operation -> EntityModel.of(operation, - linkTo(methodOn(AtomicDataTypeController.class).getOperationsForAtomicDataType(id)).withSelfRel(), - linkTo(methodOn(AtomicDataTypeController.class).getAtomicDataType(id)).withRel("atomicDataType"))) - .collect(Collectors.toList()); - - CollectionModel> collectionModel = CollectionModel.of( - operations, - linkTo(methodOn(AtomicDataTypeController.class).getOperationsForAtomicDataType(id)).withSelfRel(), - linkTo(methodOn(AtomicDataTypeController.class).getAtomicDataType(id)).withRel("atomicDataType") - ); - - return ResponseEntity.ok(collectionModel); - } - private boolean hasValidationErrors(ValidationResult validationResult) { return validationResult.getOutputMessages() .entrySet() diff --git a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/v1/TypeProfileController.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/v1/TypeProfileController.java index 9023ef0..f88eac5 100644 --- a/src/main/java/edu/kit/datamanager/idoris/datatypes/web/v1/TypeProfileController.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/web/v1/TypeProfileController.java @@ -28,6 +28,12 @@ import edu.kit.datamanager.idoris.rules.logic.RuleService; import edu.kit.datamanager.idoris.rules.logic.RuleTask; import edu.kit.datamanager.idoris.rules.validation.ValidationResult; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.annotation.Timed; +import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.annotations.WithSpan; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -56,6 +62,7 @@ @RestController @RequestMapping("/v1/typeProfiles") @Tag(name = "TypeProfile", description = "API for managing TypeProfiles") +@Observed(contextualName = "typeProfileController") public class TypeProfileController implements ITypeProfileApi { private final TypeProfileService typeProfileService; private final OperationService operationService; @@ -89,6 +96,9 @@ public TypeProfileController(TypeProfileService typeProfileService, schema = @Schema(implementation = TypeProfile.class))) } ) + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "typeProfileController.getAllTypeProfiles", description = "Time taken to get all type profiles", histogram = true) + @Counted(value = "typeProfileController.getAllTypeProfiles.count", description = "Number of get all type profiles requests") public ResponseEntity>> getAllTypeProfiles() { List> typeProfiles = StreamSupport.stream(typeProfileService.getAllTypeProfiles().spliterator(), false) .map(typeProfileModelAssembler::toModel) @@ -117,9 +127,12 @@ public ResponseEntity>> getAllTypeProfi @ApiResponse(responseCode = "404", description = "TypeProfile not found") } ) + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "typeProfileController.getTypeProfile", description = "Time taken to get a type profile", histogram = true) + @Counted(value = "typeProfileController.getTypeProfile.count", description = "Number of get type profile requests") public ResponseEntity> getTypeProfile( @Parameter(description = "PID or internal ID of the TypeProfile", required = true) - @PathVariable String id) { + @SpanAttribute @PathVariable String id) { return typeProfileService.getTypeProfile(id) .map(typeProfileModelAssembler::toModel) .map(ResponseEntity::ok) @@ -141,9 +154,12 @@ public ResponseEntity> getTypeProfile( @ApiResponse(responseCode = "404", description = "TypeProfile not found") } ) + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "typeProfileController.getOperationsForTypeProfile", description = "Time taken to get operations for a type profile", histogram = true) + @Counted(value = "typeProfileController.getOperationsForTypeProfile.count", description = "Number of get operations for type profile requests") public ResponseEntity>> getOperationsForTypeProfile( @Parameter(description = "PID or internal ID of the TypeProfile", required = true) - @PathVariable String id) { + @SpanAttribute @PathVariable String id) { if (!typeProfileService.getTypeProfile(id).isPresent()) { return ResponseEntity.notFound().build(); } @@ -177,9 +193,12 @@ public ResponseEntity>> getOperationsForT @ApiResponse(responseCode = "404", description = "TypeProfile not found") } ) + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "typeProfileController.validate", description = "Time taken to validate a type profile", histogram = true) + @Counted(value = "typeProfileController.validate.count", description = "Number of validate type profile requests") public ResponseEntity validate( @Parameter(description = "PID or internal ID of the TypeProfile", required = true) - @PathVariable String id) { + @SpanAttribute @PathVariable String id) { ValidationResult result = typeProfileService.validateTypeProfile(id); if (result.isValid()) { return ResponseEntity.ok(result); @@ -203,9 +222,12 @@ public ResponseEntity validate( @ApiResponse(responseCode = "404", description = "TypeProfile not found") } ) + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "typeProfileController.getInheritedAttributes", description = "Time taken to get inherited attributes", histogram = true) + @Counted(value = "typeProfileController.getInheritedAttributes.count", description = "Number of get inherited attributes requests") public ResponseEntity>> getInheritedAttributes( @Parameter(description = "PID or internal ID of the TypeProfile", required = true) - @PathVariable String id) { + @SpanAttribute @PathVariable String id) { Iterable inheritanceChain = typeProfileService.getInheritanceChain(id); List> attributes = new ArrayList<>(); inheritanceChain.forEach(typeProfile -> { @@ -238,9 +260,12 @@ public ResponseEntity>> getInheritedAttri @ApiResponse(responseCode = "400", description = "Invalid input or validation failed") } ) + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "typeProfileController.createTypeProfile", description = "Time taken to create a type profile", histogram = true) + @Counted(value = "typeProfileController.createTypeProfile.count", description = "Number of create type profile requests") public ResponseEntity> createTypeProfile( @Parameter(description = "TypeProfile to create", required = true) - @Valid @RequestBody TypeProfile typeProfile) { + @SpanAttribute @Valid @RequestBody TypeProfile typeProfile) { // Validate BEFORE saving ValidationResult validationResult = ruleService.executeRules( @@ -276,11 +301,14 @@ public ResponseEntity> createTypeProfile( @ApiResponse(responseCode = "404", description = "TypeProfile not found") } ) + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "typeProfileController.updateTypeProfile", description = "Time taken to update a type profile", histogram = true) + @Counted(value = "typeProfileController.updateTypeProfile.count", description = "Number of update type profile requests") public ResponseEntity> updateTypeProfile( @Parameter(description = "PID or internal ID of the TypeProfile", required = true) - @PathVariable String id, + @SpanAttribute @PathVariable String id, @Parameter(description = "Updated TypeProfile", required = true) - @Valid @RequestBody TypeProfile typeProfile) { + @SpanAttribute @Valid @RequestBody TypeProfile typeProfile) { // Check if the entity exists if (!typeProfileService.getTypeProfile(id).isPresent()) { return ResponseEntity.notFound().build(); @@ -326,9 +354,12 @@ public ResponseEntity> updateTypeProfile( @ApiResponse(responseCode = "404", description = "TypeProfile not found") } ) + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "typeProfileController.deleteTypeProfile", description = "Time taken to delete a type profile", histogram = true) + @Counted(value = "typeProfileController.deleteTypeProfile.count", description = "Number of delete type profile requests") public ResponseEntity deleteTypeProfile( @Parameter(description = "PID or internal ID of the TypeProfile", required = true) - @PathVariable String id) { + @SpanAttribute @PathVariable String id) { if (!typeProfileService.getTypeProfile(id).isPresent()) { return ResponseEntity.notFound().build(); } @@ -337,6 +368,33 @@ public ResponseEntity deleteTypeProfile( return ResponseEntity.noContent().build(); } + /** + * {@inheritDoc} + */ + @Override + @GetMapping("/{id}/inheritanceTree") + @io.swagger.v3.oas.annotations.Operation( + summary = "Get inheritance tree of a TypeProfile", + description = "Returns the inheritance tree of a TypeProfile", + responses = { + @ApiResponse(responseCode = "200", description = "Inheritance tree found", + content = @Content(mediaType = "application/hal+json")), + @ApiResponse(responseCode = "404", description = "TypeProfile not found") + } + ) + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "typeProfileController.getInheritanceTree", description = "Time taken to get inheritance tree", histogram = true) + @Counted(value = "typeProfileController.getInheritanceTree.count", description = "Number of get inheritance tree requests") + public ResponseEntity> getInheritanceTree( + @Parameter(description = "PID or internal ID of the TypeProfile", required = true) + @SpanAttribute @NotNull @PathVariable String id) { + EntityModel resources = buildInheritanceTree(typeProfileService.getTypeProfile(id).orElseThrow()); + resources.add(linkTo(methodOn(TypeProfileController.class).getInheritanceTree(id)).withSelfRel()); + resources.add(linkTo(methodOn(TypeProfileController.class).getTypeProfile(id)).withRel("typeProfile")); + + return ResponseEntity.ok(resources); + } + /** * {@inheritDoc} */ @@ -353,11 +411,14 @@ public ResponseEntity deleteTypeProfile( @ApiResponse(responseCode = "404", description = "TypeProfile not found") } ) + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "typeProfileController.patchTypeProfile", description = "Time taken to patch a type profile", histogram = true) + @Counted(value = "typeProfileController.patchTypeProfile.count", description = "Number of patch type profile requests") public ResponseEntity> patchTypeProfile( @Parameter(description = "PID or internal ID of the TypeProfile", required = true) - @PathVariable String id, + @SpanAttribute @PathVariable String id, @Parameter(description = "Partial TypeProfile with fields to update", required = true) - @RequestBody TypeProfile typeProfilePatch) { + @SpanAttribute @RequestBody TypeProfile typeProfilePatch) { if (!typeProfileService.getTypeProfile(id).isPresent()) { return ResponseEntity.notFound().build(); } @@ -394,27 +455,20 @@ public ResponseEntity> patchTypeProfile( } /** - * {@inheritDoc} + * Checks if a validation result contains errors based on the configured validation level. + * + * @param validationResult the validation result to check + * @return true if the validation result contains errors, false otherwise */ - @Override - @GetMapping("/{id}/inheritanceTree") - @io.swagger.v3.oas.annotations.Operation( - summary = "Get inheritance tree of a TypeProfile", - description = "Returns the inheritance tree of a TypeProfile", - responses = { - @ApiResponse(responseCode = "200", description = "Inheritance tree found", - content = @Content(mediaType = "application/hal+json")), - @ApiResponse(responseCode = "404", description = "TypeProfile not found") - } - ) - public ResponseEntity> getInheritanceTree( - @Parameter(description = "PID or internal ID of the TypeProfile", required = true) - @NotNull @PathVariable String id) { - EntityModel resources = buildInheritanceTree(typeProfileService.getTypeProfile(id).orElseThrow()); - resources.add(linkTo(methodOn(TypeProfileController.class).getInheritanceTree(id)).withSelfRel()); - resources.add(linkTo(methodOn(TypeProfileController.class).getTypeProfile(id)).withRel("typeProfile")); - - return ResponseEntity.ok(resources); + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "typeProfileController.hasValidationErrors", description = "Time taken to check validation errors", histogram = true) + @Counted(value = "typeProfileController.hasValidationErrors.count", description = "Number of validation error checks") + private boolean hasValidationErrors(@SpanAttribute ValidationResult validationResult) { + return validationResult.getOutputMessages() + .entrySet() + .stream() + .anyMatch(entry -> entry.getKey().isHigherOrEqualTo(applicationProperties.getValidationLevel()) + && !entry.getValue().isEmpty()); } /** @@ -423,7 +477,10 @@ public ResponseEntity> getInheritanceTree( * @param typeProfile the TypeProfile to build the inheritance tree for * @return an EntityModel containing the inheritance tree */ - private EntityModel buildInheritanceTree(TypeProfile typeProfile) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "typeProfileController.buildInheritanceTree", description = "Time taken to build inheritance tree", histogram = true) + @Counted(value = "typeProfileController.buildInheritanceTree.count", description = "Number of build inheritance tree calls") + private EntityModel buildInheritanceTree(@SpanAttribute TypeProfile typeProfile) { List> attributes = new ArrayList<>(); typeProfile.getAttributes().forEach(profileAttribute -> { EntityModel attribute = EntityModel.of(profileAttribute); @@ -452,20 +509,6 @@ private EntityModel buildInheritanceTree(TypeProfile typ return node; } - /** - * Checks if a validation result contains errors based on the configured validation level. - * - * @param validationResult the validation result to check - * @return true if the validation result contains errors, false otherwise - */ - private boolean hasValidationErrors(ValidationResult validationResult) { - return validationResult.getOutputMessages() - .entrySet() - .stream() - .anyMatch(entry -> entry.getKey().isHigherOrEqualTo(applicationProperties.getValidationLevel()) - && !entry.getValue().isEmpty()); - } - public record TypeProfileInheritance( String id, String name, diff --git a/src/main/java/edu/kit/datamanager/idoris/operations/services/OperationService.java b/src/main/java/edu/kit/datamanager/idoris/operations/services/OperationService.java index 1355b59..af3969d 100644 --- a/src/main/java/edu/kit/datamanager/idoris/operations/services/OperationService.java +++ b/src/main/java/edu/kit/datamanager/idoris/operations/services/OperationService.java @@ -19,6 +19,12 @@ import edu.kit.datamanager.idoris.core.events.EventPublisherService; import edu.kit.datamanager.idoris.operations.dao.IOperationDao; import edu.kit.datamanager.idoris.operations.entities.Operation; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.annotation.Timed; +import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.annotations.WithSpan; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -33,6 +39,7 @@ */ @Service @Slf4j +@Observed(contextualName = "operationService") public class OperationService { private final IOperationDao operationDao; private final EventPublisherService eventPublisher; @@ -55,7 +62,10 @@ public OperationService(IOperationDao operationDao, EventPublisherService eventP * @return the created Operation entity */ @Transactional - public Operation createOperation(Operation operation) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "operationService.createOperation", description = "Time taken to create an operation", histogram = true) + @Counted(value = "operationService.createOperation.count", description = "Number of operation creations") + public Operation createOperation(@SpanAttribute Operation operation) { log.debug("Creating Operation: {}", operation); Operation saved = operationDao.save(operation); eventPublisher.publishEntityCreated(saved); @@ -71,7 +81,10 @@ public Operation createOperation(Operation operation) { * @throws IllegalArgumentException if the Operation does not exist */ @Transactional - public Operation updateOperation(Operation operation) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "operationService.updateOperation", description = "Time taken to update an operation", histogram = true) + @Counted(value = "operationService.updateOperation.count", description = "Number of operation updates") + public Operation updateOperation(@SpanAttribute Operation operation) { log.debug("Updating Operation: {}", operation); if (operation.getId() == null || operation.getId().isEmpty()) { @@ -97,7 +110,10 @@ public Operation updateOperation(Operation operation) { * @throws IllegalArgumentException if the Operation does not exist */ @Transactional - public void deleteOperation(String id) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "operationService.deleteOperation", description = "Time taken to delete an operation", histogram = true) + @Counted(value = "operationService.deleteOperation.count", description = "Number of operation deletions") + public void deleteOperation(@SpanAttribute String id) { log.debug("Deleting Operation with ID: {}", id); Operation operation = operationDao.findById(id) @@ -115,7 +131,10 @@ public void deleteOperation(String id) { * @return an Optional containing the Operation, or empty if not found */ @Transactional(readOnly = true) - public Optional getOperation(String id) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "operationService.getOperation", description = "Time taken to get an operation", histogram = true) + @Counted(value = "operationService.getOperation.count", description = "Number of operation retrievals") + public Optional getOperation(@SpanAttribute String id) { log.debug("Retrieving Operation with ID: {}", id); return operationDao.findById(id); } @@ -126,6 +145,9 @@ public Optional getOperation(String id) { * @return a list of all Operation entities */ @Transactional(readOnly = true) + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "operationService.getAllOperations", description = "Time taken to get all operations", histogram = true) + @Counted(value = "operationService.getAllOperations.count", description = "Number of get all operations requests") public List getAllOperations() { log.debug("Retrieving all Operations"); return operationDao.findAll(); @@ -138,7 +160,10 @@ public List getAllOperations() { * @return an iterable of Operations for the DataType */ @Transactional(readOnly = true) - public Iterable getOperationsForDataType(String dataTypeId) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "operationService.getOperationsForDataType", description = "Time taken to get operations for data type", histogram = true) + @Counted(value = "operationService.getOperationsForDataType.count", description = "Number of get operations for data type requests") + public Iterable getOperationsForDataType(@SpanAttribute String dataTypeId) { log.debug("Retrieving Operations for DataType with ID: {}", dataTypeId); return operationDao.getOperationsForDataType(dataTypeId); } @@ -152,7 +177,10 @@ public Iterable getOperationsForDataType(String dataTypeId) { * @throws IllegalArgumentException if the Operation does not exist */ @Transactional - public Operation patchOperation(String id, Operation operationPatch) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "operationService.patchOperation", description = "Time taken to patch an operation", histogram = true) + @Counted(value = "operationService.patchOperation.count", description = "Number of operation patches") + public Operation patchOperation(@SpanAttribute String id, @SpanAttribute Operation operationPatch) { log.debug("Patching Operation with ID: {}, patch: {}", id, operationPatch); if (id == null || id.isEmpty()) { throw new IllegalArgumentException("Operation ID cannot be null or empty"); diff --git a/src/main/java/edu/kit/datamanager/idoris/operations/web/v1/OperationController.java b/src/main/java/edu/kit/datamanager/idoris/operations/web/v1/OperationController.java index 177b41b..539c558 100644 --- a/src/main/java/edu/kit/datamanager/idoris/operations/web/v1/OperationController.java +++ b/src/main/java/edu/kit/datamanager/idoris/operations/web/v1/OperationController.java @@ -23,6 +23,12 @@ import edu.kit.datamanager.idoris.operations.web.hateoas.OperationModelAssembler; import edu.kit.datamanager.idoris.rules.validation.ValidationPolicyValidator; import edu.kit.datamanager.idoris.rules.validation.ValidationResult; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.annotation.Timed; +import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.annotations.WithSpan; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -50,6 +56,7 @@ @RestController @RequestMapping("/v1/operations") @Tag(name = "Operation", description = "API for managing Operations") +@Observed(contextualName = "operationController") public class OperationController implements IOperationApi { @Autowired @@ -72,6 +79,9 @@ public class OperationController implements IOperationApi { schema = @Schema(implementation = Operation.class))) } ) + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "operationController.getAllOperations", description = "Time taken to get all operations", histogram = true) + @Counted(value = "operationController.getAllOperations.count", description = "Number of get all operations requests") public ResponseEntity>> getAllOperations() { List> operations = StreamSupport.stream(operationService.getAllOperations().spliterator(), false) .map(operationModelAssembler::toModel) @@ -100,9 +110,12 @@ public ResponseEntity>> getAllOperations( @ApiResponse(responseCode = "404", description = "Operation not found") } ) + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "operationController.getOperation", description = "Time taken to get an operation", histogram = true) + @Counted(value = "operationController.getOperation.count", description = "Number of get operation requests") public ResponseEntity> getOperation( @Parameter(description = "PID or internal ID of the Operation", required = true) - @PathVariable String id) { + @SpanAttribute("operation.id") @PathVariable String id) { return operationService.getOperation(id) .map(operationModelAssembler::toModel) .map(ResponseEntity::ok) @@ -124,6 +137,9 @@ public ResponseEntity> getOperation( @ApiResponse(responseCode = "400", description = "Invalid input or validation failed") } ) + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "operationController.createOperation", description = "Time taken to create an operation", histogram = true) + @Counted(value = "operationController.createOperation.count", description = "Number of create operation requests") public ResponseEntity> createOperation( @Parameter(description = "Operation to create", required = true) @Valid @RequestBody Operation operation) { @@ -158,11 +174,14 @@ public ResponseEntity> createOperation( @ApiResponse(responseCode = "404", description = "Operation not found") } ) + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "operationController.updateOperation", description = "Time taken to update an operation", histogram = true) + @Counted(value = "operationController.updateOperation.count", description = "Number of update operation requests") public ResponseEntity> updateOperation( @Parameter(description = "PID or internal ID of the Operation", required = true) - @PathVariable String id, + @SpanAttribute @PathVariable String id, @Parameter(description = "Updated Operation", required = true) - @Valid @RequestBody Operation operation) { + @SpanAttribute @Valid @RequestBody Operation operation) { // Check if the entity exists if (!operationService.getOperation(id).isPresent()) { return ResponseEntity.notFound().build(); @@ -205,9 +224,12 @@ public ResponseEntity> updateOperation( @ApiResponse(responseCode = "404", description = "Operation not found") } ) + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "operationController.deleteOperation", description = "Time taken to delete an operation", histogram = true) + @Counted(value = "operationController.deleteOperation.count", description = "Number of delete operation requests") public ResponseEntity deleteOperation( @Parameter(description = "PID or internal ID of the Operation", required = true) - @PathVariable String id) { + @SpanAttribute("operation.id") @PathVariable String id) { if (!operationService.getOperation(id).isPresent()) { return ResponseEntity.notFound().build(); } @@ -230,9 +252,12 @@ public ResponseEntity deleteOperation( @ApiResponse(responseCode = "404", description = "Operation not found") } ) + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "operationController.validate", description = "Time taken to validate an operation", histogram = true) + @Counted(value = "operationController.validate.count", description = "Number of validate operation requests") public ResponseEntity validate( @Parameter(description = "PID or internal ID of the Operation", required = true) - @PathVariable String id) { + @SpanAttribute("operation.id") @PathVariable String id) { if (!operationService.getOperation(id).isPresent()) { return ResponseEntity.notFound().build(); } @@ -262,9 +287,12 @@ public ResponseEntity validate( schema = @Schema(implementation = Operation.class))) } ) + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "operationController.getOperationsForDataType", description = "Time taken to get operations for a data type", histogram = true) + @Counted(value = "operationController.getOperationsForDataType.count", description = "Number of get operations for data type requests") public ResponseEntity>> getOperationsForDataType( @Parameter(description = "PID or internal ID of the data type", required = true) - @RequestParam String id) { + @SpanAttribute("dataType.id") @RequestParam String id) { List> operations = StreamSupport.stream(operationService.getOperationsForDataType(id).spliterator(), false) .map(operationModelAssembler::toModel) .collect(Collectors.toList()); @@ -293,11 +321,14 @@ public ResponseEntity>> getOperationsForD @ApiResponse(responseCode = "404", description = "Operation not found") } ) + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "operationController.patchOperation", description = "Time taken to patch an operation", histogram = true) + @Counted(value = "operationController.patchOperation.count", description = "Number of patch operation requests") public ResponseEntity> patchOperation( @Parameter(description = "PID or internal ID of the Operation", required = true) - @PathVariable String id, + @SpanAttribute @PathVariable String id, @Parameter(description = "Partial Operation with fields to update", required = true) - @RequestBody Operation operationPatch) { + @SpanAttribute @RequestBody Operation operationPatch) { if (!operationService.getOperation(id).isPresent()) { return ResponseEntity.notFound().build(); } diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/MetadataEventListener.java b/src/main/java/edu/kit/datamanager/idoris/pids/MetadataEventListener.java index ec5f7d8..044deac 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/MetadataEventListener.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/MetadataEventListener.java @@ -23,6 +23,8 @@ import edu.kit.datamanager.idoris.core.events.EventPublisherService; import edu.kit.datamanager.idoris.pids.entities.PersistentIdentifier; import edu.kit.datamanager.idoris.pids.services.PersistentIdentifierService; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.annotation.Timed; import io.micrometer.observation.annotation.Observed; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.annotations.WithSpan; @@ -40,7 +42,7 @@ */ @Component @Slf4j -@Observed +@Observed(contextualName = "metadataEventListener") public class MetadataEventListener { private final PersistentIdentifierService pidService; private final EventPublisherService eventPublisher; @@ -66,6 +68,8 @@ public MetadataEventListener(PersistentIdentifierService pidService, EventPublis @EventListener(classes = {EntityCreatedEvent.class}) @Transactional @WithSpan(kind = SpanKind.CONSUMER) + @Timed(value = "metadataEventListener.handleEntityCreatedEvent", description = "Time taken to handle entity created event", histogram = true) + @Counted(value = "metadataEventListener.handleEntityCreatedEvent.count", description = "Number of entity created events handled") public void handleEntityCreatedEvent(EntityCreatedEvent event) { AdministrativeMetadata entity = event.getEntity(); log.debug("Handling EntityCreatedEvent for entity: {}", entity); @@ -97,6 +101,8 @@ public void handleEntityCreatedEvent(EntityCreatedEvent @EventListener(classes = {EntityUpdatedEvent.class}) @Transactional @WithSpan(kind = SpanKind.CONSUMER) + @Timed(value = "metadataEventListener.handleEntityUpdatedEvent", description = "Time taken to handle entity updated event", histogram = true) + @Counted(value = "metadataEventListener.handleEntityUpdatedEvent.count", description = "Number of entity updated events handled") public void handleEntityUpdatedEvent(EntityUpdatedEvent event) { AdministrativeMetadata entity = event.getEntity(); log.debug("Handling EntityUpdatedEvent for entity: {}", entity); @@ -125,6 +131,8 @@ public void handleEntityUpdatedEvent(EntityUpdatedEvent @EventListener(classes = {EntityDeletedEvent.class}) @Transactional @WithSpan(kind = SpanKind.CONSUMER) + @Timed(value = "metadataEventListener.handleEntityDeletedEvent", description = "Time taken to handle entity deleted event", histogram = true) + @Counted(value = "metadataEventListener.handleEntityDeletedEvent.count", description = "Number of entity deleted events handled") public void handleEntityDeletedEvent(EntityDeletedEvent event) { AdministrativeMetadata entity = event.getEntity(); log.debug("Handling EntityDeletedEvent for entity: {}", entity); diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClient.java b/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClient.java index 099c19d..98d223b 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClient.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClient.java @@ -17,13 +17,16 @@ package edu.kit.datamanager.idoris.pids.client; import edu.kit.datamanager.idoris.pids.client.model.PIDRecord; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.annotation.Timed; import io.micrometer.observation.annotation.Observed; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.annotations.SpanAttribute; import io.opentelemetry.instrumentation.annotations.WithSpan; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.service.annotation.GetExchange; import org.springframework.web.service.annotation.HttpExchange; import org.springframework.web.service.annotation.PostExchange; @@ -34,41 +37,54 @@ * This interface defines the operations for interacting with the service. */ @HttpExchange("/api/v1/pit/pid") -@Observed +@Observed(contextualName = "pidMakerClient") public interface TypedPIDMakerClient { /** * Creates a new PID record using the SimplePidRecord format. * * @param record The PID record to create - * @return The created PID record + * @return The created PID record with response headers (including ETag) */ - @PostExchange(value = "/", accept = "application/vnd.datamanager.pid.simple+json", contentType = "application/vnd.datamanager.pid.simple+json") - @ResponseBody + @PostExchange( + value = "/", + accept = "application/vnd.datamanager.pid.simple+json", + contentType = "application/vnd.datamanager.pid.simple+json") @WithSpan(kind = SpanKind.CLIENT) - PIDRecord createPIDRecord(@SpanAttribute @RequestBody PIDRecord record); - + @Timed(value = "pidMakerClient.createPIDRecord", description = "Time taken to create a PID record", histogram = true) + @Counted(value = "pidMakerClient.createPIDRecord.count", description = "Number of PID record creations") + ResponseEntity createPIDRecord(@RequestBody PIDRecord record); /** * Gets a PID record by its PID using the SimplePidRecord format. * * @param pid The PID of the record to get - * @return The PID record + * @return The PID record with response headers (including ETag) */ - @GetExchange(value = "/{pid}", accept = "application/vnd.datamanager.pid.simple+json") - @ResponseBody + @GetExchange( + value = "/{pid}", + accept = "application/vnd.datamanager.pid.simple+json") @WithSpan(kind = SpanKind.CLIENT) - PIDRecord getPIDRecord(@SpanAttribute @PathVariable String pid); + @Timed(value = "pidMakerClient.getPIDRecord", description = "Time taken to retrieve a PID record", histogram = true) + @Counted(value = "pidMakerClient.getPIDRecord.count", description = "Number of PID record retrievals") + ResponseEntity getPIDRecord(@SpanAttribute("pid.value") @PathVariable String pid); /** * Updates an existing PID record using the SimplePidRecord format. * * @param pid The PID of the record to update * @param record The updated PID record - * @return The updated PID record + * @param etag The ETag value for the If-Match header + * @return The updated PID record with response headers (including ETag) */ - @PutExchange(value = "/{pid}", accept = "application/vnd.datamanager.pid.simple+json", contentType = "application/vnd.datamanager.pid.simple+json") - @ResponseBody + @PutExchange( + value = "/{pid}", + accept = "application/vnd.datamanager.pid.simple+json", + contentType = "application/vnd.datamanager.pid.simple+json") @WithSpan(kind = SpanKind.CLIENT) - PIDRecord updatePIDRecord(@SpanAttribute @PathVariable String pid, @SpanAttribute @RequestBody PIDRecord record); -} + @Timed(value = "pidMakerClient.updatePIDRecord", description = "Time taken to update a PID record", histogram = true) + @Counted(value = "pidMakerClient.updatePIDRecord.count", description = "Number of PID record updates") + ResponseEntity updatePIDRecord(@SpanAttribute("pid.value") @PathVariable String pid, + @RequestBody PIDRecord record, + @RequestHeader("If-Match") String etag); +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/services/PersistentIdentifierService.java b/src/main/java/edu/kit/datamanager/idoris/pids/services/PersistentIdentifierService.java index 40b0073..974c295 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/services/PersistentIdentifierService.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/services/PersistentIdentifierService.java @@ -28,6 +28,7 @@ import io.opentelemetry.instrumentation.annotations.WithSpan; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -102,7 +103,14 @@ public PersistentIdentifier createPersistentIdentifier(AdministrativeMetadata en PIDRecord record = mapper.toPIDRecord(tempPid); // Create the PID record in the Typed PID Maker service - PIDRecord createdRecord = client.createPIDRecord(record); + ResponseEntity createdResponse = client.createPIDRecord(record); + + if (!createdResponse.getStatusCode().is2xxSuccessful()) { + log.error("Failed to create PID record in Typed PID Maker service: {}", createdResponse.getStatusCode()); + throw new RuntimeException("Failed to create PID record in Typed PID Maker service"); + } + String etag = createdResponse.getHeaders().getETag(); + PIDRecord createdRecord = createdResponse.getBody(); log.debug("Created first PID record: {}", createdRecord); @@ -126,32 +134,16 @@ public PersistentIdentifier createPersistentIdentifier(AdministrativeMetadata en PIDRecord updatedRecord = new PIDRecord(createdRecord.pid(), entries); // Update the PID record in the Typed PID Maker service with the saved PID log.debug("Updating PID record with saved PID: {}", updatedRecord); - client.updatePIDRecord(savedPid.getPid(), updatedRecord); - log.info("Created PersistentIdentifier: {} with record", savedPid); - return savedPid; - } + ResponseEntity updatedResponse = client.updatePIDRecord(savedPid.getPid(), updatedRecord, etag); - /** - * Updates the PID record for the given PersistentIdentifier. - * This method updates the PID record in the Typed PID Maker service with the latest metadata from the entity. - * - * @param pid The PersistentIdentifier to update the PID record for - * @return The updated PersistentIdentifier - */ - @Transactional - @WithSpan - public PersistentIdentifier updatePIDRecord(PersistentIdentifier pid) { - log.debug("Updating PID record for PersistentIdentifier: {}", pid); - - // Create a PID record with metadata from the entity - PIDRecord record = mapper.toPIDRecord(pid); - - // Update the PID record in the Typed PID Maker service - client.updatePIDRecord(pid.getPid(), record); + if (!updatedResponse.getStatusCode().is2xxSuccessful()) { + log.error("Failed to update PID record in Typed PID Maker service: {}", updatedResponse.getStatusCode()); + throw new RuntimeException("Failed to update PID record in Typed PID Maker service"); + } - log.info("Updated PID record for PersistentIdentifier: {}", pid); - return pid; + log.info("Created PersistentIdentifier: {} with record", savedPid); + return savedPid; } /** @@ -188,6 +180,47 @@ public Optional markAsTombstone(AdministrativeMetadata ent return Optional.of(savedPid); } + /** + * Updates the PID record for the given PersistentIdentifier. + * This method updates the PID record in the Typed PID Maker service with the latest metadata from the entity. + * + * @param pid The PersistentIdentifier to update the PID record for + * @return The updated PersistentIdentifier + */ + @Transactional + @WithSpan + public PersistentIdentifier updatePIDRecord(PersistentIdentifier pid) { + log.debug("Updating PID record for PersistentIdentifier: {}", pid); + + // Create a PID record with metadata from the entity + PIDRecord record = mapper.toPIDRecord(pid); + + // If the PID is null, it means the PID has not been created yet + ResponseEntity getResponse = client.getPIDRecord(pid.getPid()); + + if (!getResponse.getStatusCode().is2xxSuccessful()) { + log.error("Failed to retrieve PID record from Typed PID Maker service: {}", getResponse.getStatusCode()); + throw new RuntimeException("Failed to retrieve PID record from Typed PID Maker service"); + } + + // Ensure the record is not null and has the correct PID + if (record == null || !record.pid().equals(pid.getPid()) || !record.pid().equals(Objects.requireNonNull(getResponse.getBody()).pid())) { + log.error("PID record is null or PID does not match: expected {}, got {}", pid.getPid(), record != null ? record.pid() : "null"); + throw new RuntimeException("PID record is null or PID does not match"); + } + + // Update the PID record in the Typed PID Maker service + ResponseEntity updatedResponse = client.updatePIDRecord(record.pid(), record, getResponse.getHeaders().getETag()); + + if (!updatedResponse.getStatusCode().is2xxSuccessful()) { + log.error("Failed to update PID record in Typed PID Maker service: {}", updatedResponse.getStatusCode()); + throw new RuntimeException("Failed to update PID record in Typed PID Maker service"); + } + + log.info("Updated PID record for PersistentIdentifier: {}", pid); + return pid; + } + /** * Gets the PersistentIdentifier for the given entity. * diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/utils/PIDRecordMapper.java b/src/main/java/edu/kit/datamanager/idoris/pids/utils/PIDRecordMapper.java index 9994937..88f4d19 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/utils/PIDRecordMapper.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/utils/PIDRecordMapper.java @@ -130,7 +130,7 @@ public PIDRecord toPIDRecord(PersistentIdentifier pid) { // Add version information Long version = entity.getVersion(); if (version != null) { - recordEntries.add(new PIDRecordEntry("21.T11148/c692273deb2772da307f", version.toString())); + recordEntries.add(new PIDRecordEntry("21.T11148/c692273deb2772da307f", "v" + version)); } // Add contributors diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/validation/SyntaxValidator.java b/src/main/java/edu/kit/datamanager/idoris/rules/validation/SyntaxValidator.java index 9e8e3c1..9a5bdbe 100644 --- a/src/main/java/edu/kit/datamanager/idoris/rules/validation/SyntaxValidator.java +++ b/src/main/java/edu/kit/datamanager/idoris/rules/validation/SyntaxValidator.java @@ -53,7 +53,8 @@ }, name = "SyntaxValidationRule", description = "Validates that entities follow required syntax rules and constraints", - tasks = {RuleTask.VALIDATE} + tasks = {RuleTask.VALIDATE}, + executeBefore = {InheritanceValidator.class} ) public class SyntaxValidator extends ValidationVisitor { diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationResult.java b/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationResult.java index d74a37c..5196e74 100644 --- a/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationResult.java +++ b/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationResult.java @@ -68,6 +68,22 @@ public class ValidationResult implements RuleOutput { */ private List children = new ArrayList<>(); + public static ValidationResult combine(ValidationResult... results) { + ValidationResult combined = new ValidationResult(); + if (results != null) { + for (ValidationResult result : results) { + if (result != null && !result.isEmpty()) { + combined.merge(result); + } + } + } + return combined; + } + + public static ValidationResult error(String message, Object element) { + return new ValidationResult().addMessage(message, element, OutputMessage.MessageSeverity.ERROR); + } + /** * Adds a validation message to this result. * @@ -81,6 +97,18 @@ public ValidationResult addMessage(String message, Object element, OutputMessage return this; } + public static ValidationResult warning(String message, Object element) { + return new ValidationResult().addMessage(message, element, OutputMessage.MessageSeverity.WARNING); + } + + public static ValidationResult info(String message, Object element) { + return new ValidationResult().addMessage(message, element, OutputMessage.MessageSeverity.INFO); + } + + public static ValidationResult ok() { + return new ValidationResult(); + } + /** * Adds a child validation result to this result. * Empty child results are not added to avoid cluttering the hierarchy. diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 43e7b72..bca2312 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -27,29 +27,94 @@ spring.neo4j.uri=bolt://localhost:7687 spring.neo4j.authentication.username=neo4j spring.neo4j.authentication.password=superSecret -# Actuator and Metrics Configuration -# Expose only necessary endpoints for security -#management.endpoints.web.exposure.include=health,info,metrics -management.endpoints.web.exposure.include=* -management.endpoint.health.show-details=always -# Disable Prometheus endpoint as we're using OTLP for metrics -management.endpoint.prometheus.enabled=false +################################# +######## Observability ########## +################################# +## Generic OpenTelemetry Configuration +#management.endpoints.web.exposure.include=* +#management.endpoint.health.show-details=always +#management.endpoint.prometheus.access=unrestricted +#management.metrics.distribution.sla.http.server.requests=100ms,500ms,1000ms +#management.metrics.export.defaults.step=15s +#management.metrics.distribution.percentiles-histogram.http.server.requests=true +#management.metrics.tags.service_name=${spring.application.name} +#management.metrics.tags.environment=${spring.profiles.active} +#management.prometheus.metrics.export.enabled=false +#otel.java.global-autoconfigure.enabled=true +#otel.instrumentation.micrometer.enabled=true +#otel.service.name=${spring.application.name} +# +## OpenTelemetry Metrics Configuration +#otel.metrics.exporter=otlp +#otel.exporter.otlp.endpoint=http://localhost:4318 +#otel.exporter.otlp.protocol=http/protobuf +#management.otlp.metrics.export.enabled=true +#management.otlp.metrics.export.step=2s +#management.otlp.metrics.export.url=http://localhost:4318/v1/metrics +# +## OpenTelemetry Logging Configuration +#management.otlp.logging.export.enabled=true +#management.otlp.logging.endpoint=http://localhost:4318/v1/logs +##otel.instrumentation.logback-appender.experimental.capture-mdc-attributes=* +#otel.instrumentation.logback-appender.experimental-log-attributes=true +#otel.instrumentation.logback-appender.experimental.capture-code-attributes=true +#otel.instrumentation.logback-appender.experimental.capture-marker-attribute=true +#otel.instrumentation.logback-appender.experimental.capture-mdc-attributes=trace_id,span_id +##logging.pattern.level=%5p [${spring.application.name:},%X{trace_id:-},%X{span_id:-}] +#logging.context.enabled=true +# +## Tracing Configuration +#management.tracing.sampling.probability=1.0 +#management.otlp.tracing.endpoint=http://localhost:4318/v1/traces +#management.httpexchanges.recording.enabled=true +#management.tracing.baggage.correlation.enabled=true +#management.tracing.opentelemetry.export.include-unsampled=true +#management.observations.annotations.enabled=true +#otel.instrumentation.http.client.emit-experimental-telemetry=true +#otel.instrumentation.runtime-telemetry-java17.enabled=true +#otel.instrumentation.spring-webmvc.enabled=true +#otel.instrumentation.annotations.enabled=true +#otel.instrumentation.http.client.capture-request-headers=true +#otel.instrumentation.http.client.capture-response-headers=true +#otel.instrumentation.http.client.experimental.redact-query-parameters=false +#otel.instrumentation.jdbc.experimental.transaction.enabled=true +#otel.resource.attributes.exclude=process.command_args,process.command_line +#otel.propagators=tracecontext,baggage +#otel.traces.sampler=parentbased_traceidratio +#otel.traces.sampler.arg=1 +################################ +####### Observability ########## +################################ +# Generic OpenTelemetry Configuration +management.endpoint.prometheus.access=unrestricted +otel.java.global-autoconfigure.enabled=true +otel.instrumentation.micrometer.enabled=true +otel.service.name=${spring.application.name} + +# OpenTelemetry Metrics Configuration +otel.metrics.exporter=otlp +otel.exporter.otlp.endpoint=http://localhost:4318 +otel.exporter.otlp.protocol=http/protobuf +management.otlp.metrics.export.enabled=true +management.otlp.metrics.export.step=2s +management.otlp.metrics.export.url=http://localhost:4318/v1/metrics +management.metrics.distribution.sla.http.server.requests=1ms,10ms,50ms,100ms,200ms,500ms,1s,2s,5s +management.metrics.distribution.percentiles-histogram[timer]=true +management.metrics.distribution.sla[timer]=0.1ms,0.5ms,1ms,10ms,50ms,100ms,200ms,500ms,1s,2s,5s +management.metrics.export.defaults.step=15s management.metrics.distribution.percentiles-histogram.http.server.requests=true -management.metrics.tags.application=${spring.application.name} +management.metrics.tags.service_name=${spring.application.name} management.metrics.tags.environment=${spring.profiles.active} +management.prometheus.metrics.export.enabled=false # OpenTelemetry Logging Configuration management.otlp.logging.export.enabled=true management.otlp.logging.endpoint=http://localhost:4318/v1/logs -otel.instrumentation.logback-appender.experimental.capture-mdc-attributes=* otel.instrumentation.logback-appender.experimental-log-attributes=true otel.instrumentation.logback-appender.experimental.capture-code-attributes=true otel.instrumentation.logback-appender.experimental.capture-marker-attribute=true - -# OpenTelemetry Metrics Configuration -management.otlp.metrics.export.enabled=true -management.otlp.metrics.export.step=10s -management.otlp.metrics.export.url=http://localhost:4318/v1/metrics +otel.instrumentation.logback-appender.experimental.capture-mdc-attributes=trace_id,span_id +logging.context.enabled=true # Tracing Configuration management.tracing.sampling.probability=1.0 @@ -58,6 +123,7 @@ management.httpexchanges.recording.enabled=true management.tracing.baggage.correlation.enabled=true management.tracing.opentelemetry.export.include-unsampled=true management.observations.annotations.enabled=true +management.observations.enable.all=true otel.instrumentation.http.client.emit-experimental-telemetry=true otel.instrumentation.runtime-telemetry-java17.enabled=true otel.instrumentation.spring-webmvc.enabled=true @@ -66,10 +132,25 @@ otel.instrumentation.http.client.capture-request-headers=true otel.instrumentation.http.client.capture-response-headers=true otel.instrumentation.http.client.experimental.redact-query-parameters=false otel.instrumentation.jdbc.experimental.transaction.enabled=true +otel.resource.attributes.exclude=process.command_args,process.command_line otel.propagators=tracecontext,baggage otel.traces.sampler=parentbased_traceidratio otel.traces.sampler.arg=1 +# Enhanced Spring Tracing Configuration +otel.instrumentation.spring-boot.enabled=true +otel.instrumentation.spring-data.enabled=true +otel.instrumentation.spring-rabbit.enabled=true +otel.instrumentation.spring-security.enabled=true + +# Additional Spring instrumentation details +otel.instrumentation.spring-webmvc.experimental.capture-request-parameters=true +otel.instrumentation.spring-webmvc.experimental.capture-controller-telemetry=true +otel.instrumentation.spring-webmvc.experimental.capture-view-telemetry=true + +# Enhanced JPA/Hibernate tracing +otel.instrumentation.hibernate.experimental.span-suppression-strategy=statement-only +otel.instrumentation.jpa.experimental.query-reporting=true # Spring Modulith Observability Configuration spring.modulith.events.externalization.enabled=true @@ -87,4 +168,4 @@ idoris.base-url=http://localhost:8095/api idoris.validation-level=info idoris.validation-policy=strict idoris.typed-pid-maker.base-url=http://localhost:8090 -idoris.typed-pid-maker.timeout=5000 +idoris.typed-pid-maker.timeout=5000 \ No newline at end of file diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml new file mode 100644 index 0000000..92302d4 --- /dev/null +++ b/src/main/resources/log4j2.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index 484785a..e1c6f28 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -22,16 +22,19 @@ - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} [trace_id=%X{trace_id:-}, span_id=%X{span_id:-}] - + %msg%n true - * true true + true + true + * From 0d89e2d6bdca5d0d7bcfa0f73eb4a30d8e906e3e Mon Sep 17 00:00:00 2001 From: Maximilian Inckmann Date: Fri, 15 Aug 2025 16:10:22 +0200 Subject: [PATCH 10/19] added more observability annotations Signed-off-by: Maximilian Inckmann --- build.gradle | 46 ++++--- .../web/v1/AttributeController.java | 49 ++++++-- .../datatypes/rules/AcyclicityValidator.java | 60 +++++---- .../client/TypedPIDMakerClientConfig.java | 9 +- .../services/PersistentIdentifierService.java | 57 +++++++-- .../idoris/pids/utils/PIDRecordMapper.java | 12 +- .../idoris/pids/web/v1/PidController.java | 22 +++- .../idoris/rules/logic/RuleService.java | 101 +++++++-------- .../idoris/rules/logic/Visitor.java | 25 ++-- .../validation/InheritanceValidator.java | 13 ++ .../rules/validation/SyntaxValidator.java | 33 ++++- .../validation/ValidationPolicyValidator.java | 14 ++- .../rules/validation/ValidationVisitor.java | 8 +- .../services/TechnologyInterfaceService.java | 39 +++++- .../web/v1/TechnologyInterfaceController.java | 85 +++++++++++-- .../idoris/users/services/UserService.java | 15 +-- .../idoris/users/web/v1/UserController.java | 115 +++++++++++++++--- src/main/resources/application.properties | 57 --------- src/main/resources/logback-spring.xml | 44 ------- 19 files changed, 509 insertions(+), 295 deletions(-) delete mode 100644 src/main/resources/logback-spring.xml diff --git a/build.gradle b/build.gradle index ade8e7a..83e4abc 100644 --- a/build.gradle +++ b/build.gradle @@ -65,7 +65,7 @@ repositories { } ext { - springBootVersion = "3.5.0" + springBootVersion = "3.5.4" springDocVersion = "2.8.9" errorproneVersion = "2.38.0" errorproneJavacVersion = "9+181-r4173-1" @@ -73,11 +73,13 @@ ext { javersVersion = "7.3.7" micrometerVersion = "1.12.5" openTelemetryVersion = "1.49.0" - openTelemetryInstrumentationVersion = "2.16.0" + openTelemetryInstrumentationVersion = "2.18.1" logbackVersion = "1.5.13" pyroscopeVersion = "0.13.0" + lombokVersion = "1.18.38" + springRestDocsVersion = "3.0.3" set("snippetsDir", file("build/generated-snippets")) - set('springModulithVersion', "1.4.1") + set("springModulithVersion", "1.4.1") } dependencies { @@ -85,7 +87,7 @@ dependencies { implementation platform("org.springframework.boot:spring-boot-dependencies:${springBootVersion}") /* Rules API */ - implementation project(':rules-api') + implementation project(":rules-api") /* Spring Boot starters (version comes from the BOM) */ implementation "org.springframework.boot:spring-boot-starter-web" @@ -93,12 +95,10 @@ dependencies { implementation "org.springframework.boot:spring-boot-starter-actuator" implementation "org.springframework.boot:spring-boot-starter-hateoas" implementation "org.springframework.boot:spring-boot-starter-validation" - implementation "org.springframework:spring-web" implementation "org.springframework.modulith:spring-modulith-starter-core" implementation "org.springframework.modulith:spring-modulith-starter-neo4j:${springModulithVersion}" implementation "org.springframework.modulith:spring-modulith-events-api:${springModulithVersion}" - runtimeOnly "org.springframework.boot:spring-boot-starter-actuator" - runtimeOnly 'org.springframework.modulith:spring-modulith-runtime' + runtimeOnly "org.springframework.modulith:spring-modulith-runtime" runtimeOnly "org.springframework.modulith:spring-modulith-observability:${springModulithVersion}" runtimeOnly "org.springframework.modulith:spring-modulith-actuator:${springModulithVersion}" runtimeOnly "org.springframework.modulith:spring-modulith-starter-insight:${springModulithVersion}" @@ -111,29 +111,28 @@ dependencies { /* HTTP client */ implementation "org.apache.httpcomponents.client5:httpclient5:${httpClientVersion}" - /* Observability */ - implementation(platform("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom:2.18.0")) + implementation(platform("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom:${openTelemetryInstrumentationVersion}")) implementation "io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter" - implementation 'io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations' - implementation "io.opentelemetry.contrib:opentelemetry-samplers:1.47.0-alpha" + implementation "io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations" + implementation "io.opentelemetry.contrib:opentelemetry-samplers:1.48.0-alpha" implementation "org.springframework.boot:spring-boot-starter-aop" - implementation 'io.micrometer:micrometer-tracing-bridge-otel' - implementation 'io.opentelemetry:opentelemetry-exporter-otlp' + implementation "io.micrometer:micrometer-tracing-bridge-otel" + implementation "io.opentelemetry:opentelemetry-exporter-otlp" /* Development helpers */ implementation "org.springframework.boot:spring-boot-configuration-processor" developmentOnly "org.springframework.boot:spring-boot-devtools" /* Lombok */ - compileOnly "org.projectlombok:lombok:1.18.38" - annotationProcessor "org.projectlombok:lombok:1.18.38" + compileOnly "org.projectlombok:lombok:${lombokVersion}" + annotationProcessor "org.projectlombok:lombok:${lombokVersion}" - /* JavaX Annotations */ - implementation 'javax.annotation:javax.annotation-api:1.3.2' + /* Jakarta Annotations */ + implementation "jakarta.annotation:jakarta.annotation-api" // Add the processor module - annotationProcessor project(':rules-processor') + annotationProcessor project(":rules-processor") /* Error-prone */ errorprone "com.google.errorprone:error_prone_core:${errorproneVersion}" @@ -141,18 +140,17 @@ dependencies { /* Tests */ testImplementation "org.springframework.boot:spring-boot-starter-test" - testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc:3.0.3" + testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc:${springRestDocsVersion}" testImplementation "org.springframework.security:spring-security-test" - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.modulith:spring-modulith-starter-test' - testImplementation "org.junit.jupiter:junit-jupiter:5.13.0" - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation "org.springframework.modulith:spring-modulith-starter-test" + testImplementation "org.junit.jupiter:junit-jupiter" + testRuntimeOnly "org.junit.platform:junit-platform-launcher" } dependencyManagement { imports { mavenBom "org.springframework.modulith:spring-modulith-bom:${springModulithVersion}" - mavenBom("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom:2.17.1") + mavenBom("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom:${openTelemetryInstrumentationVersion}") } } diff --git a/src/main/java/edu/kit/datamanager/idoris/attributes/web/v1/AttributeController.java b/src/main/java/edu/kit/datamanager/idoris/attributes/web/v1/AttributeController.java index f0b261a..b76139d 100644 --- a/src/main/java/edu/kit/datamanager/idoris/attributes/web/v1/AttributeController.java +++ b/src/main/java/edu/kit/datamanager/idoris/attributes/web/v1/AttributeController.java @@ -28,6 +28,7 @@ import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.annotations.SpanAttribute; import io.opentelemetry.instrumentation.annotations.WithSpan; +import lombok.extern.slf4j.Slf4j; import org.springframework.hateoas.CollectionModel; import org.springframework.hateoas.EntityModel; import org.springframework.http.HttpStatus; @@ -47,6 +48,7 @@ */ @RestController @RequestMapping("/v1/attributes") +@Slf4j @Observed(contextualName = "attributeController") public class AttributeController implements IAttributeApi { @@ -68,6 +70,7 @@ public AttributeController(AttributeService attributeService, AttributeModelAsse @Timed(value = "attributeController.getAllAttributes", description = "Time taken to get all attributes", histogram = true) @Counted(value = "attributeController.getAllAttributes.count", description = "Number of get all attributes requests") public ResponseEntity>> getAllAttributes() { + log.debug("Getting all Attributes"); List> attributes = attributeService.getAllAttributes().stream() .map(attributeModelAssembler::toModel) .collect(Collectors.toList()); @@ -77,6 +80,7 @@ public ResponseEntity>> getAllAttributes( linkTo(methodOn(AttributeController.class).getAllAttributes()).withSelfRel() ); + log.info("Retrieved {} attributes", attributes.size()); return ResponseEntity.ok(collectionModel); } @@ -87,11 +91,18 @@ public ResponseEntity>> getAllAttributes( @WithSpan(kind = SpanKind.SERVER) @Timed(value = "attributeController.getAttribute", description = "Time taken to get an attribute", histogram = true) @Counted(value = "attributeController.getAttribute.count", description = "Number of get attribute requests") - public ResponseEntity> getAttribute(@SpanAttribute String pid) { + public ResponseEntity> getAttribute(@SpanAttribute("attribute.pid") String pid) { + log.debug("Getting Attribute with PID: {}", pid); return attributeService.getAttribute(pid) - .map(attributeModelAssembler::toModel) + .map(attribute -> { + log.info("Found Attribute with PID: {}", pid); + return attributeModelAssembler.toModel(attribute); + }) .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); + .orElseGet(() -> { + log.warn("Attribute not found with PID: {}", pid); + return ResponseEntity.notFound().build(); + }); } /** @@ -101,12 +112,19 @@ public ResponseEntity> getAttribute(@SpanAttribute String @WithSpan(kind = SpanKind.SERVER) @Timed(value = "attributeController.getDataType", description = "Time taken to get a data type", histogram = true) @Counted(value = "attributeController.getDataType.count", description = "Number of get data type requests") - public ResponseEntity> getDataType(@SpanAttribute String pid) { + public ResponseEntity> getDataType(@SpanAttribute("attribute.pid") String pid) { + log.debug("Getting DataType for Attribute with PID: {}", pid); return attributeService.getAttribute(pid) - .map(Attribute::getDataType) + .map(attribute -> { + log.info("Found DataType for Attribute with PID: {}", pid); + return attribute.getDataType(); + }) .map(dataTypeModelAssembler::toModel) .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); + .orElseGet(() -> { + log.warn("Attribute not found with PID: {}", pid); + return ResponseEntity.notFound().build(); + }); } /** @@ -117,8 +135,10 @@ public ResponseEntity> getDataType(@SpanAttribute String p @Timed(value = "attributeController.createAttribute", description = "Time taken to create an attribute", histogram = true) @Counted(value = "attributeController.createAttribute.count", description = "Number of create attribute requests") public ResponseEntity> createAttribute(@SpanAttribute Attribute attribute) { + log.debug("Creating Attribute: {}", attribute.getName()); Attribute createdAttribute = attributeService.createAttribute(attribute); EntityModel entityModel = attributeModelAssembler.toModel(createdAttribute); + log.info("Created Attribute with PID: {}", createdAttribute.getId()); return ResponseEntity.status(HttpStatus.CREATED).body(entityModel); } @@ -129,9 +149,11 @@ public ResponseEntity> createAttribute(@SpanAttribute Att @WithSpan(kind = SpanKind.SERVER) @Timed(value = "attributeController.updateAttribute", description = "Time taken to update an attribute", histogram = true) @Counted(value = "attributeController.updateAttribute.count", description = "Number of update attribute requests") - public ResponseEntity> updateAttribute(@SpanAttribute String id, @SpanAttribute Attribute attribute) { + public ResponseEntity> updateAttribute(@SpanAttribute("attribute.id") String id, @SpanAttribute Attribute attribute) { + log.debug("Updating Attribute with ID: {}", id); // Check if the entity exists if (attributeService.getAttribute(id).isEmpty()) { + log.warn("Attribute not found with ID: {}", id); return ResponseEntity.notFound().build(); } @@ -146,6 +168,7 @@ public ResponseEntity> updateAttribute(@SpanAttribute Str Attribute updatedAttribute = attributeService.updateAttribute(attribute); EntityModel entityModel = attributeModelAssembler.toModel(updatedAttribute); + log.info("Updated Attribute with ID: {}", id); return ResponseEntity.ok(entityModel); } @@ -156,12 +179,15 @@ public ResponseEntity> updateAttribute(@SpanAttribute Str @WithSpan(kind = SpanKind.SERVER) @Timed(value = "attributeController.deleteAttribute", description = "Time taken to delete an attribute", histogram = true) @Counted(value = "attributeController.deleteAttribute.count", description = "Number of delete attribute requests") - public ResponseEntity deleteAttribute(@SpanAttribute String pid) { + public ResponseEntity deleteAttribute(@SpanAttribute("attribute.pid") String pid) { + log.debug("Deleting Attribute with PID: {}", pid); if (attributeService.getAttribute(pid).isEmpty()) { + log.warn("Attribute not found with PID: {}", pid); return ResponseEntity.notFound().build(); } attributeService.deleteAttribute(pid); + log.info("Deleted Attribute with PID: {}", pid); return ResponseEntity.noContent().build(); } @@ -173,7 +199,9 @@ public ResponseEntity deleteAttribute(@SpanAttribute String pid) { @Timed(value = "attributeController.deleteOrphanedAttributes", description = "Time taken to delete orphaned attributes", histogram = true) @Counted(value = "attributeController.deleteOrphanedAttributes.count", description = "Number of delete orphaned attributes requests") public ResponseEntity deleteOrphanedAttributes() { + log.debug("Deleting orphaned attributes"); attributeService.deleteOrphanedAttributes(); + log.info("Deleted orphaned attributes"); return ResponseEntity.noContent().build(); } @@ -184,13 +212,16 @@ public ResponseEntity deleteOrphanedAttributes() { @WithSpan(kind = SpanKind.SERVER) @Timed(value = "attributeController.patchAttribute", description = "Time taken to patch an attribute", histogram = true) @Counted(value = "attributeController.patchAttribute.count", description = "Number of patch attribute requests") - public ResponseEntity> patchAttribute(@SpanAttribute String pid, @SpanAttribute Attribute attributePatch) { + public ResponseEntity> patchAttribute(@SpanAttribute("attribute.pid") String pid, @SpanAttribute Attribute attributePatch) { + log.debug("Patching Attribute with PID: {}", pid); if (attributeService.getAttribute(pid).isEmpty()) { + log.warn("Attribute not found with PID: {}", pid); return ResponseEntity.notFound().build(); } Attribute patchedAttribute = attributeService.patchAttribute(pid, attributePatch); EntityModel entityModel = attributeModelAssembler.toModel(patchedAttribute); + log.info("Patched Attribute with PID: {}", pid); return ResponseEntity.ok(entityModel); } } diff --git a/src/main/java/edu/kit/datamanager/idoris/datatypes/rules/AcyclicityValidator.java b/src/main/java/edu/kit/datamanager/idoris/datatypes/rules/AcyclicityValidator.java index 798cee5..e01e462 100644 --- a/src/main/java/edu/kit/datamanager/idoris/datatypes/rules/AcyclicityValidator.java +++ b/src/main/java/edu/kit/datamanager/idoris/datatypes/rules/AcyclicityValidator.java @@ -23,6 +23,11 @@ import edu.kit.datamanager.idoris.rules.validation.SyntaxValidator; import edu.kit.datamanager.idoris.rules.validation.ValidationResult; import edu.kit.datamanager.idoris.rules.validation.ValidationVisitor; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.annotation.Timed; +import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.annotations.WithSpan; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.neo4j.core.Neo4jClient; @@ -32,6 +37,7 @@ @Slf4j @Component +@Observed(contextualName = "acyclicityValidator") @Rule( appliesTo = { edu.kit.datamanager.idoris.datatypes.entities.AtomicDataType.class, @@ -47,11 +53,15 @@ public class AcyclicityValidator extends ValidationVisitor { private Neo4jClient neo4jClient; @Override + @WithSpan(kind = SpanKind.INTERNAL) public ValidationResult visit(AtomicDataType atomicDataType, Object... args) { return doesNotInheritItself(atomicDataType); } @Override + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "rules.acyclicityValidator.visitTypeProfile", description = "Time to check acyclicity for TypeProfile", histogram = true) + @Counted(value = "rules.acyclicityValidator.visitTypeProfile.count", description = "Number of TypeProfile acyclicity validations") public ValidationResult visit(TypeProfile profile, Object... args) { return ValidationResult.combine( doesNotInheritItself(profile), @@ -59,34 +69,15 @@ public ValidationResult visit(TypeProfile profile, Object... args) { ); } - /** - * Validates that a DataType (TypeProfile or AtomicDataType) does not inherit from itself, preventing circular inheritance. - * - * @param dataType The DataType to validate - * @return ValidationResult containing any validation errors - */ - private ValidationResult doesNotInheritItself(DataType dataType) { - String query = "MATCH path = (n:DataType {id: $nodeID})-[:inheritsFrom*1..]->(n) RETURN path"; - - // Query the path from the Neo4j database - var path = neo4jClient.query(query) - .bind(dataType.getId()).to("nodeID") - .fetch() - .all(); - - if (!path.isEmpty()) { - return ValidationResult.error("Circular inheritance detected", Map.of("element", dataType, "path", path)); - } else { - return ValidationResult.ok(); - } - } - /** * Validates that a TypeProfile does not use itself as an attribute type, either directly or through overrides. * * @param profile The TypeProfile to validate * @return ValidationResult containing any validation errors */ + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "rules.acyclicityValidator.doesNotUseItselfAsAttribute", description = "Time to check for self-reference as attribute", histogram = true) + @Counted(value = "rules.acyclicityValidator.doesNotUseItselfAsAttribute.count", description = "Number of self-reference attribute checks") private ValidationResult doesNotUseItselfAsAttribute(TypeProfile profile) { // Optimized single unified query to check for all types of self-reference cycles // Using MATCH...WHERE pattern for better readability and performance @@ -136,4 +127,29 @@ private ValidationResult doesNotUseItselfAsAttribute(TypeProfile profile) { return ValidationResult.ok(); } + + /** + * Validates that a DataType (TypeProfile or AtomicDataType) does not inherit from itself, preventing circular inheritance. + * + * @param dataType The DataType to validate + * @return ValidationResult containing any validation errors + */ + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "rules.acyclicityValidator.doesNotInheritItself", description = "Time to check self-inheritance acyclicity", histogram = true) + @Counted(value = "rules.acyclicityValidator.doesNotInheritItself.count", description = "Number of self-inheritance acyclicity checks") + private ValidationResult doesNotInheritItself(DataType dataType) { + String query = "MATCH path = (n:DataType {id: $nodeID})-[:inheritsFrom*1..]->(n) RETURN path"; + + // Query the path from the Neo4j database + var path = neo4jClient.query(query) + .bind(dataType.getId()).to("nodeID") + .fetch() + .all(); + + if (!path.isEmpty()) { + return ValidationResult.error("Circular inheritance detected", Map.of("element", dataType, "path", path)); + } else { + return ValidationResult.ok(); + } + } } diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClientConfig.java b/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClientConfig.java index 18751ad..d07a1dc 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClientConfig.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClientConfig.java @@ -18,6 +18,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import edu.kit.datamanager.idoris.configuration.TypedPIDMakerConfig; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.annotation.Timed; import io.micrometer.observation.annotation.Observed; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanContext; @@ -63,7 +65,10 @@ public class TypedPIDMakerClientConfig { */ @Bean @WithSpan(kind = SpanKind.CLIENT) + @Timed(value = "typedPIDMakerClientConfig.createClient", description = "Time taken to create TypedPIDMakerClient", histogram = true) + @Counted(value = "typedPIDMakerClientConfig.createClient.count", description = "Number of TypedPIDMakerClient creations") public TypedPIDMakerClient typedPIDMakerClient(TypedPIDMakerConfig config, ObjectMapper objectMapper) { + log.info("Creating TypedPIDMakerClient with base URL: {}", config.getBaseUrl()); // Create a client HTTP request factory with the configured timeout ClientHttpRequestFactory requestFactory = new DecodingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()); @@ -147,7 +152,9 @@ protected boolean canRead(MediaType mediaType) { .builderFor(RestClientAdapter.create(restClient)) .build(); - return factory.createClient(TypedPIDMakerClient.class); + TypedPIDMakerClient client = factory.createClient(TypedPIDMakerClient.class); + log.info("Successfully created TypedPIDMakerClient for base URL: {}", config.getBaseUrl()); + return client; } /** diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/services/PersistentIdentifierService.java b/src/main/java/edu/kit/datamanager/idoris/pids/services/PersistentIdentifierService.java index 974c295..6297111 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/services/PersistentIdentifierService.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/services/PersistentIdentifierService.java @@ -24,7 +24,11 @@ import edu.kit.datamanager.idoris.pids.entities.PersistentIdentifier; import edu.kit.datamanager.idoris.pids.repositories.PersistentIdentifierRepository; import edu.kit.datamanager.idoris.pids.utils.PIDRecordMapper; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.annotation.Timed; import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; import io.opentelemetry.instrumentation.annotations.WithSpan; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -43,7 +47,7 @@ */ @Service @Slf4j -@Observed +@Observed(contextualName = "persistentIdentifierService") public class PersistentIdentifierService { private final PersistentIdentifierRepository repository; @@ -79,8 +83,10 @@ public PersistentIdentifierService(PersistentIdentifierRepository repository, * @return The created PersistentIdentifier */ @Transactional - @WithSpan - public PersistentIdentifier createPersistentIdentifier(AdministrativeMetadata entity) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "persistentIdentifierService.createPersistentIdentifier", description = "Time taken to create a persistent identifier", histogram = true) + @Counted(value = "persistentIdentifierService.createPersistentIdentifier.count", description = "Number of persistent identifier creations") + public PersistentIdentifier createPersistentIdentifier(@SpanAttribute AdministrativeMetadata entity) { log.debug("Creating PersistentIdentifier for entity: {}", entity); // Check if a PID already exists for this entity @@ -154,8 +160,10 @@ public PersistentIdentifier createPersistentIdentifier(AdministrativeMetadata en * @return The updated PersistentIdentifier, or empty if no PID exists for the entity */ @Transactional - @WithSpan - public Optional markAsTombstone(AdministrativeMetadata entity) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "persistentIdentifierService.markAsTombstone", description = "Time taken to mark persistent identifier as tombstone", histogram = true) + @Counted(value = "persistentIdentifierService.markAsTombstone.count", description = "Number of persistent identifiers marked as tombstone") + public Optional markAsTombstone(@SpanAttribute AdministrativeMetadata entity) { log.debug("Marking PersistentIdentifier as tombstone for entity: {}", entity); // Find the PID for the entity @@ -188,8 +196,10 @@ public Optional markAsTombstone(AdministrativeMetadata ent * @return The updated PersistentIdentifier */ @Transactional - @WithSpan - public PersistentIdentifier updatePIDRecord(PersistentIdentifier pid) { + @WithSpan(kind = SpanKind.CLIENT) + @Timed(value = "persistentIdentifierService.updatePIDRecord", description = "Time taken to update PID record", histogram = true) + @Counted(value = "persistentIdentifierService.updatePIDRecord.count", description = "Number of PID record updates") + public PersistentIdentifier updatePIDRecord(@SpanAttribute PersistentIdentifier pid) { log.debug("Updating PID record for PersistentIdentifier: {}", pid); // Create a PID record with metadata from the entity @@ -227,7 +237,10 @@ public PersistentIdentifier updatePIDRecord(PersistentIdentifier pid) { * @param entity The entity to get the PID for * @return An Optional containing the PersistentIdentifier if found, or empty if not found */ - public Optional getPersistentIdentifier(AdministrativeMetadata entity) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "persistentIdentifierService.getPersistentIdentifierByEntity", description = "Time taken to get persistent identifier by entity", histogram = true) + @Counted(value = "persistentIdentifierService.getPersistentIdentifierByEntity.count", description = "Number of get persistent identifier by entity requests") + public Optional getPersistentIdentifier(@SpanAttribute AdministrativeMetadata entity) { log.debug("Getting PersistentIdentifier for entity: {}", entity); return repository.findByEntityInternalId(entity.getInternalId()); } @@ -238,7 +251,10 @@ public Optional getPersistentIdentifier(AdministrativeMeta * @param pid The PID to get the PersistentIdentifier for * @return An Optional containing the PersistentIdentifier if found, or empty if not found */ - public Optional getPersistentIdentifier(String pid) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "persistentIdentifierService.getPersistentIdentifierByPid", description = "Time taken to get persistent identifier by PID", histogram = true) + @Counted(value = "persistentIdentifierService.getPersistentIdentifierByPid.count", description = "Number of get persistent identifier by PID requests") + public Optional getPersistentIdentifier(@SpanAttribute("pid.value") String pid) { log.debug("Getting PersistentIdentifier with PID: {}", pid); return repository.findById(pid); } @@ -248,9 +264,14 @@ public Optional getPersistentIdentifier(String pid) { * * @return A list of all PersistentIdentifiers */ + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "persistentIdentifierService.getAllPersistentIdentifiers", description = "Time taken to get all persistent identifiers", histogram = true) + @Counted(value = "persistentIdentifierService.getAllPersistentIdentifiers.count", description = "Number of get all persistent identifiers requests") public List getAllPersistentIdentifiers() { log.debug("Getting all PersistentIdentifiers"); - return repository.findAll(); + List pids = repository.findAll(); + log.info("Retrieved {} persistent identifiers", pids.size()); + return pids; } /** @@ -259,9 +280,14 @@ public List getAllPersistentIdentifiers() { * @param entityType The type of entity to get PIDs for * @return A list of PersistentIdentifiers for entities of the given type */ - public List getPersistentIdentifiersByEntityType(String entityType) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "persistentIdentifierService.getPersistentIdentifiersByEntityType", description = "Time taken to get persistent identifiers by entity type", histogram = true) + @Counted(value = "persistentIdentifierService.getPersistentIdentifiersByEntityType.count", description = "Number of get persistent identifiers by entity type requests") + public List getPersistentIdentifiersByEntityType(@SpanAttribute("entity.type") String entityType) { log.debug("Getting PersistentIdentifiers for entity type: {}", entityType); - return repository.findByEntityType(entityType); + List pids = repository.findByEntityType(entityType); + log.info("Retrieved {} persistent identifiers for entity type: {}", pids.size(), entityType); + return pids; } /** @@ -269,8 +295,13 @@ public List getPersistentIdentifiersByEntityType(String en * * @return A list of PersistentIdentifiers that are tombstones */ + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "persistentIdentifierService.getTombstones", description = "Time taken to get tombstone persistent identifiers", histogram = true) + @Counted(value = "persistentIdentifierService.getTombstones.count", description = "Number of get tombstone persistent identifiers requests") public List getTombstones() { log.debug("Getting tombstone PersistentIdentifiers"); - return repository.findByTombstoneTrue(); + List tombstones = repository.findByTombstoneTrue(); + log.info("Retrieved {} tombstone persistent identifiers", tombstones.size()); + return tombstones; } } diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/utils/PIDRecordMapper.java b/src/main/java/edu/kit/datamanager/idoris/pids/utils/PIDRecordMapper.java index 88f4d19..003418e 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/utils/PIDRecordMapper.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/utils/PIDRecordMapper.java @@ -23,6 +23,10 @@ import edu.kit.datamanager.idoris.pids.client.model.PIDRecordEntry; import edu.kit.datamanager.idoris.pids.entities.PersistentIdentifier; import edu.kit.datamanager.idoris.users.entities.ORCiDUser; +import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.annotations.WithSpan; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -41,6 +45,7 @@ */ @Component @Slf4j +@Observed(contextualName = "pidRecordMapper") public class PIDRecordMapper { private final ApplicationProperties applicationProperties; @@ -65,7 +70,9 @@ public PIDRecordMapper(ApplicationProperties applicationProperties, TypedPIDMake * @param pid The PersistentIdentifier to convert * @return The converted PIDRecord */ - public PIDRecord toPIDRecord(PersistentIdentifier pid) { + @WithSpan(kind = SpanKind.INTERNAL) + public PIDRecord toPIDRecord(@SpanAttribute PersistentIdentifier pid) { + log.debug("Converting PersistentIdentifier to PIDRecord: {}", pid.getPid()); List recordEntries = new ArrayList<>(); AdministrativeMetadata entity = pid.getEntity(); @@ -176,7 +183,9 @@ public PIDRecord toPIDRecord(PersistentIdentifier pid) { * * @return The base URL */ + @WithSpan(kind = SpanKind.INTERNAL) private String getBaseUrl() { + log.debug("Getting base URL from application properties"); String baseUrl = applicationProperties.getBaseUrl(); if (baseUrl == null || baseUrl.trim().isEmpty()) { log.error("Base URL is not configured or is empty."); @@ -185,6 +194,7 @@ private String getBaseUrl() { if (baseUrl.endsWith("/")) { baseUrl = baseUrl.substring(0, baseUrl.length() - 1); } + log.debug("Using base URL: {}", baseUrl); return baseUrl; } } \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/web/v1/PidController.java b/src/main/java/edu/kit/datamanager/idoris/pids/web/v1/PidController.java index 7a768ff..27d3d85 100644 --- a/src/main/java/edu/kit/datamanager/idoris/pids/web/v1/PidController.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/web/v1/PidController.java @@ -19,6 +19,12 @@ import edu.kit.datamanager.idoris.pids.services.PersistentIdentifierService; import edu.kit.datamanager.idoris.pids.web.api.IPidApi; import edu.kit.datamanager.idoris.pids.web.hateoas.PersistentIdentifierModelAssembler; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.annotation.Timed; +import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.annotations.WithSpan; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -49,6 +55,7 @@ @RestController @RequestMapping("/v1/pid") @Slf4j +@Observed(contextualName = "pidController") @Tag(name = "Persistent Identifier", description = "API for accessing Persistent Identifiers (PIDs)") public class PidController implements IPidApi { @@ -74,6 +81,9 @@ public PidController(PersistentIdentifierService pidService, PersistentIdentifie */ @Override @GetMapping + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "pidController.getAllPersistentIdentifiers", description = "Time taken to get all persistent identifiers", histogram = true) + @Counted(value = "pidController.getAllPersistentIdentifiers.count", description = "Number of get all persistent identifiers requests") public ResponseEntity>> getAllPersistentIdentifiers() { log.debug("Getting all PersistentIdentifiers"); List> pids = pidService.getAllPersistentIdentifiers().stream() @@ -85,6 +95,7 @@ public ResponseEntity>> getAll linkTo(methodOn(PidController.class).getAllPersistentIdentifiers()).withSelfRel() ); + log.info("Retrieved {} persistent identifiers", pids.size()); return ResponseEntity.ok(collectionModel); } @@ -98,7 +109,10 @@ public ResponseEntity>> getAll */ @Override @GetMapping("/{pidValue}") - public ResponseEntity redirectToEntity(@PathVariable("pidValue") String pidValue) { + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "pidController.redirectToEntity", description = "Time taken to redirect to entity", histogram = true) + @Counted(value = "pidController.redirectToEntity.count", description = "Number of redirect to entity requests") + public ResponseEntity redirectToEntity(@SpanAttribute("pid.value") @PathVariable("pidValue") String pidValue) { log.debug("Redirecting PID: {}", pidValue); // Get the PersistentIdentifier for the given PID @@ -142,7 +156,10 @@ public ResponseEntity redirectToEntity(@PathVariable("pidValue") String pi */ @Override @GetMapping("/tombstone/{pidValue}") - public ResponseEntity handleTombstone(@PathVariable("pidValue") String pidValue) { + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "pidController.handleTombstone", description = "Time taken to handle tombstone request", histogram = true) + @Counted(value = "pidController.handleTombstone.count", description = "Number of tombstone requests") + public ResponseEntity handleTombstone(@SpanAttribute("pid.value") @PathVariable("pidValue") String pidValue) { log.debug("Handling tombstone request for PID: {}", pidValue); // Get the PersistentIdentifier for the given PID @@ -166,6 +183,7 @@ public ResponseEntity handleTombstone(@PathVariable("pidValue") String p String message = String.format("The entity with PID %s has been deleted at %s. Entity type: %s", pidValue, pid.getDeletedAt(), pid.getEntityType()); log.debug("Returning tombstone message: {}", message); + log.info("Served tombstone for PID: {}", pidValue); return ResponseEntity.status(HttpStatus.GONE) .body(message); } diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleService.java b/src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleService.java index d44f4d5..d75fd1c 100644 --- a/src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleService.java +++ b/src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleService.java @@ -16,6 +16,14 @@ package edu.kit.datamanager.idoris.rules.logic; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.annotation.Timed; +import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.annotations.WithSpan; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -31,50 +39,10 @@ import java.util.function.Supplier; import java.util.stream.Collectors; -/** - * Central service that manages rule discovery and execution based on precomputed dependency graphs. - *

    - * The RuleService is the orchestration core of the rule execution engine. It leverages a - * precomputed dependency graph generated at compile-time by the annotation processor to - * efficiently execute rules in the correct order. This approach eliminates the overhead - * of runtime dependency resolution and enables optimal parallel execution. - *

    - * This service follows an optimization-first design with several key principles: - *

      - *
    • Just-in-time rule instantiation: Only rules referenced in the precomputed - * graph are loaded, reducing memory usage and startup time
    • - *
    • Zero runtime dependency calculation: All rule ordering is determined - * at compile-time through static analysis
    • - *
    • Maximum parallelism: Rules are executed concurrently using CompletableFuture - * while still respecting their execution order
    • - *
    • Type-safe execution: Strong generic typing ensures rules receive - * compatible input types and produce correct output types
    • - *
    • Resilient processing: Failures in individual rules are isolated and won't - * cause the entire rule processing pipeline to fail
    • - *
    - *

    - * Usage example: - *

    - * {@code
    - * // Create a rule result factory
    - * Supplier resultFactory = ValidationResult::new;
    - *
    - * // Execute validation rules for an Operation
    - * ValidationResult result = ruleService.executeRules(
    - *     RuleTask.VALIDATE,
    - *     operation,
    - *     resultFactory
    - * );
    - * }
    - * 
    - *

    - * Extension points: The rule engine can be extended by implementing the {@link IRule} - * interface and annotating the implementation with {@link Rule}. The annotation processor will - * automatically incorporate the new rule into the precomputed graph. - */ @Component @RequiredArgsConstructor @Slf4j +@Observed(contextualName = "ruleService") public class RuleService { /** @@ -159,6 +127,7 @@ void initialize() { * which could happen if the annotation processor didn't run * or if the generated class is not on the classpath */ + @WithSpan(kind = SpanKind.INTERNAL) private void loadPrecomputedGraph() { log.info("Loading precomputed rule dependency graph..."); @@ -193,6 +162,7 @@ private void loadPrecomputedGraph() { * can help identify configuration issues early during application startup rather than * failing at runtime. */ + @WithSpan(kind = SpanKind.INTERNAL) private void discoverRequiredRules() { log.info("Discovering required rule implementations..."); @@ -288,8 +258,11 @@ private void discoverRequiredRules() { * @throws RuntimeException if a critical error occurs during rule execution that prevents * completion of the operation */ + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "rules.ruleService.executeRules", description = "Time to execute all rules for a given task/element", histogram = true) + @Counted(value = "rules.ruleService.executeRules.count", description = "Rule execution entrypoints") public > R executeRules( - RuleTask task, + @SpanAttribute RuleTask task, T element, Supplier resultFactory ) { @@ -336,8 +309,9 @@ public > R executeRules( * @return the merged result of all executed rules * @throws RuntimeException if rule execution fails critically */ + @WithSpan(kind = SpanKind.INTERNAL) private > R executeRulesInParallel( - List ruleClassNames, + @SpanAttribute List ruleClassNames, T element, Supplier resultFactory ) { @@ -390,29 +364,35 @@ private > R executeRulesInPa * @return a CompletableFuture containing the rule's execution result */ @SuppressWarnings("unchecked") + @WithSpan(kind = SpanKind.INTERNAL) private > CompletableFuture executeRule( IRule rule, T element, Supplier resultFactory ) { + // OpenTelemetry context propagation setup + Context parentOtelContext = io.opentelemetry.context.Context.current(); + return CompletableFuture.supplyAsync(() -> { - String ruleName = rule.getClass().getSimpleName(); - log.debug("Executing rule: {}", ruleName); - - R result = resultFactory.get(); - - try { - // Perform a type-safe cast to the specific generic parameter types needed for this rule execution - // This cast is guaranteed to be safe because the precomputed graph ensures type compatibility - IRule typedRule = (IRule) rule; - - // Execute the rule's processing logic with the input element and result container - typedRule.process(element, result); - log.debug("Rule {} execution completed successfully", ruleName); - return result; - } catch (Exception e) { - log.error("Rule {} execution failed: {}", ruleName, e.getMessage(), e); - throw new RuntimeException("Rule execution failed: " + e.getMessage(), e); + try (Scope scope = parentOtelContext.makeCurrent()) { + String ruleName = rule.getClass().getSimpleName(); + log.debug("Executing rule: {}", ruleName); + + R result = resultFactory.get(); + + try { + // Perform a type-safe cast to the specific generic parameter types needed for this rule execution + // This cast is guaranteed to be safe because the precomputed graph ensures type compatibility + IRule typedRule = (IRule) rule; + + // Execute the rule's processing logic with the input element and result container + typedRule.process(element, result); + log.debug("Rule {} execution completed successfully", ruleName); + return result; + } catch (Exception e) { + log.error("Rule {} execution failed: {}", ruleName, e.getMessage(), e); + throw new RuntimeException("Rule execution failed: " + e.getMessage(), e); + } } }); } @@ -437,6 +417,7 @@ private > CompletableFuture< * @param result type extending RuleOutput * @return a single merged result containing the combined output of all rules */ + @WithSpan(kind = SpanKind.INTERNAL) private > R mergeResults( List> resultFutures, Supplier resultFactory diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/logic/Visitor.java b/src/main/java/edu/kit/datamanager/idoris/rules/logic/Visitor.java index f2f449b..1e71da3 100644 --- a/src/main/java/edu/kit/datamanager/idoris/rules/logic/Visitor.java +++ b/src/main/java/edu/kit/datamanager/idoris/rules/logic/Visitor.java @@ -23,6 +23,7 @@ import edu.kit.datamanager.idoris.operations.entities.Operation; import edu.kit.datamanager.idoris.operations.entities.OperationStep; import edu.kit.datamanager.idoris.technologyinterfaces.entities.TechnologyInterface; +import io.micrometer.observation.annotation.Observed; import jakarta.validation.constraints.NotNull; import lombok.extern.slf4j.Slf4j; @@ -50,6 +51,7 @@ * @param the type of rule output produced by this visitor, must extend RuleOutput */ @Slf4j +@Observed public abstract class Visitor> { /** * Set to track visited element IDs to detect cycles in the visitation graph @@ -87,6 +89,17 @@ public T visit(Attribute attribute, Object... args) { return notAllowed(attribute); } + /** + * Handles an element of a type that is not supported by this visitor. + * Logs a warning and returns an empty output instance. + * + * @param element the element that is not allowed to be processed by this visitor + * @return an empty output instance + */ + protected T notAllowed(@NotNull VisitableElement element) { + log.warn("Element of type {} not allowed in {}. Ignoring...", element.getClass().getSimpleName(), this.getClass().getSimpleName()); + return outputFactory.get(); + } /** * Visits an AttributeMapping element and processes it. @@ -202,18 +215,6 @@ protected T handleCircle(String id) { return outputFactory.get().addMessage("Cycle detected", OutputMessage.MessageSeverity.ERROR, id); } - /** - * Handles an element of a type that is not supported by this visitor. - * Logs a warning and returns an empty output instance. - * - * @param element the element that is not allowed to be processed by this visitor - * @return an empty output instance - */ - protected T notAllowed(@NotNull VisitableElement element) { - log.warn("Element of type {} not allowed in {}. Ignoring...", element.getClass().getSimpleName(), this.getClass().getSimpleName()); - return outputFactory.get(); - } - /** * Saves the processing result for an element to the cache. * This prevents redundant processing if the same element is visited again. diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/validation/InheritanceValidator.java b/src/main/java/edu/kit/datamanager/idoris/rules/validation/InheritanceValidator.java index 14e69d7..94ad40b 100644 --- a/src/main/java/edu/kit/datamanager/idoris/rules/validation/InheritanceValidator.java +++ b/src/main/java/edu/kit/datamanager/idoris/rules/validation/InheritanceValidator.java @@ -21,6 +21,11 @@ import edu.kit.datamanager.idoris.datatypes.entities.TypeProfile; import edu.kit.datamanager.idoris.rules.logic.Rule; import edu.kit.datamanager.idoris.rules.logic.RuleTask; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.annotation.Timed; +import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.annotations.WithSpan; import lombok.extern.slf4j.Slf4j; import static edu.kit.datamanager.idoris.rules.logic.OutputMessage.MessageSeverity.ERROR; @@ -32,6 +37,7 @@ * inherited properties maintain consistency with parent entities. */ @Slf4j +@Observed(contextualName = "inheritanceValidator") @Rule( appliesTo = { AtomicDataType.class, @@ -52,6 +58,7 @@ public class InheritanceValidator extends ValidationVisitor { * @return ValidationResult containing any validation errors */ @Override + @WithSpan(kind = SpanKind.INTERNAL) public ValidationResult visit(Attribute attribute, Object... args) { ValidationResult result = new ValidationResult(); @@ -85,6 +92,9 @@ public ValidationResult visit(Attribute attribute, Object... args) { * @return ValidationResult containing any validation errors */ @Override + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "rules.inheritanceValidator.visitAtomicDataType", description = "Time to validate inheritance for AtomicDataType", histogram = true) + @Counted(value = "rules.inheritanceValidator.visitAtomicDataType.count", description = "Number of AtomicDataType inheritance validations") public ValidationResult visit(AtomicDataType atomicDataType, Object... args) { ValidationResult result = new ValidationResult(); @@ -130,6 +140,9 @@ else if (!atomicDataType.getForbiddenValues().containsAll(parent.getForbiddenVal * @return ValidationResult containing any validation errors */ @Override + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "rules.inheritanceValidator.visitTypeProfile", description = "Time to validate inheritance for TypeProfile", histogram = true) + @Counted(value = "rules.inheritanceValidator.visitTypeProfile.count", description = "Number of TypeProfile inheritance validations") public ValidationResult visit(TypeProfile typeProfile, Object... args) { ValidationResult result = new ValidationResult(); diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/validation/SyntaxValidator.java b/src/main/java/edu/kit/datamanager/idoris/rules/validation/SyntaxValidator.java index 9a5bdbe..27d994b 100644 --- a/src/main/java/edu/kit/datamanager/idoris/rules/validation/SyntaxValidator.java +++ b/src/main/java/edu/kit/datamanager/idoris/rules/validation/SyntaxValidator.java @@ -29,18 +29,19 @@ import edu.kit.datamanager.idoris.rules.logic.Rule; import edu.kit.datamanager.idoris.rules.logic.RuleTask; import edu.kit.datamanager.idoris.technologyinterfaces.entities.TechnologyInterface; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.annotation.Timed; +import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.annotations.WithSpan; import lombok.extern.slf4j.Slf4j; import java.util.Arrays; import static edu.kit.datamanager.idoris.rules.logic.OutputMessage.MessageSeverity.*; -/** - * Rule-based validator that checks syntax constraints for entities. - * This validator ensures that entities follow the required syntax rules - * for better usability and correctness. - */ @Slf4j +@Observed(contextualName = "syntaxValidator") @Rule( appliesTo = { AtomicDataType.class, @@ -66,6 +67,7 @@ public class SyntaxValidator extends ValidationVisitor { * @return ValidationResult containing any validation errors */ @Override + @WithSpan(kind = SpanKind.INTERNAL) public ValidationResult visit(Attribute attribute, Object... args) { ValidationResult result = new ValidationResult(); @@ -113,6 +115,9 @@ public ValidationResult visit(Attribute attribute, Object... args) { * @return ValidationResult containing any validation errors */ @Override + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "rules.syntaxValidator.visitAttributeMapping", description = "Time to validate syntax for AttributeMapping", histogram = true) + @Counted(value = "rules.syntaxValidator.visitAttributeMapping.count", description = "Number of AttributeMapping syntax validations") public ValidationResult visit(AttributeMapping attributeMapping, Object... args) { ValidationResult result = new ValidationResult(); @@ -168,6 +173,9 @@ public ValidationResult visit(AttributeMapping attributeMapping, Object... args) * @return ValidationResult containing any validation errors */ @Override + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "rules.syntaxValidator.visitAtomicDataType", description = "Time to validate syntax for AtomicDataType", histogram = true) + @Counted(value = "rules.syntaxValidator.visitAtomicDataType.count", description = "Number of AtomicDataType syntax validations") public ValidationResult visit(AtomicDataType atomicDataType, Object... args) { ValidationResult result = new ValidationResult(); @@ -209,6 +217,9 @@ public ValidationResult visit(AtomicDataType atomicDataType, Object... args) { * @return ValidationResult containing any validation errors */ @Override + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "rules.syntaxValidator.visitTypeProfile", description = "Time to validate syntax for TypeProfile", histogram = true) + @Counted(value = "rules.syntaxValidator.visitTypeProfile.count", description = "Number of TypeProfile syntax validations") public ValidationResult visit(TypeProfile typeProfile, Object... args) { ValidationResult result = new ValidationResult(); @@ -230,6 +241,9 @@ public ValidationResult visit(TypeProfile typeProfile, Object... args) { * @return ValidationResult containing any validation errors */ @Override + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "rules.syntaxValidator.visitOperation", description = "Time to validate syntax for Operation", histogram = true) + @Counted(value = "rules.syntaxValidator.visitOperation.count", description = "Number of Operation syntax validations") public ValidationResult visit(Operation operation, Object... args) { ValidationResult result = new ValidationResult(); @@ -268,6 +282,9 @@ public ValidationResult visit(Operation operation, Object... args) { * @return ValidationResult containing any validation errors */ @Override + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "rules.syntaxValidator.visitOperationStep", description = "Time to validate syntax for OperationStep", histogram = true) + @Counted(value = "rules.syntaxValidator.visitOperationStep.count", description = "Number of OperationStep syntax validations") public ValidationResult visit(OperationStep operationStep, Object... args) { ValidationResult result = new ValidationResult(); @@ -303,6 +320,9 @@ public ValidationResult visit(OperationStep operationStep, Object... args) { * @return ValidationResult containing any validation errors */ @Override + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "rules.syntaxValidator.visitTechnologyInterface", description = "Time to validate syntax for TechnologyInterface", histogram = true) + @Counted(value = "rules.syntaxValidator.visitTechnologyInterface.count", description = "Number of TechnologyInterface syntax validations") public ValidationResult visit(TechnologyInterface technologyInterface, Object... args) { ValidationResult result = new ValidationResult(); @@ -330,6 +350,9 @@ public ValidationResult visit(TechnologyInterface technologyInterface, Object... * @param dataType The data type to validate * @param result The validation result to add messages to */ + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "rules.syntaxValidator.validateDataType", description = "Time to validate common data type properties", histogram = true) + @Counted(value = "rules.syntaxValidator.validateDataType.count", description = "Number of data type property validations") private void validateDataType(DataType dataType, ValidationResult result) { if (dataType.getName() == null || dataType.getName().isEmpty()) { result.addMessage("For better human readability and understanding, you MUST provide a name for the data type.", diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationPolicyValidator.java b/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationPolicyValidator.java index b40af5e..0c5ccdb 100644 --- a/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationPolicyValidator.java +++ b/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationPolicyValidator.java @@ -21,6 +21,11 @@ import edu.kit.datamanager.idoris.datatypes.entities.TypeProfile; import edu.kit.datamanager.idoris.rules.logic.Rule; import edu.kit.datamanager.idoris.rules.logic.RuleTask; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.annotation.Timed; +import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.annotations.WithSpan; import lombok.extern.slf4j.Slf4j; import java.util.HashMap; @@ -35,6 +40,7 @@ * are followed correctly. */ @Slf4j +@Observed(contextualName = "validationPolicyValidator") @Rule( appliesTo = { TypeProfile.class @@ -53,6 +59,9 @@ public class ValidationPolicyValidator extends ValidationVisitor { * @return ValidationResult containing any validation errors */ @Override + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "rules.validationPolicyValidator.visitTypeProfile", description = "Time to validate policy for TypeProfile", histogram = true) + @Counted(value = "rules.validationPolicyValidator.visitTypeProfile.count", description = "Number of TypeProfile validation policy validations") public ValidationResult visit(TypeProfile typeProfile, Object... args) { ValidationResult result = new ValidationResult(); @@ -134,7 +143,10 @@ public ValidationResult visit(TypeProfile typeProfile, Object... args) { * @param otherInformation Additional information to include * @return Map containing elementary information about the type profiles */ - private Object getTypeProfileAndParentElementaryInformation(TypeProfile typeProfile, TypeProfile parent, Map otherInformation) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "rules.validationPolicyValidator.getElementaryInformation", description = "Time to build elementary information for validation message", histogram = true) + @Counted(value = "rules.validationPolicyValidator.getElementaryInformation.count", description = "Number of elementary information builds") + private Map getTypeProfileAndParentElementaryInformation(TypeProfile typeProfile, TypeProfile parent, Map otherInformation) { Map result = new HashMap<>(); result.put("this", new ElementaryInformation(typeProfile.getId(), typeProfile.getName(), typeProfile.getValidationPolicy())); result.put("parent", new ElementaryInformation(parent.getId(), parent.getName(), parent.getValidationPolicy())); diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationVisitor.java b/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationVisitor.java index 851ab59..baf844c 100644 --- a/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationVisitor.java +++ b/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationVisitor.java @@ -33,7 +33,12 @@ import edu.kit.datamanager.idoris.core.domain.VisitableElement; import edu.kit.datamanager.idoris.rules.logic.IRule; import edu.kit.datamanager.idoris.rules.logic.Visitor; +import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.annotations.WithSpan; +@Observed public abstract class ValidationVisitor extends Visitor implements IRule { /** @@ -52,7 +57,8 @@ public ValidationVisitor() { * @param output the output to update with processing results */ @Override - public void process(VisitableElement input, ValidationResult output) { + @WithSpan(kind = SpanKind.INTERNAL) + public void process(@SpanAttribute VisitableElement input, @SpanAttribute ValidationResult output) { ValidationResult result = input.execute(this); output.merge(result); } diff --git a/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/services/TechnologyInterfaceService.java b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/services/TechnologyInterfaceService.java index 710bbe3..639c6ac 100644 --- a/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/services/TechnologyInterfaceService.java +++ b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/services/TechnologyInterfaceService.java @@ -19,6 +19,12 @@ import edu.kit.datamanager.idoris.core.events.EventPublisherService; import edu.kit.datamanager.idoris.technologyinterfaces.dao.ITechnologyInterfaceDao; import edu.kit.datamanager.idoris.technologyinterfaces.entities.TechnologyInterface; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.annotation.Timed; +import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.annotations.WithSpan; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -33,6 +39,7 @@ */ @Service @Slf4j +@Observed(contextualName = "technologyInterfaceService") public class TechnologyInterfaceService { private final ITechnologyInterfaceDao technologyInterfaceDao; private final EventPublisherService eventPublisher; @@ -55,7 +62,10 @@ public TechnologyInterfaceService(ITechnologyInterfaceDao technologyInterfaceDao * @return the created TechnologyInterface entity */ @Transactional - public TechnologyInterface createTechnologyInterface(TechnologyInterface technologyInterface) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "technologyInterfaceService.createTechnologyInterface", description = "Time taken to create a technology interface", histogram = true) + @Counted(value = "technologyInterfaceService.createTechnologyInterface.count", description = "Number of technology interface creations") + public TechnologyInterface createTechnologyInterface(@SpanAttribute TechnologyInterface technologyInterface) { log.debug("Creating TechnologyInterface: {}", technologyInterface); TechnologyInterface saved = technologyInterfaceDao.save(technologyInterface); eventPublisher.publishEntityCreated(saved); @@ -71,7 +81,10 @@ public TechnologyInterface createTechnologyInterface(TechnologyInterface technol * @throws IllegalArgumentException if the TechnologyInterface does not exist */ @Transactional - public TechnologyInterface updateTechnologyInterface(TechnologyInterface technologyInterface) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "technologyInterfaceService.updateTechnologyInterface", description = "Time taken to update a technology interface", histogram = true) + @Counted(value = "technologyInterfaceService.updateTechnologyInterface.count", description = "Number of technology interface updates") + public TechnologyInterface updateTechnologyInterface(@SpanAttribute TechnologyInterface technologyInterface) { log.debug("Updating TechnologyInterface: {}", technologyInterface); if (technologyInterface.getId() == null || technologyInterface.getId().isEmpty()) { @@ -97,7 +110,10 @@ public TechnologyInterface updateTechnologyInterface(TechnologyInterface technol * @throws IllegalArgumentException if the TechnologyInterface does not exist */ @Transactional - public void deleteTechnologyInterface(String id) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "technologyInterfaceService.deleteTechnologyInterface", description = "Time taken to delete a technology interface", histogram = true) + @Counted(value = "technologyInterfaceService.deleteTechnologyInterface.count", description = "Number of technology interface deletions") + public void deleteTechnologyInterface(@SpanAttribute("technologyInterface.id") String id) { log.debug("Deleting TechnologyInterface with ID: {}", id); TechnologyInterface technologyInterface = technologyInterfaceDao.findById(id) @@ -115,7 +131,10 @@ public void deleteTechnologyInterface(String id) { * @return an Optional containing the TechnologyInterface, or empty if not found */ @Transactional(readOnly = true) - public Optional getTechnologyInterface(String id) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "technologyInterfaceService.getTechnologyInterface", description = "Time taken to get a technology interface", histogram = true) + @Counted(value = "technologyInterfaceService.getTechnologyInterface.count", description = "Number of technology interface retrievals") + public Optional getTechnologyInterface(@SpanAttribute("technologyInterface.id") String id) { log.debug("Retrieving TechnologyInterface with ID: {}", id); return technologyInterfaceDao.findById(id); } @@ -126,9 +145,14 @@ public Optional getTechnologyInterface(String id) { * @return a list of all TechnologyInterface entities */ @Transactional(readOnly = true) + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "technologyInterfaceService.getAllTechnologyInterfaces", description = "Time taken to get all technology interfaces", histogram = true) + @Counted(value = "technologyInterfaceService.getAllTechnologyInterfaces.count", description = "Number of get all technology interfaces requests") public List getAllTechnologyInterfaces() { log.debug("Retrieving all TechnologyInterfaces"); - return technologyInterfaceDao.findAll(); + List interfaces = technologyInterfaceDao.findAll(); + log.info("Retrieved {} technology interfaces", interfaces.size()); + return interfaces; } /** @@ -140,7 +164,10 @@ public List getAllTechnologyInterfaces() { * @throws IllegalArgumentException if the TechnologyInterface does not exist */ @Transactional - public TechnologyInterface patchTechnologyInterface(String id, TechnologyInterface technologyInterfacePatch) { + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "technologyInterfaceService.patchTechnologyInterface", description = "Time taken to patch a technology interface", histogram = true) + @Counted(value = "technologyInterfaceService.patchTechnologyInterface.count", description = "Number of technology interface patches") + public TechnologyInterface patchTechnologyInterface(@SpanAttribute("technologyInterface.id") String id, @SpanAttribute TechnologyInterface technologyInterfacePatch) { log.debug("Patching TechnologyInterface with ID: {}, patch: {}", id, technologyInterfacePatch); if (id == null || id.isEmpty()) { throw new IllegalArgumentException("TechnologyInterface ID cannot be null or empty"); diff --git a/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/web/v1/TechnologyInterfaceController.java b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/web/v1/TechnologyInterfaceController.java index 714c9be..c395fd0 100644 --- a/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/web/v1/TechnologyInterfaceController.java +++ b/src/main/java/edu/kit/datamanager/idoris/technologyinterfaces/web/v1/TechnologyInterfaceController.java @@ -22,6 +22,13 @@ import edu.kit.datamanager.idoris.technologyinterfaces.services.TechnologyInterfaceService; import edu.kit.datamanager.idoris.technologyinterfaces.web.api.ITechnologyInterfaceApi; import edu.kit.datamanager.idoris.technologyinterfaces.web.hateoas.TechnologyInterfaceModelAssembler; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.annotation.Timed; +import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.annotations.WithSpan; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.hateoas.CollectionModel; import org.springframework.hateoas.EntityModel; @@ -43,6 +50,8 @@ */ @RestController @RequestMapping("/v1/technologyInterfaces") +@Slf4j +@Observed(contextualName = "technologyInterfaceController") public class TechnologyInterfaceController implements ITechnologyInterfaceApi { @Autowired @@ -58,7 +67,11 @@ public class TechnologyInterfaceController implements ITechnologyInterfaceApi { * {@inheritDoc} */ @Override + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "technologyInterfaceController.getAllTechnologyInterfaces", description = "Time taken to get all technology interfaces", histogram = true) + @Counted(value = "technologyInterfaceController.getAllTechnologyInterfaces.count", description = "Number of get all technology interfaces requests") public ResponseEntity>> getAllTechnologyInterfaces() { + log.debug("Getting all TechnologyInterfaces"); List> technologyInterfaces = StreamSupport.stream(technologyInterfaceService.getAllTechnologyInterfaces().spliterator(), false) .map(technologyInterfaceModelAssembler::toModel) .collect(Collectors.toList()); @@ -68,6 +81,7 @@ public ResponseEntity>> getAllT linkTo(methodOn(TechnologyInterfaceController.class).getAllTechnologyInterfaces()).withSelfRel() ); + log.info("Retrieved {} technology interfaces", technologyInterfaces.size()); return ResponseEntity.ok(collectionModel); } @@ -75,18 +89,32 @@ public ResponseEntity>> getAllT * {@inheritDoc} */ @Override - public ResponseEntity> getTechnologyInterface(String id) { + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "technologyInterfaceController.getTechnologyInterface", description = "Time taken to get a technology interface", histogram = true) + @Counted(value = "technologyInterfaceController.getTechnologyInterface.count", description = "Number of get technology interface requests") + public ResponseEntity> getTechnologyInterface(@SpanAttribute("technologyInterface.id") String id) { + log.debug("Getting TechnologyInterface with ID: {}", id); return technologyInterfaceService.getTechnologyInterface(id) - .map(technologyInterfaceModelAssembler::toModel) + .map(technologyInterface -> { + log.info("Found TechnologyInterface with ID: {}", id); + return technologyInterfaceModelAssembler.toModel(technologyInterface); + }) .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); + .orElseGet(() -> { + log.warn("TechnologyInterface not found with ID: {}", id); + return ResponseEntity.notFound().build(); + }); } /** * {@inheritDoc} */ @Override - public ResponseEntity>> getAttributes(String id) { + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "technologyInterfaceController.getAttributes", description = "Time taken to get technology interface attributes", histogram = true) + @Counted(value = "technologyInterfaceController.getAttributes.count", description = "Number of get technology interface attributes requests") + public ResponseEntity>> getAttributes(@SpanAttribute("technologyInterface.id") String id) { + log.debug("Getting attributes for TechnologyInterface with ID: {}", id); return technologyInterfaceService.getTechnologyInterface(id) .map(technologyInterface -> { List> attributes = StreamSupport.stream(technologyInterface.getAttributes().spliterator(), false) @@ -99,16 +127,24 @@ public ResponseEntity>> getAttributes(Str linkTo(methodOn(TechnologyInterfaceController.class).getTechnologyInterface(id)).withRel("technologyInterface") ); + log.info("Retrieved {} attributes for TechnologyInterface with ID: {}", attributes.size(), id); return ResponseEntity.ok(collectionModel); }) - .orElse(ResponseEntity.notFound().build()); + .orElseGet(() -> { + log.warn("TechnologyInterface not found with ID: {}", id); + return ResponseEntity.notFound().build(); + }); } /** * {@inheritDoc} */ @Override - public ResponseEntity>> getOutputs(String id) { + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "technologyInterfaceController.getOutputs", description = "Time taken to get technology interface outputs", histogram = true) + @Counted(value = "technologyInterfaceController.getOutputs.count", description = "Number of get technology interface outputs requests") + public ResponseEntity>> getOutputs(@SpanAttribute("technologyInterface.id") String id) { + log.debug("Getting outputs for TechnologyInterface with ID: {}", id); return technologyInterfaceService.getTechnologyInterface(id) .map(technologyInterface -> { List> outputs = StreamSupport.stream(technologyInterface.getOutputs().spliterator(), false) @@ -121,18 +157,27 @@ public ResponseEntity>> getOutputs(String linkTo(methodOn(TechnologyInterfaceController.class).getTechnologyInterface(id)).withRel("technologyInterface") ); + log.info("Retrieved {} outputs for TechnologyInterface with ID: {}", outputs.size(), id); return ResponseEntity.ok(collectionModel); }) - .orElse(ResponseEntity.notFound().build()); + .orElseGet(() -> { + log.warn("TechnologyInterface not found with ID: {}", id); + return ResponseEntity.notFound().build(); + }); } /** * {@inheritDoc} */ @Override - public ResponseEntity> createTechnologyInterface(TechnologyInterface technologyInterface) { + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "technologyInterfaceController.createTechnologyInterface", description = "Time taken to create a technology interface", histogram = true) + @Counted(value = "technologyInterfaceController.createTechnologyInterface.count", description = "Number of create technology interface requests") + public ResponseEntity> createTechnologyInterface(@SpanAttribute TechnologyInterface technologyInterface) { + log.debug("Creating TechnologyInterface: {}", technologyInterface.getName()); TechnologyInterface createdTechnologyInterface = technologyInterfaceService.createTechnologyInterface(technologyInterface); EntityModel entityModel = technologyInterfaceModelAssembler.toModel(createdTechnologyInterface); + log.info("Created TechnologyInterface with ID: {}", createdTechnologyInterface.getId()); return ResponseEntity.status(HttpStatus.CREATED).body(entityModel); } @@ -140,9 +185,14 @@ public ResponseEntity> createTechnologyInterfac * {@inheritDoc} */ @Override - public ResponseEntity> updateTechnologyInterface(String id, TechnologyInterface technologyInterface) { + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "technologyInterfaceController.updateTechnologyInterface", description = "Time taken to update a technology interface", histogram = true) + @Counted(value = "technologyInterfaceController.updateTechnologyInterface.count", description = "Number of update technology interface requests") + public ResponseEntity> updateTechnologyInterface(@SpanAttribute("technologyInterface.id") String id, @SpanAttribute TechnologyInterface technologyInterface) { + log.debug("Updating TechnologyInterface with ID: {}", id); // Check if the entity exists if (!technologyInterfaceService.getTechnologyInterface(id).isPresent()) { + log.warn("TechnologyInterface not found with ID: {}", id); return ResponseEntity.notFound().build(); } @@ -157,6 +207,7 @@ public ResponseEntity> updateTechnologyInterfac TechnologyInterface updatedTechnologyInterface = technologyInterfaceService.updateTechnologyInterface(technologyInterface); EntityModel entityModel = technologyInterfaceModelAssembler.toModel(updatedTechnologyInterface); + log.info("Updated TechnologyInterface with ID: {}", id); return ResponseEntity.ok(entityModel); } @@ -164,12 +215,18 @@ public ResponseEntity> updateTechnologyInterfac * {@inheritDoc} */ @Override - public ResponseEntity deleteTechnologyInterface(String id) { + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "technologyInterfaceController.deleteTechnologyInterface", description = "Time taken to delete a technology interface", histogram = true) + @Counted(value = "technologyInterfaceController.deleteTechnologyInterface.count", description = "Number of delete technology interface requests") + public ResponseEntity deleteTechnologyInterface(@SpanAttribute("technologyInterface.id") String id) { + log.debug("Deleting TechnologyInterface with ID: {}", id); if (!technologyInterfaceService.getTechnologyInterface(id).isPresent()) { + log.warn("TechnologyInterface not found with ID: {}", id); return ResponseEntity.notFound().build(); } technologyInterfaceService.deleteTechnologyInterface(id); + log.info("Deleted TechnologyInterface with ID: {}", id); return ResponseEntity.noContent().build(); } @@ -177,13 +234,19 @@ public ResponseEntity deleteTechnologyInterface(String id) { * {@inheritDoc} */ @Override - public ResponseEntity> patchTechnologyInterface(String id, TechnologyInterface technologyInterfacePatch) { + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "technologyInterfaceController.patchTechnologyInterface", description = "Time taken to patch a technology interface", histogram = true) + @Counted(value = "technologyInterfaceController.patchTechnologyInterface.count", description = "Number of patch technology interface requests") + public ResponseEntity> patchTechnologyInterface(@SpanAttribute("technologyInterface.id") String id, @SpanAttribute TechnologyInterface technologyInterfacePatch) { + log.debug("Patching TechnologyInterface with ID: {}", id); if (!technologyInterfaceService.getTechnologyInterface(id).isPresent()) { + log.warn("TechnologyInterface not found with ID: {}", id); return ResponseEntity.notFound().build(); } TechnologyInterface patchedTechnologyInterface = technologyInterfaceService.patchTechnologyInterface(id, technologyInterfacePatch); EntityModel entityModel = technologyInterfaceModelAssembler.toModel(patchedTechnologyInterface); + log.info("Patched TechnologyInterface with ID: {}", id); return ResponseEntity.ok(entityModel); } } diff --git a/src/main/java/edu/kit/datamanager/idoris/users/services/UserService.java b/src/main/java/edu/kit/datamanager/idoris/users/services/UserService.java index d483e51..432c8bf 100644 --- a/src/main/java/edu/kit/datamanager/idoris/users/services/UserService.java +++ b/src/main/java/edu/kit/datamanager/idoris/users/services/UserService.java @@ -19,6 +19,7 @@ import edu.kit.datamanager.idoris.users.entities.ORCiDUser; import edu.kit.datamanager.idoris.users.entities.TextUser; import edu.kit.datamanager.idoris.users.entities.User; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; import java.net.URL; import java.util.List; @@ -43,7 +44,7 @@ public interface UserService { * @param id The PID or internal ID of the user * @return Optional containing the user if found, empty otherwise */ - Optional findUserById(String id); + Optional findUserById(@SpanAttribute("user.id") String id); /** * Find all TextUsers in the system. @@ -58,7 +59,7 @@ public interface UserService { * @param email The email of the TextUser * @return Optional containing the TextUser if found, empty otherwise */ - Optional findTextUserByEmail(String email); + Optional findTextUserByEmail(@SpanAttribute("user.email") String email); /** * Find all ORCiDUsers in the system. @@ -73,7 +74,7 @@ public interface UserService { * @param orcid The ORCID of the user * @return Optional containing the ORCiDUser if found, empty otherwise */ - Optional findORCiDUserByORCiD(URL orcid); + Optional findORCiDUserByORCiD(@SpanAttribute("user.orcid") URL orcid); /** * Create a new TextUser. @@ -81,7 +82,7 @@ public interface UserService { * @param user The TextUser to create * @return The created TextUser */ - TextUser createTextUser(TextUser user); + TextUser createTextUser(@SpanAttribute TextUser user); /** * Create a new ORCiDUser. @@ -89,7 +90,7 @@ public interface UserService { * @param user The ORCiDUser to create * @return The created ORCiDUser */ - ORCiDUser createORCiDUser(ORCiDUser user); + ORCiDUser createORCiDUser(@SpanAttribute ORCiDUser user); /** * Update an existing user. @@ -99,7 +100,7 @@ public interface UserService { * @return The updated user * @throws IllegalArgumentException if the user is not found */ - User updateUser(String id, User user); + User updateUser(@SpanAttribute("user.id") String id, @SpanAttribute User user); /** * Delete a user by their PID or internal ID. @@ -107,5 +108,5 @@ public interface UserService { * @param id The PID or internal ID of the user to delete * @throws IllegalArgumentException if the user is not found */ - void deleteUser(String id); + void deleteUser(@SpanAttribute("user.id") String id); } diff --git a/src/main/java/edu/kit/datamanager/idoris/users/web/v1/UserController.java b/src/main/java/edu/kit/datamanager/idoris/users/web/v1/UserController.java index a1d8dde..648e0f6 100644 --- a/src/main/java/edu/kit/datamanager/idoris/users/web/v1/UserController.java +++ b/src/main/java/edu/kit/datamanager/idoris/users/web/v1/UserController.java @@ -22,6 +22,13 @@ import edu.kit.datamanager.idoris.users.services.UserService; import edu.kit.datamanager.idoris.users.web.api.IUserApi; import edu.kit.datamanager.idoris.users.web.hateoas.UserModelAssembler; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.annotation.Timed; +import io.micrometer.observation.annotation.Observed; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.annotations.WithSpan; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.hateoas.CollectionModel; import org.springframework.hateoas.EntityModel; @@ -45,6 +52,8 @@ */ @RestController @RequestMapping("/v1/users") +@Slf4j +@Observed(contextualName = "userController") public class UserController implements IUserApi { private final UserService userService; @@ -57,7 +66,11 @@ public UserController(UserService userService, UserModelAssembler userModelAssem } @Override + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "userController.getAllUsers", description = "Time taken to get all users", histogram = true) + @Counted(value = "userController.getAllUsers.count", description = "Number of get all users requests") public ResponseEntity>> getAllUsers() { + log.debug("Getting all users"); List> users = userService.findAllUsers().stream() .map(userModelAssembler::toModel) .collect(Collectors.toList()); @@ -67,19 +80,34 @@ public ResponseEntity>> getAllUsers() { linkTo(methodOn(UserController.class).getAllUsers()).withSelfRel() ); + log.info("Retrieved {} users", users.size()); return ResponseEntity.ok(collectionModel); } @Override - public ResponseEntity> getUserById(String id) { + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "userController.getUserById", description = "Time taken to get a user by ID", histogram = true) + @Counted(value = "userController.getUserById.count", description = "Number of get user by ID requests") + public ResponseEntity> getUserById(@SpanAttribute("user.id") String id) { + log.debug("Getting user by ID: {}", id); return userService.findUserById(id) - .map(userModelAssembler::toModel) + .map(user -> { + log.info("Found user with ID: {}", id); + return userModelAssembler.toModel(user); + }) .map(ResponseEntity::ok) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found")); + .orElseThrow(() -> { + log.warn("User not found with ID: {}", id); + return new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found"); + }); } @Override + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "userController.getAllTextUsers", description = "Time taken to get all text users", histogram = true) + @Counted(value = "userController.getAllTextUsers.count", description = "Number of get all text users requests") public ResponseEntity>> getAllTextUsers() { + log.debug("Getting all text users"); List> users = userService.findAllTextUsers().stream() .map(user -> EntityModel.of(user, linkTo(methodOn(UserController.class).getTextUserByEmail(user.getEmail())).withSelfRel(), @@ -92,22 +120,37 @@ public ResponseEntity>> getAllTextUsers() linkTo(methodOn(UserController.class).getAllUsers()).withRel("users") ); + log.info("Retrieved {} text users", users.size()); return ResponseEntity.ok(collectionModel); } @Override - public ResponseEntity> getTextUserByEmail(String email) { + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "userController.getTextUserByEmail", description = "Time taken to get a text user by email", histogram = true) + @Counted(value = "userController.getTextUserByEmail.count", description = "Number of get text user by email requests") + public ResponseEntity> getTextUserByEmail(@SpanAttribute("user.email") String email) { + log.debug("Getting text user by email: {}", email); return userService.findTextUserByEmail(email) - .map(user -> EntityModel.of(user, - linkTo(methodOn(UserController.class).getTextUserByEmail(email)).withSelfRel(), - linkTo(methodOn(UserController.class).getAllTextUsers()).withRel("textUsers"), - linkTo(methodOn(UserController.class).getAllUsers()).withRel("users"))) + .map(user -> { + log.info("Found text user with email: {}", email); + return EntityModel.of(user, + linkTo(methodOn(UserController.class).getTextUserByEmail(email)).withSelfRel(), + linkTo(methodOn(UserController.class).getAllTextUsers()).withRel("textUsers"), + linkTo(methodOn(UserController.class).getAllUsers()).withRel("users")); + }) .map(ResponseEntity::ok) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Text user not found")); + .orElseThrow(() -> { + log.warn("Text user not found with email: {}", email); + return new ResponseStatusException(HttpStatus.NOT_FOUND, "Text user not found"); + }); } @Override + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "userController.getAllORCiDUsers", description = "Time taken to get all ORCID users", histogram = true) + @Counted(value = "userController.getAllORCiDUsers.count", description = "Number of get all ORCID users requests") public ResponseEntity>> getAllORCiDUsers() { + log.debug("Getting all ORCID users"); List> users = userService.findAllORCiDUsers().stream() .map(user -> { // Extract ORCID identifier from the URL @@ -124,39 +167,60 @@ public ResponseEntity>> getAllORCiDUsers( linkTo(methodOn(UserController.class).getAllUsers()).withRel("users") ); + log.info("Retrieved {} ORCID users", users.size()); return ResponseEntity.ok(collectionModel); } @Override - public ResponseEntity> getORCiDUserByORCiD(String orcidStr) { + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "userController.getORCiDUserByORCiD", description = "Time taken to get an ORCID user by ORCID", histogram = true) + @Counted(value = "userController.getORCiDUserByORCiD.count", description = "Number of get ORCID user by ORCID requests") + public ResponseEntity> getORCiDUserByORCiD(@SpanAttribute("user.orcid") String orcidStr) { + log.debug("Getting ORCID user by ORCID: {}", orcidStr); try { // Convert ORCID string to URL URL orcid = URI.create("https://orcid.org/" + orcidStr).toURL(); return userService.findORCiDUserByORCiD(orcid) - .map(user -> EntityModel.of(user, - linkTo(methodOn(UserController.class).getORCiDUserByORCiD(orcidStr)).withSelfRel(), - linkTo(methodOn(UserController.class).getAllORCiDUsers()).withRel("orcidUsers"), - linkTo(methodOn(UserController.class).getAllUsers()).withRel("users"))) + .map(user -> { + log.info("Found ORCID user with ORCID: {}", orcidStr); + return EntityModel.of(user, + linkTo(methodOn(UserController.class).getORCiDUserByORCiD(orcidStr)).withSelfRel(), + linkTo(methodOn(UserController.class).getAllORCiDUsers()).withRel("orcidUsers"), + linkTo(methodOn(UserController.class).getAllUsers()).withRel("users")); + }) .map(ResponseEntity::ok) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "ORCID user not found")); + .orElseThrow(() -> { + log.warn("ORCID user not found with ORCID: {}", orcidStr); + return new ResponseStatusException(HttpStatus.NOT_FOUND, "ORCID user not found"); + }); } catch (java.net.MalformedURLException e) { + log.error("Invalid ORCID format: {}", orcidStr, e); // Only catch MalformedURLException to return BAD_REQUEST throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid ORCID format: " + orcidStr, e); } } @Override - public ResponseEntity> createTextUser(TextUser user) { + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "userController.createTextUser", description = "Time taken to create a text user", histogram = true) + @Counted(value = "userController.createTextUser.count", description = "Number of create text user requests") + public ResponseEntity> createTextUser(@SpanAttribute TextUser user) { + log.debug("Creating text user: {}", user.getEmail()); TextUser createdUser = userService.createTextUser(user); EntityModel entityModel = EntityModel.of(createdUser, linkTo(methodOn(UserController.class).getTextUserByEmail(createdUser.getEmail())).withSelfRel(), linkTo(methodOn(UserController.class).getAllTextUsers()).withRel("textUsers"), linkTo(methodOn(UserController.class).getAllUsers()).withRel("users")); + log.info("Created text user with email: {}", createdUser.getEmail()); return ResponseEntity.status(HttpStatus.CREATED).body(entityModel); } @Override - public ResponseEntity> createORCiDUser(ORCiDUser user) { + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "userController.createORCiDUser", description = "Time taken to create an ORCID user", histogram = true) + @Counted(value = "userController.createORCiDUser.count", description = "Number of create ORCID user requests") + public ResponseEntity> createORCiDUser(@SpanAttribute ORCiDUser user) { + log.debug("Creating ORCID user: {}", user.getOrcid()); ORCiDUser createdUser = userService.createORCiDUser(user); // Extract ORCID identifier from the URL String orcidStr = createdUser.getOrcid().toString().replace("https://orcid.org/", ""); @@ -164,26 +228,39 @@ public ResponseEntity> createORCiDUser(ORCiDUser user) { linkTo(methodOn(UserController.class).getORCiDUserByORCiD(orcidStr)).withSelfRel(), linkTo(methodOn(UserController.class).getAllORCiDUsers()).withRel("orcidUsers"), linkTo(methodOn(UserController.class).getAllUsers()).withRel("users")); + log.info("Created ORCID user with ORCID: {}", createdUser.getOrcid()); return ResponseEntity.status(HttpStatus.CREATED).body(entityModel); } @Override - public ResponseEntity> updateUser(String id, User user) { + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "userController.updateUser", description = "Time taken to update a user", histogram = true) + @Counted(value = "userController.updateUser.count", description = "Number of update user requests") + public ResponseEntity> updateUser(@SpanAttribute("user.id") String id, @SpanAttribute User user) { + log.debug("Updating user with ID: {}", id); try { User updatedUser = userService.updateUser(id, user); EntityModel entityModel = userModelAssembler.toModel(updatedUser); + log.info("Updated user with ID: {}", id); return ResponseEntity.ok(entityModel); } catch (IllegalArgumentException e) { + log.error("Failed to update user with ID: {}", id, e); throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); } } @Override - public ResponseEntity deleteUser(String id) { + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "userController.deleteUser", description = "Time taken to delete a user", histogram = true) + @Counted(value = "userController.deleteUser.count", description = "Number of delete user requests") + public ResponseEntity deleteUser(@SpanAttribute("user.id") String id) { + log.debug("Deleting user with ID: {}", id); try { userService.deleteUser(id); + log.info("Deleted user with ID: {}", id); return ResponseEntity.noContent().build(); } catch (IllegalArgumentException e) { + log.error("Failed to delete user with ID: {}", id, e); throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index bca2312..b419801 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -20,68 +20,12 @@ spring.profiles.active=default logging.level.root=INFO logging.level.org.springframework=DEBUG logging.level.edu.kit.datamanager=DEBUG -#logging.config=classpath:logback-spring.xml # Database Configuration spring.neo4j.uri=bolt://localhost:7687 spring.neo4j.authentication.username=neo4j spring.neo4j.authentication.password=superSecret -################################# -######## Observability ########## -################################# -## Generic OpenTelemetry Configuration -#management.endpoints.web.exposure.include=* -#management.endpoint.health.show-details=always -#management.endpoint.prometheus.access=unrestricted -#management.metrics.distribution.sla.http.server.requests=100ms,500ms,1000ms -#management.metrics.export.defaults.step=15s -#management.metrics.distribution.percentiles-histogram.http.server.requests=true -#management.metrics.tags.service_name=${spring.application.name} -#management.metrics.tags.environment=${spring.profiles.active} -#management.prometheus.metrics.export.enabled=false -#otel.java.global-autoconfigure.enabled=true -#otel.instrumentation.micrometer.enabled=true -#otel.service.name=${spring.application.name} -# -## OpenTelemetry Metrics Configuration -#otel.metrics.exporter=otlp -#otel.exporter.otlp.endpoint=http://localhost:4318 -#otel.exporter.otlp.protocol=http/protobuf -#management.otlp.metrics.export.enabled=true -#management.otlp.metrics.export.step=2s -#management.otlp.metrics.export.url=http://localhost:4318/v1/metrics -# -## OpenTelemetry Logging Configuration -#management.otlp.logging.export.enabled=true -#management.otlp.logging.endpoint=http://localhost:4318/v1/logs -##otel.instrumentation.logback-appender.experimental.capture-mdc-attributes=* -#otel.instrumentation.logback-appender.experimental-log-attributes=true -#otel.instrumentation.logback-appender.experimental.capture-code-attributes=true -#otel.instrumentation.logback-appender.experimental.capture-marker-attribute=true -#otel.instrumentation.logback-appender.experimental.capture-mdc-attributes=trace_id,span_id -##logging.pattern.level=%5p [${spring.application.name:},%X{trace_id:-},%X{span_id:-}] -#logging.context.enabled=true -# -## Tracing Configuration -#management.tracing.sampling.probability=1.0 -#management.otlp.tracing.endpoint=http://localhost:4318/v1/traces -#management.httpexchanges.recording.enabled=true -#management.tracing.baggage.correlation.enabled=true -#management.tracing.opentelemetry.export.include-unsampled=true -#management.observations.annotations.enabled=true -#otel.instrumentation.http.client.emit-experimental-telemetry=true -#otel.instrumentation.runtime-telemetry-java17.enabled=true -#otel.instrumentation.spring-webmvc.enabled=true -#otel.instrumentation.annotations.enabled=true -#otel.instrumentation.http.client.capture-request-headers=true -#otel.instrumentation.http.client.capture-response-headers=true -#otel.instrumentation.http.client.experimental.redact-query-parameters=false -#otel.instrumentation.jdbc.experimental.transaction.enabled=true -#otel.resource.attributes.exclude=process.command_args,process.command_line -#otel.propagators=tracecontext,baggage -#otel.traces.sampler=parentbased_traceidratio -#otel.traces.sampler.arg=1 ################################ ####### Observability ########## ################################ @@ -132,7 +76,6 @@ otel.instrumentation.http.client.capture-request-headers=true otel.instrumentation.http.client.capture-response-headers=true otel.instrumentation.http.client.experimental.redact-query-parameters=false otel.instrumentation.jdbc.experimental.transaction.enabled=true -otel.resource.attributes.exclude=process.command_args,process.command_line otel.propagators=tracecontext,baggage otel.traces.sampler=parentbased_traceidratio otel.traces.sampler.arg=1 diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml deleted file mode 100644 index e1c6f28..0000000 --- a/src/main/resources/logback-spring.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} [trace_id=%X{trace_id:-}, span_id=%X{span_id:-}] - - %msg%n - - - - - true - true - true - true - true - * - - - - - - - \ No newline at end of file From 45970872d8ac5b7a4ad217c17cf456f62afce92f Mon Sep 17 00:00:00 2001 From: Maximilian Inckmann Date: Fri, 15 Aug 2025 17:34:59 +0200 Subject: [PATCH 11/19] bugfix Signed-off-by: Maximilian Inckmann --- .../idoris/rules/logic/RuleService.java | 205 +++++------------- 1 file changed, 57 insertions(+), 148 deletions(-) diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleService.java b/src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleService.java index d75fd1c..63fc4f8 100644 --- a/src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleService.java +++ b/src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleService.java @@ -20,8 +20,6 @@ import io.micrometer.core.annotation.Timed; import io.micrometer.observation.annotation.Observed; import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.context.Context; -import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.annotations.SpanAttribute; import io.opentelemetry.instrumentation.annotations.WithSpan; import jakarta.annotation.PostConstruct; @@ -30,11 +28,8 @@ import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.stereotype.Component; -import java.lang.reflect.Array; import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -52,7 +47,6 @@ public class RuleService { * in the precomputed dependency graph, avoiding the instantiation of unused rules. */ private final ListableBeanFactory beanFactory; - /** * Thread-safe registry mapping fully qualified class names to rule instances. *

    @@ -219,35 +213,17 @@ private void discoverRequiredRules() { *

    * This method is the primary entry point for rule execution. It retrieves the correct * sequence of rules from the precomputed graph based on the task and element type, - * then executes them in parallel while respecting their dependency order. + * then executes them sequentially while maintaining OpenTelemetry span context. *

    * The method follows these steps: *

      *
    1. Identify the correct set of rule class names from the precomputed graph
    2. - *
    3. Execute those rules in parallel using {@link CompletableFuture}
    4. + *
    5. Execute those rules sequentially in their dependency order
    6. *
    7. Merge the results from all rules into a single result object
    8. *
    *

    * If no rules are found for the given task and element type, an empty result is returned. *

    - * Example usage: - *

    -     * {@code
    -     * // Validate an Operation
    -     * ValidationResult validationResult = ruleService.executeRules(
    -     *     RuleTask.VALIDATE,
    -     *     operation,
    -     *     ValidationResult::new
    -     * );
    -     *
    -     * // Enrich a TypeProfile
    -     * EnrichmentResult enrichmentResult = ruleService.executeRules(
    -     *     RuleTask.ENRICH,
    -     *     typeProfile,
    -     *     EnrichmentResult::new
    -     * );
    -     * }
    -     * 
    * * @param task the rule task to execute (e.g., {@link RuleTask#VALIDATE}) * @param element the domain element to process @@ -279,23 +255,23 @@ public > R executeRules( log.debug("Found {} rules for task={}, elementType={}", ruleClassNames.size(), task, elementType); - // Execute rules in parallel and merge results - return executeRulesInParallel(ruleClassNames, element, resultFactory); + // Execute rules sequentially to maintain proper dependency order and OpenTelemetry context + return executeRulesSequentially(ruleClassNames, element, resultFactory); } /** - * Executes rules in parallel while respecting their precomputed ordering. + * Executes rules sequentially in their precomputed ordering. *

    - * This method is responsible for the actual parallel execution of rules. It takes the + * This method is responsible for the sequential execution of rules. It takes the * list of rule class names in their precomputed execution order, retrieves the rule - * instances from the registry, and executes them concurrently. + * instances from the registry, and executes them one by one while maintaining + * the OpenTelemetry span context. *

    * Key aspects of this implementation: *

      *
    • Selective execution: Only rules that are found in the registry are executed
    • - *
    • Parallel processing: Each rule executes in its own {@link CompletableFuture}
    • - *
    • Coordinated completion: The method waits for all rule executions to complete
    • - *
    • Result aggregation: Results from all rules are merged into a single result
    • + *
    • Sequential processing: Each rule executes in order, maintaining span context
    • + *
    • Result accumulation: Results from all rules are merged into a single result
    • *
    *

    * If no rule instances are available for execution, an empty result is returned. This ensures @@ -309,42 +285,50 @@ public > R executeRules( * @return the merged result of all executed rules * @throws RuntimeException if rule execution fails critically */ + @SuppressWarnings("unchecked") @WithSpan(kind = SpanKind.INTERNAL) - private > R executeRulesInParallel( + private > R executeRulesSequentially( @SpanAttribute List ruleClassNames, T element, Supplier resultFactory ) { - // Create result futures for rule execution, but only for rules that are actually available in the registry - // This pipeline: 1) Gets rule instances from registry, 2) Filters out missing rules, 3) Executes each rule asynchronously - List> resultFutures = ruleClassNames.stream() - .map(ruleRegistry::get) // Look up each rule by class name - .filter(Objects::nonNull) // Skip rules that weren't found (null) - .map(rule -> executeRule(rule, element, resultFactory)) // Execute each rule asynchronously - .collect(Collectors.toList()); // Collect all future results - - if (resultFutures.isEmpty()) { - log.debug("No available rule instances found for execution"); - return resultFactory.get(); - } + R finalResult = resultFactory.get(); - // Wait for all executions to complete - try { - CompletableFuture.allOf(resultFutures.toArray(CompletableFuture[]::new)).join(); - } catch (Exception e) { - log.error("Error during rule execution", e); - throw new RuntimeException("Rule execution failed", e); + for (String ruleClassName : ruleClassNames) { + IRule rule = getRuleFromRegistry(ruleClassName); + if (rule != null) { + try { + R ruleResult = executeRule(rule, element, resultFactory); + finalResult = finalResult.merge(ruleResult); + } catch (Exception e) { + log.error("Rule {} execution failed: {}", rule.getClass().getSimpleName(), e.getMessage(), e); + throw new RuntimeException("Rule execution failed: " + e.getMessage(), e); + } + } } - // Merge results - return mergeResults(resultFutures, resultFactory); + return finalResult; + } + + /** + * Retrieves a rule from the registry by its class name. + * Logs a debug message if the rule is not found. + * + * @param ruleClassName the fully qualified class name of the rule + * @return the rule instance, or null if not found + */ + private IRule getRuleFromRegistry(String ruleClassName) { + IRule rule = ruleRegistry.get(ruleClassName); + if (rule == null) { + log.debug("Skipping rule not found in registry: {}", ruleClassName); + } + return rule; } /** - * Executes a single rule asynchronously and returns its result future. + * Executes a single rule synchronously and returns its result. *

    - * This method wraps the execution of an individual rule in a {@link CompletableFuture} to - * enable asynchronous processing. It handles the lifecycle of rule execution including: + * This method handles the lifecycle of rule execution including: *