diff --git a/.gitignore b/.gitignore index d1a70ec..3b08a63 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ db/ neo4j-data/ neo4j-data-new/ +observability/ +logs/ # Ignore Gradle project-specific cache directory nbproject @@ -233,7 +235,7 @@ Temporary Items ### Gradle ### .gradle **/build/ -!src/**/build/ +!idoris/**/build/ # Ignore Gradle GUI configuration gradle-app.setting @@ -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/ +*/build diff --git a/README.md b/README.md index b9b46c6..cc35271 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. @@ -49,7 +37,7 @@ For macOS, you can install it using Homebrew: ```brew install openjdk@21```. For Fedora, you can install it using DNF: ```sudo dnf install java-21```. -Configure the [application.properties](src/main/resources/application.properties) file to contain the Neo4j API +Configure the [application.properties](idoris/main/resources/application.properties) file to contain the Neo4j API credentials. ``` @@ -58,7 +46,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 +60,11 @@ When Neo4j is running, start IDORIS with the following command: ``` You can access the IDORIS API at http://localhost:8095/api. + +### 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/catalog-info.yaml b/catalog-info.yaml index d0a6c11..65bbed0 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -19,7 +19,7 @@ metadata: spec: lifecycle: experimental owner: user:maximiliani - type: service + type: logic domain: fairdo --- @@ -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 @@ -89,7 +80,7 @@ metadata: backstage.io/techdocs-ref: dir:. backstage.io/managed-by-origin-location: url:http://github.com/maximiliani/idoris/blob/master/catalog-info.yaml spec: - type: service + type: logic lifecycle: experimental owner: user:maximiliani system: idoris diff --git a/docker-compose-observability.yml b/docker-compose-observability.yml new file mode 100644 index 0000000..4814e99 --- /dev/null +++ b/docker-compose-observability.yml @@ -0,0 +1,142 @@ +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 + + # 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 + 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/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 + + # 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 + 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 + - mimir + - loki + - tempo + - pyroscope + networks: + - observability-network + +volumes: + prometheus_data: + mimir_data: + loki_data: + tempo_data: + pyroscope_data: + grafana_data: + +networks: + observability-network: + driver: bridge \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1819666..bb0ab07 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2025 Karlsruhe Institute of Technology +# Copyright (c) 2025-2026 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. @@ -15,7 +15,7 @@ # distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/build.gradle b/idoris/build.gradle similarity index 50% rename from build.gradle rename to idoris/build.gradle index 8046799..6b98817 100644 --- a/build.gradle +++ b/idoris/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Karlsruhe Institute of Technology + * Copyright (c) 2025-2026 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. @@ -18,33 +18,30 @@ import java.text.SimpleDateFormat plugins { id "java" - id "org.springframework.boot" version "3.5.0" // 3.5.0-M* + id "org.springframework.boot" version "4.0.2" id "io.spring.dependency-management" version "1.1.7" - id "io.freefair.lombok" version "8.13.1" - id "io.freefair.maven-publish-java" version "8.13.1" - id "org.owasp.dependencycheck" version "12.1.1" - id "org.asciidoctor.jvm.convert" version "4.0.4" - id "net.ltgt.errorprone" version "4.2.0" + id "io.freefair.lombok" version "9.2.0" + id "io.freefair.maven-publish-java" version "9.2.0" + id "org.owasp.dependencycheck" version "12.2.0" + id "org.asciidoctor.jvm.convert" version "4.0.5" + id "net.ltgt.errorprone" version "4.4.0" id "net.researchgate.release" version "3.1.0" - id "com.gorylenko.gradle-git-properties" version "2.5.0" + id "com.gorylenko.gradle-git-properties" version "2.5.4" id "jacoco" - id "com.github.ben-manes.versions" version "0.52.0" + id "com.github.ben-manes.versions" version "0.53.0" } description = 'IDORIS - An Integrated Data Type and Operations Registry with Inheritance System' group = 'edu.kit.datamanager' version = '0.0.2-SNAPSHOT' -// Update source/target compatibility syntax java { toolchain { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(25) } } configurations { -// annotationProcessorPath - compileOnly { extendsFrom annotationProcessor } @@ -53,59 +50,75 @@ configurations { repositories { mavenLocal() mavenCentral() - maven { - url = uri("https://repo.spring.io/milestone") - } } ext { - springBootVersion = "3.5.0" - springDocVersion = "2.8.8" - errorproneVersion = "2.38.0" - errorproneJavacVersion = "9+181-r4173-1" // keep until a newer tag is published - httpClientVersion = "5.5" - javersVersion = "7.3.7" // unchanged (latest) + springDocVersion = "3.0.1" + errorproneVersion = "2.46.0" + javersVersion = "7.3.7" + openTelemetryInstrumentationVersion = "2.24.0" set("snippetsDir", file("build/generated-snippets")) + set("springModulithVersion", "2.0.2") + gitProperties.dotGitDirectory = file("../.git") } dependencies { - /* Spring BOM – drives every spring-boot starter below */ - implementation platform("org.springframework.boot:spring-boot-dependencies:${springBootVersion}") + /** + * Service base from KIT-Data Manager + */ +// implementation("edu.kit.datamanager:service-base:1.3.6") +// implementation("edu.kit.datamanager:repo-core:1.2.6") /* Rules API */ - implementation project(':rules-api') + implementation project(":rules-api") + annotationProcessor project(":rules-processor") /* Spring Boot starters (version comes from the BOM) */ - implementation "org.springframework.boot:spring-boot-starter-web" + implementation 'org.springframework.boot:spring-boot-starter-restclient' + implementation "org.springframework.boot:spring-boot-starter-webmvc" + implementation 'org.springframework.boot:spring-boot-starter-aspectj' + implementation 'org.springframework.boot:spring-boot-starter-amqp' 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.boot:spring-boot-starter-security" + implementation "org.springframework.modulith:spring-modulith-starter-core" + implementation "org.springframework.modulith:spring-modulith-starter-neo4j" + implementation "org.springframework.modulith:spring-modulith-events-api" + implementation "org.springframework.modulith:spring-modulith-starter-insight" + runtimeOnly "org.springframework.modulith:spring-modulith-runtime" + runtimeOnly "org.springframework.modulith:spring-modulith-observability" + runtimeOnly "org.springframework.modulith:spring-modulith-actuator" + /* OpenAPI */ implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${springDocVersion}" implementation "org.springdoc:springdoc-openapi-starter-webmvc-api:${springDocVersion}" implementation "org.springdoc:springdoc-openapi-starter-common:${springDocVersion}" - /* HTTP client */ - implementation "org.apache.httpcomponents.client5:httpclient5:${httpClientVersion}" + /* Observability */ + implementation(platform("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom:${openTelemetryInstrumentationVersion}")) + implementation 'org.springframework.boot:spring-boot-micrometer-tracing' + implementation 'org.springframework.boot:spring-boot-starter-micrometer-metrics' + implementation 'org.springframework.boot:spring-boot-starter-opentelemetry' + implementation "org.springframework.boot:spring-boot-starter-actuator" + implementation "io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter" + implementation "io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations" +// implementation "io.opentelemetry.contrib:opentelemetry-sampler" + implementation "io.micrometer:micrometer-tracing-bridge-otel" + implementation "io.opentelemetry:opentelemetry-exporter-otlp" /* Development helpers */ - implementation "org.springframework.boot:spring-boot-configuration-processor" + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" developmentOnly "org.springframework.boot:spring-boot-devtools" +// developmentOnly 'org.springframework.boot:spring-boot-docker-compose' /* Lombok */ - compileOnly "org.projectlombok:lombok:1.18.38" - annotationProcessor "org.projectlombok:lombok:1.18.38" - - /* JavaX Annotations */ - implementation 'javax.annotation:javax.annotation-api:1.3.2' + compileOnly "org.projectlombok:lombok" + annotationProcessor "org.projectlombok:lombok" - // Add the processor module - annotationProcessor project(':rules-processor') + /* Jakarta Annotations */ + implementation "jakarta.annotation:jakarta.annotation-api" /* Error-prone */ errorprone "com.google.errorprone:error_prone_core:${errorproneVersion}" @@ -113,12 +126,29 @@ 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" testImplementation "org.springframework.security:spring-security-test" - testImplementation "org.junit.jupiter:junit-jupiter:5.13.0" + testImplementation "org.springframework.modulith:spring-modulith-starter-test" + testImplementation 'org.springframework.boot:spring-boot-micrometer-tracing-test' + testImplementation 'org.springframework.boot:spring-boot-starter-actuator-test' + testImplementation 'org.springframework.boot:spring-boot-starter-data-neo4j-test' + testImplementation 'org.springframework.boot:spring-boot-starter-hateoas-test' + testImplementation 'org.springframework.boot:spring-boot-starter-opentelemetry-test' + testImplementation 'org.springframework.boot:spring-boot-starter-restclient-test' + testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' + testImplementation 'org.springframework.boot:spring-boot-starter-amqp-test' + testImplementation 'org.springframework.boot:spring-boot-starter-aspectj-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:${openTelemetryInstrumentationVersion}" + } } -// Modify JavaCompile tasks configuration tasks.withType(JavaCompile).configureEach { if (name.toLowerCase().contains('test')) { options.errorprone.enabled = false @@ -132,21 +162,15 @@ 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 } } -// Update test configuration tasks.named('test') { - useJUnitPlatform() outputs.dir snippetsDir + useJUnitPlatform() finalizedBy tasks.named('jacocoTestReport') jvmArgs = ['-Xmx4g'] // Replace maxHeapSize @@ -183,12 +207,10 @@ tasks.named('asciidoctor') { } } -// Update jar configuration tasks.named('jar') { enabled = false } -// Update bootJar configuration bootJar { duplicatesStrategy = DuplicatesStrategy.EXCLUDE manifest { @@ -198,5 +220,5 @@ bootJar { from("${asciidoctor.outputDir}") { into 'static/docs' } - launchScript() +// launchScript() } \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/IdorisApplication.java b/idoris/src/main/java/edu/kit/datamanager/idoris/Application.java similarity index 64% rename from src/main/java/edu/kit/datamanager/idoris/IdorisApplication.java rename to idoris/src/main/java/edu/kit/datamanager/idoris/Application.java index b442ae6..4453ccf 100644 --- a/src/main/java/edu/kit/datamanager/idoris/IdorisApplication.java +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/Application.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. @@ -16,39 +16,40 @@ package edu.kit.datamanager.idoris; -import edu.kit.datamanager.idoris.configuration.ApplicationProperties; import lombok.extern.java.Log; import org.neo4j.cypherdsl.core.renderer.Configuration; import org.neo4j.cypherdsl.core.renderer.Dialect; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.domain.EntityScan; -import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.boot.persistence.autoconfigure.EntityScan; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.data.neo4j.config.EnableNeo4jAuditing; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; +import org.springframework.modulith.Modulithic; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.transaction.annotation.EnableTransactionManagement; @SpringBootApplication +@EnableScheduling +@EntityScan @EnableNeo4jRepositories @EnableNeo4jAuditing @EnableTransactionManagement -@EntityScan("edu.kit.datamanager") -@org.springframework.context.annotation.Configuration +@EnableAspectJAutoProxy +// Scans for aspects in the current package and sub-packages (e.g. for PIISpanAttribute) +@ConfigurationPropertiesScan @Log -public class IdorisApplication { - public static void main(String[] args) { - SpringApplication.run(IdorisApplication.class, args); - System.out.println(); - System.out.println("---------------------------------"); +@Modulithic(systemName = "IDORIS") +@EnableAsync +public class Application { + static void main(String[] args) { + SpringApplication.run(Application.class, args); + System.out.println("\n---------------------------------"); System.out.println("IDORIS started successfully."); - System.out.println("---------------------------------"); - } - - @Bean - @ConfigurationProperties("repo") - public ApplicationProperties applicationProperties() { - return new ApplicationProperties(); + System.out.println("---------------------------------\n"); } @Bean diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/api/IAttributeService.java b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/api/IAttributeService.java new file mode 100644 index 0000000..1e96a31 --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/api/IAttributeService.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025-2026 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.attributes.api; + +import edu.kit.datamanager.idoris.attributes.dto.AttributeDto; + +import java.util.List; +import java.util.Optional; + +/** + * External API for Attribute operations exposed to web/controllers and other modules. + * DTO-first contract. + */ +public interface IAttributeService { + AttributeDto create(AttributeDto dto); + + AttributeDto update(String id, AttributeDto dto); + + AttributeDto patch(String id, AttributeDto dto); + + void delete(String id); + + Optional get(String id); + + List list(); + + // Relationship operations by IDs + AttributeDto setDataType(String attributeId, String dataTypeId); + + AttributeDto setOverride(String attributeId, String overrideAttributeId); + + AttributeDto removeOverride(String attributeId); +} diff --git a/src/test/java/edu/kit/datamanager/idoris/IdorisApplicationTests.java b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/api/package-info.java similarity index 66% rename from src/test/java/edu/kit/datamanager/idoris/IdorisApplicationTests.java rename to idoris/src/main/java/edu/kit/datamanager/idoris/attributes/api/package-info.java index c45c663..f7c8ddf 100644 --- a/src/test/java/edu/kit/datamanager/idoris/IdorisApplicationTests.java +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/api/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Karlsruhe Institute of Technology. + * 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. @@ -14,16 +14,5 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class IdorisApplicationTests { - - @Test - void contextLoads() { - } - -} +@org.springframework.modulith.NamedInterface("attributes.services.api") +package edu.kit.datamanager.idoris.attributes.api; \ No newline at end of file diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/dao/IAttributeDao.java b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/dao/IAttributeDao.java new file mode 100644 index 0000000..4e2a474 --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/dao/IAttributeDao.java @@ -0,0 +1,78 @@ +/* + * 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.attributes.dao; + +import edu.kit.datamanager.idoris.core.dao.IGenericRepo; +import edu.kit.datamanager.idoris.core.domain.Attribute; +import org.springframework.data.neo4j.repository.query.Query; +import org.springframework.data.repository.query.Param; + +public interface IAttributeDao extends IGenericRepo { + @Query("MATCH (n:Attribute)" + + " WHERE size([(n)-[:dataType]->() | 1]) = 1 AND NOT (n)<-[]-()" + + " WITH n" + + " MATCH (x)-[r]->()" + + " WHERE type(r) <> \"dataType\"" + + " WITH collect(DISTINCT x) as otherNodes, n" + + " WHERE NOT n IN otherNodes" + + " DETACH DELETE n") + void deleteOrphanedAttributes(); + + // ===== Relationship operations: dataType ===== + @Query(""" + MATCH (a:Attribute) + WHERE a.internalId = $attributeId OR EXISTS { MATCH (p:PersistentIdentifier {pid: $attributeId})-[:IDENTIFIES]->(a) } + OPTIONAL MATCH (a)-[r:dataType]->() + DELETE r + WITH a + MATCH (dt:DataType) + WHERE dt.internalId = $dataTypeId OR EXISTS { MATCH (pp:PersistentIdentifier {pid: $dataTypeId})-[:IDENTIFIES]->(dt) } + MERGE (a)-[:dataType]->(dt) + SET a.dataTypeId = dt.internalId + """) + void setDataType(@Param("attributeId") String attributeId, @Param("dataTypeId") String dataTypeId); + + @Query(""" + MATCH (a:Attribute) + WHERE a.internalId = $attributeId OR EXISTS { MATCH (p:PersistentIdentifier {pid: $attributeId})-[:IDENTIFIES]->(a) } + OPTIONAL MATCH (a)-[r:dataType]->() + DELETE r + SET a.dataTypeId = null + """) + void detachDataType(@Param("attributeId") String attributeId); + + // ===== Relationship operations: override ===== + @Query(""" + MATCH (a:Attribute) + WHERE a.internalId = $attributeId OR EXISTS { MATCH (p:PersistentIdentifier {pid: $attributeId})-[:IDENTIFIES]->(a) } + OPTIONAL MATCH (a)-[r:override]->() + DELETE r + WITH a + MATCH (b:Attribute) + WHERE b.internalId = $overrideId OR EXISTS { MATCH (pp:PersistentIdentifier {pid: $overrideId})-[:IDENTIFIES]->(b) } + MERGE (a)-[:override]->(b) + """) + void setOverride(@Param("attributeId") String attributeId, @Param("overrideId") String overrideId); + + @Query(""" + MATCH (a:Attribute) + WHERE a.internalId = $attributeId OR EXISTS { MATCH (p:PersistentIdentifier {pid: $attributeId})-[:IDENTIFIES]->(a) } + OPTIONAL MATCH (a)-[r:override]->() + DELETE r + """) + void detachOverride(@Param("attributeId") String attributeId); +} diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/dto/AttributeDto.java b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/dto/AttributeDto.java new file mode 100644 index 0000000..1a0f75d --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/dto/AttributeDto.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.attributes.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +/** + * Data Transfer Object for Attribute as a Java record. + * Contains only user-defined information and identifiers for relationships. + *

+ * Note: pidLink has been removed from DTOs. Use HATEOAS links to /pid/{pid} instead. + */ +@Builder +@Schema(name = "Attribute", description = "DTO representing an Attribute") +public record AttributeDto( + @Schema(description = "Internal ID (internal use only)") String internalId, + @Schema(description = "Name of the attribute") String name, + @Schema(description = "Description of the attribute") String description, + @Schema(description = "Default value") String defaultValue, + @Schema(description = "Constant value (if fixed)") String constantValue, + @Schema(description = "Lower bound cardinality") Integer lowerBoundCardinality, + @Schema(description = "Upper bound cardinality") Integer upperBoundCardinality, + @Schema(description = "ID of the DataType node") String dataTypeId, + @Schema(description = "ID of an Attribute this one overrides") String overrideId +) { + // JavaBean-style getters for compatibility with existing code/tests + public String getInternalId() { + return internalId; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public String getDefaultValue() { + return defaultValue; + } + + public String getConstantValue() { + return constantValue; + } + + public Integer getLowerBoundCardinality() { + return lowerBoundCardinality; + } + + public Integer getUpperBoundCardinality() { + return upperBoundCardinality; + } + + public String getDataTypeId() { + return dataTypeId; + } + + public String getOverrideId() { + return overrideId; + } +} diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/dto/package-info.java b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/dto/package-info.java new file mode 100644 index 0000000..5c0405e --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/dto/package-info.java @@ -0,0 +1,18 @@ +/* + * 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. + */ + +@org.springframework.modulith.NamedInterface("dto") +package edu.kit.datamanager.idoris.attributes.dto; \ No newline at end of file diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/events/AttributeCreatedEvent.java b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/events/AttributeCreatedEvent.java new file mode 100644 index 0000000..75a0b03 --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/events/AttributeCreatedEvent.java @@ -0,0 +1,38 @@ +/* + * 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.attributes.events; + +import edu.kit.datamanager.idoris.attributes.dto.AttributeDto; +import edu.kit.datamanager.idoris.core.events.AbstractDomainEvent; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.ToString; + +/** + * Module-scoped Attribute created event carrying AttributeDto payload. + */ +@Getter +@ToString +public class AttributeCreatedEvent extends AbstractDomainEvent { + @Schema(description = "ID (PID or internalId) of the Attribute") + private final String id; + private final AttributeDto payload; + + public AttributeCreatedEvent(String id, AttributeDto payload) { + this.id = id; + this.payload = payload; + } +} \ No newline at end of file diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/events/AttributeDeletedEvent.java b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/events/AttributeDeletedEvent.java new file mode 100644 index 0000000..775ffb0 --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/events/AttributeDeletedEvent.java @@ -0,0 +1,35 @@ +/* + * 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.attributes.events; + +import edu.kit.datamanager.idoris.attributes.dto.AttributeDto; +import edu.kit.datamanager.idoris.core.events.AbstractDomainEvent; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class AttributeDeletedEvent extends AbstractDomainEvent { + @Schema(description = "ID (PID or internalId) of the Attribute") + private final String id; + private final AttributeDto payload; + + public AttributeDeletedEvent(String id, AttributeDto payload) { + this.id = id; + this.payload = payload; + } +} \ No newline at end of file diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/events/AttributePatchedEvent.java b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/events/AttributePatchedEvent.java new file mode 100644 index 0000000..43c1ca1 --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/events/AttributePatchedEvent.java @@ -0,0 +1,37 @@ +/* + * 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.attributes.events; + +import edu.kit.datamanager.idoris.attributes.dto.AttributeDto; +import edu.kit.datamanager.idoris.core.events.AbstractDomainEvent; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class AttributePatchedEvent extends AbstractDomainEvent { + @Schema(description = "ID (PID or internalId) of the Attribute") + private final String id; + private final Long previousVersion; + private final AttributeDto payload; + + public AttributePatchedEvent(String id, Long previousVersion, AttributeDto payload) { + this.id = id; + this.previousVersion = previousVersion; + this.payload = payload; + } +} \ No newline at end of file diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/events/AttributeUpdatedEvent.java b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/events/AttributeUpdatedEvent.java new file mode 100644 index 0000000..3fb47b7 --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/events/AttributeUpdatedEvent.java @@ -0,0 +1,37 @@ +/* + * 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.attributes.events; + +import edu.kit.datamanager.idoris.attributes.dto.AttributeDto; +import edu.kit.datamanager.idoris.core.events.AbstractDomainEvent; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class AttributeUpdatedEvent extends AbstractDomainEvent { + @Schema(description = "ID (PID or internalId) of the Attribute") + private final String id; + private final Long previousVersion; + private final AttributeDto payload; + + public AttributeUpdatedEvent(String id, Long previousVersion, AttributeDto payload) { + this.id = id; + this.previousVersion = previousVersion; + this.payload = payload; + } +} \ No newline at end of file diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/events/package-info.java b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/events/package-info.java new file mode 100644 index 0000000..5a094f2 --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/events/package-info.java @@ -0,0 +1,18 @@ +/* + * 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. + */ + +@org.springframework.modulith.NamedInterface("events") +package edu.kit.datamanager.idoris.attributes.events; \ No newline at end of file diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/mappers/AttributeMapper.java b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/mappers/AttributeMapper.java new file mode 100644 index 0000000..3e06aad --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/mappers/AttributeMapper.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.attributes.mappers; + +import edu.kit.datamanager.idoris.attributes.dto.AttributeDto; +import edu.kit.datamanager.idoris.core.domain.Attribute; +import edu.kit.datamanager.idoris.core.domain.valueObjects.Name; +import io.micrometer.observation.annotation.Observed; + +/** + * Mapper for Attribute entity and DTO. + * - Convert entity -> DTO exposing user fields and relationship IDs + * - Convert DTO -> entity (scalar fields only) + * - Apply partial updates (patch semantics) for scalar fields + */ +@Observed(contextualName = "attributeMapper") +@org.springframework.stereotype.Component +public class AttributeMapper { + + public AttributeDto toDto(Attribute entity) { + if (entity == null) return null; + String dataTypeId = entity.getDataTypeId(); + String overrideId = entity.getOverride() != null ? entity.getOverride().getId() : null; + return AttributeDto.builder() + .internalId(entity.getId()) + .name(entity.getName().toString()) + .description(entity.getDescription().toString()) + .defaultValue(entity.getDefaultValue()) + .constantValue(entity.getConstantValue()) + .lowerBoundCardinality(entity.getLowerBoundCardinality()) + .upperBoundCardinality(entity.getUpperBoundCardinality()) + .dataTypeId(dataTypeId) + .overrideId(overrideId) + .build(); + } + + public Attribute toEntity(AttributeDto dto) { + if (dto == null) return null; + Attribute entity = new Attribute(); + entity.setName(new Name(dto.getName())); + entity.setDescription(new Name(dto.getDescription())); + entity.setDefaultValue(dto.getDefaultValue()); + entity.setConstantValue(dto.getConstantValue()); + entity.setLowerBoundCardinality(dto.getLowerBoundCardinality()); + entity.setUpperBoundCardinality(dto.getUpperBoundCardinality()); + // Relationships (dataType/override) are linked via logic/DAO using IDs. + return entity; + } + + public Attribute applyPatch(AttributeDto dto, Attribute entity) { + if (dto == null || entity == null) return entity; + if (dto.getName() != null) entity.setName(new Name(dto.getName())); + if (dto.getDescription() != null) entity.setDescription(new Name(dto.getDescription())); + if (dto.getDefaultValue() != null) entity.setDefaultValue(dto.getDefaultValue()); + if (dto.getConstantValue() != null) entity.setConstantValue(dto.getConstantValue()); + if (dto.getLowerBoundCardinality() != null) entity.setLowerBoundCardinality(dto.getLowerBoundCardinality()); + if (dto.getUpperBoundCardinality() != null) entity.setUpperBoundCardinality(dto.getUpperBoundCardinality()); + // Relationship IDs handled elsewhere + return entity; + } +} diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/package-info.java b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/package-info.java new file mode 100644 index 0000000..563bbf8 --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/package-info.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2025-2026 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. + */ + +/** + * 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 Attributes module depends on the core module for base abstractions and interfaces.

+ */ +@org.springframework.modulith.ApplicationModule( + displayName = "IDORIS Attributes", + allowedDependencies = {"core", "datatypes :: api"} +) +package edu.kit.datamanager.idoris.attributes; \ No newline at end of file diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/rules/AttributeInheritanceValidator.java b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/rules/AttributeInheritanceValidator.java new file mode 100644 index 0000000..21125fb --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/rules/AttributeInheritanceValidator.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2026 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.attributes.rules; + +import edu.kit.datamanager.idoris.core.domain.Attribute; +import edu.kit.datamanager.idoris.core.domain.ValidationResult; +import edu.kit.datamanager.idoris.core.domain.ValidationVisitor; +import edu.kit.datamanager.idoris.datatypes.api.IDataTypeService; +import edu.kit.datamanager.idoris.rules.logic.Rule; +import edu.kit.datamanager.idoris.rules.logic.RuleTask; +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.Map; + +import static edu.kit.datamanager.idoris.rules.logic.OutputMessage.MessageSeverity.ERROR; + +@Slf4j +@Observed +@Rule( + appliesTo = Attribute.class, + name = "AttributeInheritanceValidator", + description = "Validates that attributes correctly inherit properties from their parent definitions", + tasks = RuleTask.VALIDATE, + dependsOn = AttributeSyntaxValidator.class +) +public class AttributeInheritanceValidator extends ValidationVisitor { + + private final IDataTypeService dataTypeService; + + public AttributeInheritanceValidator(IDataTypeService dataTypeService) { + super(); + this.dataTypeService = dataTypeService; + } + + /** + * Validates inheritance relationships for Attribute entities + * + * @param attribute The attribute to validate + * @param args Additional arguments (not used in this implementation) + * @return ValidationResult containing any validation errors + */ + @WithSpan(kind = SpanKind.INTERNAL) + public ValidationResult visit(Attribute attribute, Object... args) { + ValidationResult result = new ValidationResult(); + + if (attribute.getOverride() != null && attribute.getOverride().getDataTypeId() != null) { + Attribute override = attribute.getOverride(); + + if (dataTypeService.inheritsFrom(attribute.getDataTypeId(), override.getDataTypeId()) == false) { + final Map element = Map.of( + "attribute", attribute, + "attributeDataTypeId", attribute.getDataTypeId(), + "overriddenAttribute", override, + "overriddenDataTypeId", override.getDataTypeId() + ); + result.addMessage( + "The data type of an overridden attribute MUST inherit from the data type of the attribute that is being overwritten.", + element, + rule, + ERROR); + } + + if (attribute.getLowerBoundCardinality() < override.getLowerBoundCardinality()) + result.addMessage("The lower bound cardinality of an attribute MUST be more or equally restrictive than the lower bound cardinality of the attribute that was overwritten. Overriding a more restrictive attribute as a less restrictive attribute is NOT possible.", + attribute, rule, ERROR); + + if (attribute.getUpperBoundCardinality() == null && override.getUpperBoundCardinality() != null) { + result.addMessage("The upper bound cardinality of an attribute MUST be defined if the attribute that was overwritten has an upper bound cardinality defined.", + attribute, rule, ERROR); + } else if (override.getUpperBoundCardinality() != null && attribute.getUpperBoundCardinality() > override.getUpperBoundCardinality()) + result.addMessage("The upper bound cardinality of an attribute MUST be more or equally restrictive than the upper bound cardinality of the attribute that was overwritten. Overriding a less restrictive attribute as a more restrictive attribute is NOT possible.", + attribute, rule, ERROR); + } + + return result; + } +} diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/rules/AttributeSyntaxValidator.java b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/rules/AttributeSyntaxValidator.java new file mode 100644 index 0000000..64c84a5 --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/rules/AttributeSyntaxValidator.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2024-2026 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.attributes.rules; + +import edu.kit.datamanager.idoris.core.domain.Attribute; +import edu.kit.datamanager.idoris.core.domain.ValidationResult; +import edu.kit.datamanager.idoris.core.domain.ValidationVisitor; +import edu.kit.datamanager.idoris.rules.logic.Rule; +import edu.kit.datamanager.idoris.rules.logic.RuleTask; +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; +import static edu.kit.datamanager.idoris.rules.logic.OutputMessage.MessageSeverity.WARNING; + +@Slf4j +@Observed +@Rule( + appliesTo = Attribute.class, + name = "AttributeSyntaxValidator", + description = "Validates that attributes follow required syntax rules and constraints", + tasks = RuleTask.VALIDATE, + executeBefore = AttributeInheritanceValidator.class +) +public class AttributeSyntaxValidator extends ValidationVisitor { + /** + * Validates syntax constraints for Attribute entities + * + * @param attribute The attribute to validate + * @param args Additional arguments (not used in this implementation) + * @return ValidationResult containing any validation errors + */ + @WithSpan(kind = SpanKind.INTERNAL) + public ValidationResult visit(Attribute attribute, Object... args) { + ValidationResult result = new ValidationResult(); + + if (attribute.getName() == null) { + result.addMessage("For better human readability and understanding, you MUST provide a name for the attribute.", + attribute, rule, ERROR); + } + + if (attribute.getDescription() == null) { + result.addMessage("For better human readability and understanding, you SHOULD provide a description for the attribute.", + attribute, rule, WARNING); + } + + if (attribute.getDataTypeId() == null || attribute.getDataTypeId().isBlank()) { + result.addMessage("You MUST provide a data type for the attribute.", attribute, rule, ERROR); + } + + if (attribute.getLowerBoundCardinality() == null) { + result.addMessage("You MUST provide a lower bound cardinality for the attribute.", attribute, rule, ERROR); + } else if (attribute.getLowerBoundCardinality() < 0) { + result.addMessage("The lower bound cardinality of an attribute MUST be a positive number or zero.", + attribute, rule, ERROR); + } + + if (attribute.getUpperBoundCardinality() != null && attribute.getUpperBoundCardinality() < 0) { + result.addMessage("The upper bound cardinality of an attribute MUST be a positive number or zero.", + attribute, rule, ERROR); + } else if (attribute.getUpperBoundCardinality() != null && attribute.getLowerBoundCardinality() != null && + attribute.getUpperBoundCardinality() < attribute.getLowerBoundCardinality()) { + result.addMessage("The upper bound cardinality of an attribute MUST be greater than or equal to the lower bound cardinality.", + attribute, rule, ERROR); + } else if (attribute.getUpperBoundCardinality() == null) { + result.addMessage("This attribute represents an unlimited number of values. This is not recommended, as it may lead to unexpected results in the future. Please consider setting an upper bound cardinality.", + attribute, rule, WARNING); + } + + return result; + } +} diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/services/AttributeDtoService.java b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/services/AttributeDtoService.java new file mode 100644 index 0000000..8ba3a10 --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/services/AttributeDtoService.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2025-2026 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.attributes.services; + +import edu.kit.datamanager.idoris.attributes.api.IAttributeService; +import edu.kit.datamanager.idoris.attributes.dao.IAttributeDao; +import edu.kit.datamanager.idoris.attributes.dto.AttributeDto; +import edu.kit.datamanager.idoris.attributes.mappers.AttributeMapper; +import edu.kit.datamanager.idoris.core.domain.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; + +import java.util.List; +import java.util.Optional; + +/** + * DTO-first logic for Attributes, implementing exported external and internal APIs. + * Keeps entity-based controller untouched for now; controller migration follows later. + */ +@Service +@Slf4j +@Observed(contextualName = "attributeDtoService") +public class AttributeDtoService implements IAttributeService { + + private final IAttributeDao attributeDao; + private final EventPublisherService eventPublisher; + private final AttributeMapper mapper; + + public AttributeDtoService(IAttributeDao attributeDao, EventPublisherService eventPublisher, AttributeMapper mapper) { + this.attributeDao = attributeDao; + this.eventPublisher = eventPublisher; + this.mapper = mapper; + } + + // ===== External API ===== + + @Override + @Transactional + @WithSpan(kind = SpanKind.INTERNAL) + @Timed(value = "attributeDtoService.create", description = "Time taken to create attribute DTO", histogram = true) + @Counted(value = "attributeDtoService.create.count", description = "Number of attribute DTO creations") + public AttributeDto create(@SpanAttribute AttributeDto dto) { + Attribute entity = mapper.toEntity(dto); + Attribute saved = attributeDao.save(entity); + // Publish module-scoped event + eventPublisher.publishEvent(new edu.kit.datamanager.idoris.attributes.events.AttributeCreatedEvent(saved.getId(), mapper.toDto(saved))); + // Link relationships if provided + if (dto.getDataTypeId() != null && !dto.getDataTypeId().isBlank()) { + attributeDao.setDataType(saved.getId(), dto.getDataTypeId()); + } + if (dto.getOverrideId() != null && !dto.getOverrideId().isBlank()) { + attributeDao.setOverride(saved.getId(), dto.getOverrideId()); + } + Attribute reloaded = attributeDao.findById(saved.getId()).orElse(saved); + return mapper.toDto(reloaded); + } + + @Override + @Transactional + @WithSpan(kind = SpanKind.INTERNAL) + public AttributeDto update(String id, AttributeDto dto) { + Attribute existing = attributeDao.findById(id).orElseThrow(() -> new IllegalArgumentException("Attribute not found: " + id)); + Long previousVersion = existing.getVersion(); + mapper.applyPatch(dto, existing); // scalar fields only + Attribute saved = attributeDao.save(existing); + // Publish module-scoped updated event + eventPublisher.publishEvent(new edu.kit.datamanager.idoris.attributes.events.AttributeUpdatedEvent(saved.getId(), previousVersion, mapper.toDto(saved))); + return mapper.toDto(saved); + } + + @Override + @Transactional + @WithSpan(kind = SpanKind.INTERNAL) + public AttributeDto patch(String id, AttributeDto dto) { + Attribute existing = attributeDao.findById(id).orElseThrow(() -> new IllegalArgumentException("Attribute not found: " + id)); + Long previousVersion = existing.getVersion(); + mapper.applyPatch(dto, existing); + Attribute saved = attributeDao.save(existing); + // Publish module-scoped patched event + eventPublisher.publishEvent(new edu.kit.datamanager.idoris.attributes.events.AttributePatchedEvent(saved.getId(), previousVersion, mapper.toDto(saved))); + return mapper.toDto(saved); + } + + @Override + @Transactional + @WithSpan(kind = SpanKind.INTERNAL) + public void delete(@SpanAttribute("attribute.id") String id) { + Attribute existing = attributeDao.findById(id).orElseThrow(() -> new IllegalArgumentException("Attribute not found: " + id)); + // Publish module-scoped deleted event before deletion to include full payload + eventPublisher.publishEvent(new edu.kit.datamanager.idoris.attributes.events.AttributeDeletedEvent(existing.getId(), mapper.toDto(existing))); + attributeDao.delete(existing); + } + + @Override + @Transactional(readOnly = true) + public Optional get(String id) { + return attributeDao.findById(id).map(mapper::toDto); + } + + @Override + @Transactional(readOnly = true) + public List list() { + return attributeDao.findAll().stream().map(mapper::toDto).toList(); + } + + @Override + @Transactional + public AttributeDto setDataType(String attributeId, String dataTypeId) { + attributeDao.setDataType(attributeId, dataTypeId); + return get(attributeId).orElseThrow(); + } + + + @Override + @Transactional + public AttributeDto setOverride(String attributeId, String overrideAttributeId) { + attributeDao.setOverride(attributeId, overrideAttributeId); + return get(attributeId).orElseThrow(); + } + + @Override + @Transactional + public AttributeDto removeOverride(String attributeId) { + attributeDao.detachOverride(attributeId); + return get(attributeId).orElseThrow(); + } +} diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/services/AttributeService.java b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/services/AttributeService.java new file mode 100644 index 0000000..2af920d --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/services/AttributeService.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.attributes.services; + +import edu.kit.datamanager.idoris.attributes.dao.IAttributeDao; +import edu.kit.datamanager.idoris.core.domain.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; + +import java.util.List; +import java.util.Optional; + +/** + * Service for managing Attribute entities. + * This logic provides methods for creating, updating, and retrieving Attribute entities. + * It publishes domain events when entities are created, updated, or deleted. + */ +@Service +@Slf4j +@Observed(contextualName = "attributeService") +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 logic + */ + 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 + @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); + eventPublisher.publishEntityCreated(saved); + log.info("Created Attribute with PID: {}", saved.getId()); + 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 + @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); + + 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.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.getId()); + return saved; + } + + /** + * Deletes an Attribute entity. + * + * @param id the PID or internal ID of the Attribute to delete + * @throws IllegalArgumentException if the Attribute does not exist + */ + @Transactional + @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) + .orElseThrow(() -> new IllegalArgumentException("Attribute not found with ID: " + id)); + + attributeDao.delete(attribute); + eventPublisher.publishEntityDeleted(attribute); + log.info("Deleted Attribute with ID: {}", id); + } + + /** + * Retrieves an Attribute entity by its PID or internal ID. + * + * @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) + @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); + } + + /** + * Retrieves all Attribute entities. + * + * @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(); + } + + /** + * Deletes orphaned Attribute entities. + * 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(); + log.info("Deleted orphaned Attributes"); + } + + /** + * Partially updates an existing Attribute entity. + * + * @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 + @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"); + } + + // Get the current entity + 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 + 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.getDataTypeId() != null) { + existing.setDataTypeId(attributePatch.getDataTypeId()); + } + 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.getId()); + return saved; + } +} diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/web/api/IAttributeApi.java b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/web/api/IAttributeApi.java new file mode 100644 index 0000000..58c854e --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/web/api/IAttributeApi.java @@ -0,0 +1,203 @@ +/* + * 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.attributes.web.api; + +import edu.kit.datamanager.idoris.core.domain.Attribute; +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 or internal ID. + * + * @param id the PID or internal ID of the Attribute to retrieve + * @return the Attribute entity + */ + @GetMapping("/{id}") + @Operation( + 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", + schema = @Schema(implementation = Attribute.class))), + @ApiResponse(responseCode = "404", description = "Attribute not found") + } + ) + ResponseEntity> getAttribute( + @Parameter(description = "PID or internal ID of the Attribute", required = true) + @PathVariable String id); + + /** + * Gets the DataType of an Attribute. + * + * @param id the PID or internal ID of the Attribute + * @return the DataType of the Attribute + */ + @GetMapping("/{id}/dataType") + @Operation( + summary = "Get the DataType of an Attribute", + description = "Returns the ID of the DataType of an Attribute", + responses = { + @ApiResponse(responseCode = "200", description = "DataType found", + content = @Content(mediaType = "application/hal+json", + schema = @Schema(implementation = String.class))), + @ApiResponse(responseCode = "404", description = "Attribute not found") + } + ) + ResponseEntity> getDataType( + @Parameter(description = "PID or internal ID of the Attribute", required = true) + @PathVariable String id); + + /** + * 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 id the PID or internal ID of the Attribute to update + * @param attribute the updated Attribute entity + * @return the updated Attribute entity + */ + @PutMapping("/{id}") + @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 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 id the PID or internal ID of the Attribute to delete + * @return no content + */ + @DeleteMapping("/{id}") + @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 or internal ID of the Attribute", required = true) + @PathVariable String id); + + /** + * 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(); + + /** + * Partially updates an Attribute entity. + * + * @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("/{id}") + @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 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/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/web/api/package-info.java b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/web/api/package-info.java new file mode 100644 index 0000000..07b3feb --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/web/api/package-info.java @@ -0,0 +1,18 @@ +/* + * 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. + */ + +@org.springframework.modulith.NamedInterface("attributes.web.api") +package edu.kit.datamanager.idoris.attributes.web.api; \ No newline at end of file diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/web/hateoas/AttributeModelAssembler.java b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/web/hateoas/AttributeModelAssembler.java new file mode 100644 index 0000000..bbf8024 --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/web/hateoas/AttributeModelAssembler.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.attributes.web.hateoas; + +import edu.kit.datamanager.idoris.attributes.dto.AttributeDto; +import edu.kit.datamanager.idoris.attributes.web.v1.AttributeController; +import io.micrometer.observation.annotation.Observed; +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.Link; +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 that adds HATEOAS links to AttributeDto responses. + * Adds: + * - self: /v1/attributes/{id} + * - relations: dataType set/detach, override set/detach + */ +@Component +@Observed(contextualName = "attributeDtoModelAssembler") +public class AttributeModelAssembler implements RepresentationModelAssembler> { + + @Override + public EntityModel toModel(AttributeDto entity) { + EntityModel model = EntityModel.of(entity); + + // Add collection link + model.add(linkTo(methodOn(AttributeController.class).list()).withRel("collection")); + + // Relation operation links (templated with {id}) for discoverability + // Clients can replace {id} with their known identifier (PID or internalId) + String base = "/v1/attributes/{id}"; + model.add(Link.of(base).withSelfRel().withTitle("self (templated)")); + model.add(Link.of(base + "/dataType").withRel("dataType:set")); + model.add(Link.of(base + "/dataType").withRel("dataType:detach").withTitle("DELETE")); + model.add(Link.of(base + "/override").withRel("override:set")); + model.add(Link.of(base + "/override").withRel("override:detach").withTitle("DELETE")); + + return model; + } + + @Override + public CollectionModel> toCollectionModel(Iterable entities) { + CollectionModel> collection = RepresentationModelAssembler.super.toCollectionModel(entities); + collection.add(linkTo(methodOn(AttributeController.class).list()).withSelfRel()); + return collection; + } +} diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/web/v1/AttributeController.java b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/web/v1/AttributeController.java new file mode 100644 index 0000000..33052d8 --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/attributes/web/v1/AttributeController.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2025-2026 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.attributes.web.v1; + +import edu.kit.datamanager.idoris.attributes.api.IAttributeService; +import edu.kit.datamanager.idoris.attributes.dto.AttributeDto; +import edu.kit.datamanager.idoris.attributes.web.hateoas.AttributeModelAssembler; +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; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Optional; + +/** + * DTO-first REST controller for Attributes. + * Provides endpoints for managing Attributes using DTOs exclusively. + */ +@RestController +@RequestMapping("/v1/attributes") +@Slf4j +@Observed(contextualName = "attributeController") +public class AttributeController { + + @Autowired + private IAttributeService attributeService; + + @Autowired + private AttributeModelAssembler assembler; + + @GetMapping + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "attributeController.list", description = "Time taken to list attributes", histogram = true) + @Counted(value = "attributeController.list.count", description = "Number of attribute list requests") + public ResponseEntity>> list() { + List list = attributeService.list(); + return ResponseEntity.ok(assembler.toCollectionModel(list)); + } + + @GetMapping("/{id}") + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "attributeController.get", description = "Time taken to get attribute", histogram = true) + @Counted(value = "attributeController.get.count", description = "Number of attribute get requests") + public ResponseEntity> get(@SpanAttribute("attribute.id") @PathVariable String id) { + Optional dto = attributeService.get(id); + return dto.map(d -> ResponseEntity.ok(assembler.toModel(d))) + .orElseGet(() -> ResponseEntity.notFound().build()); + } + + @PostMapping + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "attributeController.create", description = "Time taken to create attribute", histogram = true) + @Counted(value = "attributeController.create.count", description = "Number of attribute create requests") + public ResponseEntity> create(@RequestBody AttributeDto dto) { + AttributeDto created = attributeService.create(dto); + return ResponseEntity.created( + assembler.toModel(created).getRequiredLink("self").toUri() + ).build(); + } + + @PutMapping("/{id}") + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "attributeController.update", description = "Time taken to update attribute", histogram = true) + @Counted(value = "attributeController.update.count", description = "Number of attribute update requests") + public ResponseEntity> update(@SpanAttribute("attribute.id") @PathVariable String id, @RequestBody AttributeDto dto) { + if (attributeService.get(id).isEmpty()) { + return ResponseEntity.notFound().build(); + } + AttributeDto updated = attributeService.update(id, dto); + return ResponseEntity.ok(assembler.toModel(updated)); + } + + @PatchMapping("/{id}") + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "attributeController.patch", description = "Time taken to patch attribute", histogram = true) + @Counted(value = "attributeController.patch.count", description = "Number of attribute patch requests") + public ResponseEntity> patch(@SpanAttribute("attribute.id") @PathVariable String id, @RequestBody AttributeDto dto) { + if (attributeService.get(id).isEmpty()) { + return ResponseEntity.notFound().build(); + } + AttributeDto patched = attributeService.patch(id, dto); + return ResponseEntity.ok(assembler.toModel(patched)); + } + + @DeleteMapping("/{id}") + @WithSpan(kind = SpanKind.SERVER) + @Timed(value = "attributeController.delete", description = "Time taken to delete attribute", histogram = true) + @Counted(value = "attributeController.delete.count", description = "Number of attribute delete requests") + public ResponseEntity delete(@SpanAttribute("attribute.id") @PathVariable String id) { + if (attributeService.get(id).isEmpty()) { + return ResponseEntity.notFound().build(); + } + attributeService.delete(id); + return ResponseEntity.noContent().build(); + } + + // Relationship endpoints + + @PostMapping("/{id}/dataType") + @WithSpan(kind = SpanKind.SERVER) + public ResponseEntity> setDataType(@PathVariable String id, @RequestBody String dataTypeId) { + if (attributeService.get(id).isEmpty()) { + return ResponseEntity.notFound().build(); + } + AttributeDto dto = attributeService.setDataType(id, dataTypeId); + return ResponseEntity.ok(assembler.toModel(dto)); + } + + @DeleteMapping("/{id}/dataType") + @WithSpan(kind = SpanKind.SERVER) + public ResponseEntity> detachDataType(@PathVariable String id) { + if (attributeService.get(id).isEmpty()) { + return ResponseEntity.notFound().build(); + } + AttributeDto dto = attributeService.removeOverride(id); + return ResponseEntity.ok(assembler.toModel(dto)); + } + + @PostMapping("/{id}/override") + @WithSpan(kind = SpanKind.SERVER) + public ResponseEntity> setOverride(@PathVariable String id, @RequestBody String overrideAttributeId) { + if (attributeService.get(id).isEmpty()) { + return ResponseEntity.notFound().build(); + } + AttributeDto dto = attributeService.setOverride(id, overrideAttributeId); + return ResponseEntity.ok(assembler.toModel(dto)); + } + + @DeleteMapping("/{id}/override") + @WithSpan(kind = SpanKind.SERVER) + public ResponseEntity> detachOverride(@PathVariable String id) { + if (attributeService.get(id).isEmpty()) { + return ResponseEntity.notFound().build(); + } + AttributeDto dto = attributeService.removeOverride(id); + return ResponseEntity.ok(assembler.toModel(dto)); + } +} diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/AdministrativeMetadataDto.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/AdministrativeMetadataDto.java new file mode 100644 index 0000000..b5d740a --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/AdministrativeMetadataDto.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; + +import java.time.Instant; + +/** + * Marker interface for Response DTOs that expose administrative metadata maintained by the server. + * Records representing response payloads should implement this interface to provide a common + * contract across modules without leaking entity classes. + */ +public interface AdministrativeMetadataDto { + String internalId(); + + Long version(); + + Instant createdAt(); + + Instant lastModifiedAt(); + + default String createdBy() { + return null; + } + + default String lastModifiedBy() { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/configuration/ApplicationProperties.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/ApplicationProperties.java similarity index 68% rename from src/main/java/edu/kit/datamanager/idoris/configuration/ApplicationProperties.java rename to idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/ApplicationProperties.java index 521d66c..ab77408 100644 --- a/src/main/java/edu/kit/datamanager/idoris/configuration/ApplicationProperties.java +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/ApplicationProperties.java @@ -13,12 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package edu.kit.datamanager.idoris.configuration; +package edu.kit.datamanager.idoris.core.configuration; import edu.kit.datamanager.idoris.rules.logic.OutputMessage; import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; import lombok.Getter; -import org.springframework.beans.factory.annotation.Value; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.validation.annotation.Validated; @@ -30,25 +33,20 @@ * * @author maximiliani */ -@Configuration @Getter +@Setter +@ConfigurationProperties(prefix = "idoris") +@Configuration +@AllArgsConstructor +@RequiredArgsConstructor @Validated public class ApplicationProperties { - /** - * The base URL of the IDORIS service, used in e.g., the PID records. - */ - @Value("${idoris.base-url") - @NotNull(message = "Base URL is required") - private String baseUrl; - /** * The policy to use for validating the input. * * @see ValidationPolicy */ - @Value("${idoris.validation-policy:LAX}") - @NotNull private ValidationPolicy validationPolicy = ValidationPolicy.LAX; /** @@ -56,25 +54,22 @@ public class ApplicationProperties { * * @see OutputMessage.MessageSeverity */ - @Value("${idoris.validation-level:INFO}") - @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 + * The base URL of the application. + * This is used to generate links in the responses. + * It must be set to the public URL of the application. + * Example: https://idoris.example.com */ - @Value("${idoris.pid-generation}") - @NotNull - private PIDGeneration pidGeneration = PIDGeneration.LOCAL; + @NotNull(message = "Base URL is required") + private String baseUrl; + +// // Reads Keycloak related settings from application.properties +// @Bean +// public KeycloakJwtProperties properties() { +// return new KeycloakJwtProperties(); +// } /** * The policy to use for validating the input. @@ -86,9 +81,4 @@ public class ApplicationProperties { public enum ValidationPolicy { STRICT, LAX } - - public enum PIDGeneration { - LOCAL, - TYPED_PID_MAKER, - } } diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/ETagConfiguration.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/ETagConfiguration.java new file mode 100644 index 0000000..a871c7e --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/ETagConfiguration.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2025-2026 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.configuration; + +import edu.kit.datamanager.idoris.core.domain.AdministrativeMetadata; +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.core.MethodParameter; +import org.springframework.hateoas.EntityModel; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +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.filter.OncePerRequestFilter; +import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +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); + } + }; + } + + @ControllerAdvice + public static 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; + } + } +} diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/JacksonConfiguration.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/JacksonConfiguration.java new file mode 100644 index 0000000..ec8e56c --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/JacksonConfiguration.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.configuration; + +import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static tools.jackson.databind.SerializationFeature.INDENT_OUTPUT; +import static tools.jackson.databind.cfg.DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS; + +@Configuration +public class JacksonConfiguration { + @Bean + JsonMapperBuilderCustomizer jacksonCustomizer() { + return builder -> builder + .disable(WRITE_DATES_AS_TIMESTAMPS) + .enable(INDENT_OUTPUT); + } + +// @Bean +// @Primary +// public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) { +// ObjectMapper mapper = builder.build(); +// mapper.registerModule(new JavaTimeModule()); +//// mapper.registerModule(new ValueObjectJacksonModule()); +// mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); +// return mapper; +// } +} diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/Neo4JConfiguration.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/Neo4JConfiguration.java new file mode 100644 index 0000000..30ce5aa --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/Neo4JConfiguration.java @@ -0,0 +1,165 @@ +/* + * 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.configuration; + +import edu.kit.datamanager.idoris.core.domain.valueObjects.EmailAddress; +import edu.kit.datamanager.idoris.core.domain.valueObjects.Name; +import edu.kit.datamanager.idoris.core.domain.valueObjects.ORCiD; +import edu.kit.datamanager.idoris.core.domain.valueObjects.PID; +import org.neo4j.driver.Value; +import org.neo4j.driver.internal.value.StringValue; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.data.neo4j.core.convert.Neo4jConversions; + +import java.net.URL; +import java.util.HashSet; +import java.util.Set; + +@Configuration +//@EnableNeo4jRepositories +//@EnableNeo4jAuditing +//@EnableTransactionManagement +public class Neo4JConfiguration { +// @Bean +// org.neo4j.cypherdsl.core.renderer.Configuration cypherDslConfiguration() { +// return org.neo4j.cypherdsl.core.renderer.Configuration +// .newConfig() +// .withDialect(Dialect.NEO4J_5) +// .build(); +// } + + @Bean + public Neo4jConversions neo4jConversions() { + return new Neo4jConversions(Set.of( + new PIDConverter(), + new ORCIDConverter(), + new NameConverter(), + new EmailConverter() + )); + } +// +// @Bean({"neo4jTemplate"}) +// @ConditionalOnMissingBean({Neo4jOperations.class}) +// public Neo4jTemplate neo4jTemplate( +// Neo4jClient neo4jClient, +// Neo4jMappingContext neo4jMappingContext, +// Driver driver, DatabaseSelectionProvider databaseNameProvider, ObjectProvider optionalCustomizers +// ) { +// Neo4jTransactionManager transactionManager = new Neo4jTransactionManager(driver, databaseNameProvider); +// optionalCustomizers.ifAvailable((customizer) -> { +// customizer.customize(transactionManager); +// }); +// return new Neo4jTemplate(neo4jClient, neo4jMappingContext, transactionManager); +// } + + // Converter to handle conversion between PID and String + public static class PIDConverter implements GenericConverter { + + @Override + public Set getConvertibleTypes() { + Set convertiblePairs = new HashSet<>(); + convertiblePairs.add(new ConvertiblePair(PID.class, Value.class)); + convertiblePairs.add(new ConvertiblePair(Value.class, PID.class)); + return convertiblePairs; + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (PID.class.isAssignableFrom(sourceType.getType())) { + // convert to Neo4j Driver Value + return new StringValue(source.toString()); + } else { + Value value = (Value) source; + // convert to MyCustomType + return new PID(value.asString()); + } + } + + } + + public static class ORCIDConverter implements GenericConverter { + + @Override + public Set getConvertibleTypes() { + Set convertiblePairs = new HashSet<>(); + convertiblePairs.add(new ConvertiblePair(ORCiD.class, URL.class)); + convertiblePairs.add(new ConvertiblePair(URL.class, ORCiD.class)); + return convertiblePairs; + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (ORCiD.class.isAssignableFrom(sourceType.getType())) { + // convert to Neo4j Driver Value + return new StringValue(source.toString()); + } else { + // convert to MyCustomType + return new ORCiD((String) source); + } + } + + } + + public static class NameConverter implements GenericConverter { + + @Override + public Set getConvertibleTypes() { + Set convertiblePairs = new HashSet<>(); + convertiblePairs.add(new ConvertiblePair(Name.class, Value.class)); + convertiblePairs.add(new ConvertiblePair(Value.class, Name.class)); + return convertiblePairs; + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (Name.class.isAssignableFrom(sourceType.getType())) { + // convert to Neo4j Driver Value + return new StringValue(source.toString()); + } else { + // convert to MyCustomType + Value value = (Value) source; + return new Name(value.asString()); + } + } + } + + public static class EmailConverter implements GenericConverter { + + @Override + public Set getConvertibleTypes() { + Set convertiblePairs = new HashSet<>(); + convertiblePairs.add(new ConvertiblePair(EmailAddress.class, Value.class)); + convertiblePairs.add(new ConvertiblePair(Value.class, EmailAddress.class)); + return convertiblePairs; + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (EmailAddress.class.isAssignableFrom(sourceType.getType())) { + // convert to Neo4j Driver Value + return new StringValue(source.toString()); + } else { + Value value = (Value) source; + // convert to MyCustomType + return new EmailAddress(value.asString()); + } + } + } +} diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/OpenAPIConfig.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/OpenAPIConfig.java new file mode 100644 index 0000000..303155d --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/OpenAPIConfig.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.core.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 = "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 { + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(new io.swagger.v3.oas.models.info.Info() + .title("IDORIS API") + .version("0.2.0") + .description("API documentation for IDORIS system") + .contact(new Contact() + .name("KIT Data Manager Team") + .email("webmaster@datamanager.kit.edu") + .url("https://kit-data-manager.github.io/webpage"))) + .externalDocs(new ExternalDocumentation() + .description("IDORIS GitHub Repository") + .url("https://github.com/maximiliani/idoris")); + } +} \ No newline at end of file diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/PIISpanAttribute.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/PIISpanAttribute.java new file mode 100644 index 0000000..cf30f18 --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/PIISpanAttribute.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2025-2026 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.configuration; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark method parameters that contain PII (Personally Identifiable Information) + * for conditional inclusion in OpenTelemetry spans. + *

    + * This annotation works like @SpanAttribute but only processes the parameter + * when pit.observability.includePiiInTraces=true is configured. + *

    + * Usage: + *

    + * public void myMethod(
    + *      {@literal @}PIISpanAttribute("pid") String pidValue,
    + *      {@literal @}PIISpanAttribute PIDRecord record) {
    + *       // method implementation
    + * }
    + * 
    + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface PIISpanAttribute { + + /** + * The name of the span attribute. If not provided, the parameter name will be used. + * + * @return the span attribute name + */ + String value() default ""; +} diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/PIISpanAttributeAspect.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/PIISpanAttributeAspect.java new file mode 100644 index 0000000..529e923 --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/PIISpanAttributeAspect.java @@ -0,0 +1,352 @@ +/* + * 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.configuration; + +import io.opentelemetry.api.trace.Span; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +/** + * Aspect-Oriented Programming (AOP) aspect that automatically intercepts method calls + * within the PIT service to extract and add Personally Identifiable Information (PII) + * data to OpenTelemetry distributed tracing spans. + * + *

    This aspect provides enhanced observability for development and debugging purposes + * by capturing sensitive parameter values that are explicitly marked with the + * {@link PIISpanAttribute} annotation.

    + * + *

    Key Features:

    + *
      + *
    • Conditional Activation: Only created when the configuration property + * {@code pit.observability.includePiiInTraces=true} is set
    • + *
    • Broad Interception: Intercepts ALL methods in the + * {@code edu.kit.datamanager.pit} package tree
    • + *
    • Selective Processing: Only processes methods that have parameters + * annotated with {@link PIISpanAttribute}
    • + *
    • OpenTelemetry Integration: Seamlessly integrates with the existing + * tracing infrastructure
    • + *
    + * + *

    Security and Privacy Considerations:

    + *

    ⚠️ CRITICAL SECURITY WARNING: This aspect captures and exports + * potentially sensitive PII data to tracing systems. This functionality should + * NEVER be enabled in production environments and should be used + * with extreme caution in development environments.

    + * + *
      + *
    • PII data may include user IDs, email addresses, personal identifiers, etc.
    • + *
    • Traced data may be stored in external observability platforms
    • + *
    • Ensure compliance with privacy regulations (GDPR, CCPA, etc.)
    • + *
    • Consider data retention policies and access controls
    • + *
    + * + *

    Performance Considerations:

    + *
      + *
    • Intercepts ALL method calls in the pit package (performance overhead)
    • + *
    • Uses reflection to inspect method parameters (additional CPU cost)
    • + *
    • Should be disabled in performance-critical production environments
    • + *
    + * + *

    Usage Example:

    + *
    + * {@code
    + * @Service
    + * public class UserService {
    + *     public User findUser(@PIISpanAttribute("userId") String userId) {
    + *         // This method call will be intercepted and userId will be added to the span
    + *         return userRepository.findById(userId);
    + *     }
    + * }
    + * }
    + * 
    + * + * @see PIISpanAttribute + * @see Span + */ +@Aspect +@Component +@ConditionalOnProperty(name = "pit.observability.includePiiInTraces", havingValue = "true") +public class PIISpanAttributeAspect { + + private static final Logger LOG = LoggerFactory.getLogger(PIISpanAttributeAspect.class); + + /** + * Spring's parameter name discoverer used to retrieve parameter names from method signatures. + * This is essential when the {@link PIISpanAttribute} annotation doesn't specify a custom + * attribute name - we fall back to using the actual parameter name from the source code. + * + *

    The DefaultParameterNameDiscoverer tries multiple strategies: + *

      + *
    • Uses debug information if available (compiled with -g flag)
    • + *
    • Falls back to ASM-based bytecode analysis
    • + *
    • Uses Java 8+ parameter names if compiled with -parameters flag
    • + *
    + */ + private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + + /** + * Constructor that logs the activation of PII tracing with appropriate warnings. + * + *

    This constructor is only called when the Spring condition + * {@code pit.observability.includePiiInTraces=true} is met, ensuring that + * the aspect is only active when explicitly configured.

    + * + *

    The constructor logs both informational and warning messages to ensure + * that the activation of PII tracing is clearly visible in application logs, + * helping to prevent accidental deployment to production environments.

    + */ + public PIISpanAttributeAspect() { + LOG.info("PIISpanAttributeAspect created - PII data will be included in traces"); + LOG.warn("WARNING: PII tracing is enabled! This should only be used in development environments."); + LOG.info("Aspect will intercept methods in package: edu.kit.datamanager.pit.*"); + LOG.info("Only methods with @PIISpanAttribute annotated parameters will have PII data extracted"); + } + + /** + * AspectJ around advice that intercepts ALL method calls within the PIT service + * package hierarchy to identify and process methods containing PII parameters. + * + *

    This method uses a broad pointcut expression that matches every method execution + * in the {@code edu.kit.datamanager.pit} package and all its sub-packages. While this + * approach has performance implications, it ensures comprehensive coverage without + * requiring developers to explicitly mark classes or methods.

    + * + *

    Execution Flow:

    + *
      + *
    1. Intercept method call before execution
    2. + *
    3. Perform quick scan of method parameters for {@link PIISpanAttribute} annotations
    4. + *
    5. If PII parameters found, extract and add them to the current OpenTelemetry span
    6. + *
    7. Proceed with original method execution
    8. + *
    9. Handle any errors gracefully without disrupting the original method
    10. + *
    + * + *

    Performance Optimization:

    + *

    To minimize performance impact, this method performs a quick preliminary check + * for PII annotations before proceeding with the more expensive span processing. + * Methods without PII parameters are processed with minimal overhead.

    + * + *

    Error Handling:

    + *

    Any exceptions during PII processing are caught and logged but do not interfere + * with the original method execution. This ensures that tracing issues don't break + * application functionality.

    + * + * @param joinPoint the AspectJ join point containing method signature and arguments + * @return the result of the original method execution + * @throws Throwable any exception thrown by the original method (PII processing exceptions are caught) + */ + @Around("execution(* edu.kit.datamanager.pit..*(..))") + public Object interceptAllMethods(ProceedingJoinPoint joinPoint) throws Throwable { + // Extract method information using AspectJ reflection capabilities + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + Parameter[] parameters = method.getParameters(); + + // Performance optimization: Quick scan for PII annotations before expensive processing + // This avoids the overhead of span processing for methods that don't have PII data + boolean hasPIIParams = false; + for (Parameter parameter : parameters) { + if (parameter.getAnnotation(PIISpanAttribute.class) != null) { + hasPIIParams = true; + break; // Early exit once we find the first PII parameter + } + } + + // Only process methods that actually have PII parameters + if (hasPIIParams) { + LOG.info("Found method with PII parameters: {}.{}", + method.getDeclaringClass().getSimpleName(), method.getName()); + + try { + LOG.info("Processing PII parameters for tracing"); + // Delegate to specialized method for span attribute processing + addPIIAttributesToCurrentSpan(joinPoint); + } catch (Exception e) { + // Critical: Ensure that PII processing errors don't break the application + // Log the error but continue with normal method execution + LOG.warn("Failed to add PII span attributes: {}", e.getMessage(), e); + } + } + + // Always proceed with the original method execution + // This is the core of the around advice - we must call proceed() to execute the original method + return joinPoint.proceed(); + } + + /** + * Core processing method that extracts PII data from method parameters and adds + * them as attributes to the current OpenTelemetry span. + * + *

    This method performs the detailed work of: + *

      + *
    • Validating that a valid OpenTelemetry span context exists
    • + *
    • Iterating through method parameters to find PII annotations
    • + *
    • Extracting parameter values and converting them to string representations
    • + *
    • Adding the PII data as span attributes for observability
    • + *
    + * + *

    OpenTelemetry Integration:

    + *

    This method relies on OpenTelemetry's automatic span propagation through + * thread-local storage. The {@code Span.current()} call retrieves the active + * span from the current thread's context, which should have been created by + * OpenTelemetry's auto-instrumentation or manual span creation.

    + * + *

    Parameter Name Resolution:

    + *

    The method uses Spring's {@link ParameterNameDiscoverer} to resolve parameter + * names when the annotation doesn't specify a custom attribute name. This requires + * that the application be compiled with parameter name information (Java 8+ with + * -parameters flag or debug information with -g flag).

    + * + *

    Data Safety:

    + *
      + *
    • Null parameter values are safely handled and logged
    • + *
    • Very long parameter values are truncated in log messages (but not in spans)
    • + *
    • All parameter values are converted to strings using {@code toString()}
    • + *
    + * + * @param joinPoint the AspectJ join point containing method signature and runtime arguments + */ + private void addPIIAttributesToCurrentSpan(ProceedingJoinPoint joinPoint) { + // Extract all necessary method information from the join point + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + Object[] args = joinPoint.getArgs(); // Runtime argument values + Parameter[] parameters = method.getParameters(); // Method parameter definitions + + // Attempt to discover parameter names for cases where annotation doesn't specify attribute name + // This uses reflection and bytecode analysis to retrieve the original parameter names + String[] parameterNames = parameterNameDiscoverer.getParameterNames(method); + + // Retrieve the current OpenTelemetry span from thread-local context + // This span should have been created by OpenTelemetry's auto-instrumentation + Span currentSpan = Span.current(); + if (currentSpan == null) { + LOG.warn("No current span available for PII attributes - OpenTelemetry may not be properly configured"); + return; + } + + // Validate that the span context is properly initialized and active + // An invalid span context indicates tracing infrastructure issues + if (!currentSpan.getSpanContext().isValid()) { + LOG.warn("Current span context is not valid - span may have been closed or not properly created"); + return; + } + + LOG.info("Current span: {}", currentSpan.getSpanContext().getSpanId()); + + // Process each parameter to look for PII annotations + int piiParamsProcessed = 0; + for (int i = 0; i < parameters.length && i < args.length; i++) { + Parameter parameter = parameters[i]; + PIISpanAttribute piiAnnotation = parameter.getAnnotation(PIISpanAttribute.class); + + if (piiAnnotation != null) { + if (args[i] != null) { + // Determine the span attribute name (from annotation or parameter name) + String attributeName = determineAttributeName(piiAnnotation, parameterNames, i); + + // Convert parameter value to string representation + // Note: This uses toString() which may not be suitable for all object types + String attributeValue = args[i].toString(); + + // Add the PII data to the OpenTelemetry span as a custom attribute + // This data will be included in distributed traces and exported to observability platforms + currentSpan.setAttribute(attributeName, attributeValue); + piiParamsProcessed++; + + // Log the addition with truncation for very long values to avoid log spam + LOG.debug("Successfully added PII span attribute: {} = {}", attributeName, + attributeValue.length() > 100 ? attributeValue.substring(0, 100) + "..." : attributeValue); + } else { + // Handle null parameter values gracefully - log but don't add to span + LOG.debug("PII parameter at index {} is null, skipping", i); + } + } + } + + // Summary logging to track PII processing activity + LOG.info("Total PII parameters processed: {} for method {}.{}", piiParamsProcessed, + method.getDeclaringClass().getSimpleName(), method.getName()); + } + + /** + * Determines the most appropriate span attribute name for a PII parameter using + * a fallback strategy that prioritizes explicit annotation values, then parameter + * names, and finally generates a generic name. + * + *

    This method implements a three-tier naming strategy:

    + *
      + *
    1. Explicit Annotation Value: If the {@link PIISpanAttribute} + * annotation specifies a non-empty value, use it as the attribute name. + * This gives developers full control over span attribute naming.
    2. + *
    3. Parameter Name Discovery: If no explicit value is provided, + * attempt to use the actual parameter name from the method signature. + * This requires proper compilation settings to preserve parameter names.
    4. + *
    5. Generated Fallback: If parameter names are not available, + * generate a generic attribute name based on the parameter position.
    6. + *
    + * + *

    Compilation Requirements for Parameter Names:

    + *

    For the second tier to work properly, the application must be compiled with + * one of the following options:

    + *
      + *
    • Java 8+ with -parameters flag: Preserves parameter names in bytecode
    • + *
    • Debug information (-g flag): Includes variable names in debug info
    • + *
    • IDE default settings: Most IDEs enable parameter name preservation by default
    • + *
    + * + *

    Attribute Naming Best Practices:

    + *
      + *
    • Use descriptive, consistent names for span attributes
    • + *
    • Consider namespace prefixes for application-specific attributes (e.g., "app.user.id")
    • + *
    • Avoid special characters that may cause issues in observability platforms
    • + *
    • Keep names reasonably short to minimize storage overhead
    • + *
    + * + * @param annotation the PII span attribute annotation that may contain an explicit name + * @param parameterNames array of parameter names discovered from the method signature, may be null + * @param parameterIndex zero-based index of the parameter in the method signature + * @return the determined span attribute name, never null or empty + */ + private String determineAttributeName(PIISpanAttribute annotation, String[] parameterNames, int parameterIndex) { + // First priority: Use explicit annotation value if provided + // This allows developers to specify meaningful, consistent attribute names + if (!annotation.value().isEmpty()) { + return annotation.value(); + } + + // Second priority: Use discovered parameter name if available + // This provides reasonable default names that match the source code + if (parameterNames != null && parameterIndex < parameterNames.length) { + return parameterNames[parameterIndex]; + } + + // Final fallback: Generate a generic but unique attribute name + // This ensures we always have a valid attribute name, even when parameter + // names are not available due to compilation settings + return "pii_arg" + parameterIndex; + } +} diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/ValidationConfig.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/ValidationConfig.java new file mode 100644 index 0000000..c9bfc6b --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/ValidationConfig.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2025-2026 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.configuration; + +import edu.kit.datamanager.idoris.core.domain.ValidationResult; +import edu.kit.datamanager.idoris.core.services.RuleService; +import edu.kit.datamanager.idoris.rules.logic.OutputMessage; +import edu.kit.datamanager.idoris.rules.logic.RuleTask; +import edu.kit.datamanager.idoris.rules.logic.VisitableElement; +import lombok.extern.slf4j.Slf4j; +import org.jspecify.annotations.NonNull; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +import java.util.List; +import java.util.Locale; + +/** + * 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 logic + * @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 record RuleBasedVisitableElementValidator(RuleService ruleService, + ApplicationProperties applicationProperties) implements Validator { + + @Override + public boolean supports(@NonNull Class clazz) { + return VisitableElement.class.isAssignableFrom(clazz); + } + + @Override + public void validate(@NonNull Object target, @NonNull 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, 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(Locale.ROOT); + String defaultMessage = message.message(); + + if (severity == OutputMessage.MessageSeverity.ERROR) { + // Errors are always rejected, no matter the configuration + 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); + } + } + }); + } + } +} diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/WebConfig.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/WebConfig.java new file mode 100644 index 0000000..39454f5 --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/configuration/WebConfig.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2025-2026 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.configuration; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.hateoas.config.EnableHypermediaSupport; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.firewall.DefaultHttpFirewall; +import org.springframework.security.web.firewall.HttpFirewall; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer; +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, EnableHypermediaSupport.HypermediaType.HTTP_PROBLEM_DETAILS}) +@EnableWebSecurity +@EnableMethodSecurity +@Slf4j +public class WebConfig implements WebMvcConfigurer { + + @Value("${idoris.security.allowedOriginPattern:http*://localhost:[*]}") + private String allowedOriginPattern; + + @Override + public void configureApiVersioning(ApiVersionConfigurer configurer) { + configurer.setDefaultVersion("1"); + configurer.useRequestHeader("API-Version"); + } + + @Override + public void addViewControllers(ViewControllerRegistry registry) { + registry.addViewController("/").setViewName("forward:/swagger-ui.html"); + } + + @Bean + protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.csrf(AbstractHttpConfigurer::disable).authorizeHttpRequests(auth -> auth.anyRequest().permitAll()); + return http.build(); // TODO configure security properly + } + + /** + * Configures CORS settings. + * + * @return the WebMvcConfigurer with CORS configuration + */ + @Bean + public WebSecurityCustomizer webSecurity() { + return web -> web.httpFirewall(allowUrlEncodedSlashHttpFirewall()); + } + + @Bean + public HttpFirewall allowUrlEncodedSlashHttpFirewall() { + DefaultHttpFirewall firewall = new DefaultHttpFirewall(); + // might be necessary for certain identifier types. + firewall.setAllowUrlEncodedSlash(true); + return firewall; + } + + @Bean + public CorsFilter corsFilter() { + CorsConfiguration corsConfig = new CorsConfiguration(); + corsConfig.setAllowCredentials(false); + corsConfig.addAllowedOriginPattern(allowedOriginPattern); + corsConfig.addAllowedHeader("*"); + corsConfig.addAllowedMethod("*"); + corsConfig.addExposedHeader("Content-Range"); + corsConfig.addExposedHeader("ETag"); + corsConfig.addExposedHeader("Last-Modified"); + corsConfig.addExposedHeader("Expires"); + corsConfig.addExposedHeader("Cache-Control"); + corsConfig.addExposedHeader("Trace-ID"); + + final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfig); + return new CorsFilter(source); + } +} diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/dao/IGenericRepo.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/dao/IGenericRepo.java new file mode 100644 index 0000000..1cbff70 --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/dao/IGenericRepo.java @@ -0,0 +1,71 @@ +/* + * 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.dao; + +import edu.kit.datamanager.idoris.core.domain.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.Repository; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface IGenericRepo extends Repository, Neo4jRepository, ListCrudRepository, PagingAndSortingRepository { + /** + * 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 + */ + default Optional findByPIDorInternalId(String id) { + Optional byPid = findByPid(id); + return byPid.isPresent() ? byPid : findByInternalId(id); + } + + /** + * Finds an entity by its PID. + * This method queries the PIDNode 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:PIDNode {pid: $pid})-[:IDENTIFIES]->(e:IDORIS) RETURN e") + Optional findByPid(@Param("pid") String pid); + + /** + * Finds an entity by its internal ID. + * This method queries the PIDNode 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 + */ + Optional findByInternalId(@Param("internalId") String internalId); + + /** + * Finds all PIDs associated with a given internal ID. + * This method queries the PIDNode table to find all PIDs that identify the entity with the given internal ID. + * + * @param internalId The internal ID of the entity + * @return A list of PIDNode objects associated with the internal ID + */ + @Query("MATCH (p:PIDNode)-[:IDENTIFIES]->(e:IDORIS {internalId: $internalId}) RETURN p") + List findPidsByInternalId(@Param("internalId") String internalId); +} diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/GenericIDORISEntity.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/AdministrativeMetadata.java similarity index 64% rename from src/main/java/edu/kit/datamanager/idoris/domain/GenericIDORISEntity.java rename to idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/AdministrativeMetadata.java index ec29207..ac08e47 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/GenericIDORISEntity.java +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/AdministrativeMetadata.java @@ -14,17 +14,20 @@ * 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.pids.ConfigurablePIDGenerator; +import edu.kit.datamanager.idoris.core.domain.valueObjects.Name; +import edu.kit.datamanager.idoris.core.domain.valueObjects.Reference; +import edu.kit.datamanager.idoris.rules.logic.VisitableElement; import lombok.*; import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; 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 org.springframework.data.neo4j.core.support.UUIDStringGenerator; import java.io.Serializable; import java.time.Instant; @@ -32,16 +35,18 @@ @Getter @Setter -@EqualsAndHashCode(callSuper = true) +@EqualsAndHashCode @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PROTECTED) -public abstract class GenericIDORISEntity extends VisitableElement implements Serializable { - @GeneratedValue(ConfigurablePIDGenerator.class) - String pid; +@Node("IDORIS") +public abstract class AdministrativeMetadata implements Serializable, VisitableElement { + @Id + @GeneratedValue(UUIDStringGenerator.class) + String internalId; - String name; + Name name; - String description; + Name description; @Version Long version; @@ -52,10 +57,15 @@ public abstract class GenericIDORISEntity extends VisitableElement implements Se @LastModifiedDate Instant lastModifiedAt; - Set expectedUseCases; + Set expectedUseCases; @Relationship(value = "contributors", direction = Relationship.Direction.OUTGOING) Set contributors; Set references; -} \ No newline at end of file + + @Override + public String getId() { + return internalId; + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/AtomicDataType.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/AtomicDataType.java similarity index 87% rename from src/main/java/edu/kit/datamanager/idoris/domain/entities/AtomicDataType.java rename to idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/AtomicDataType.java index 9a23a6a..a4cdc58 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/AtomicDataType.java +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/AtomicDataType.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.entities; +package edu.kit.datamanager.idoris.core.domain; -import edu.kit.datamanager.idoris.domain.enums.PrimitiveDataTypes; +import edu.kit.datamanager.idoris.core.domain.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; @@ -46,7 +46,7 @@ public final class AtomicDataType extends DataType { private Integer maximum; @Override - protected > T accept(Visitor visitor, Object... args) { + public > T accept(Visitor visitor, Object... args) { return visitor.visit(this, args); } @@ -60,5 +60,4 @@ public boolean inheritsFrom(DataType dataType) { } return false; } - } diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/Attribute.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/Attribute.java similarity index 79% rename from src/main/java/edu/kit/datamanager/idoris/domain/entities/Attribute.java rename to idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/Attribute.java index 1f23fe2..3f008f9 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/Attribute.java +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/Attribute.java @@ -14,9 +14,8 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.entities; +package edu.kit.datamanager.idoris.core.domain; -import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; import edu.kit.datamanager.idoris.rules.logic.RuleOutput; import edu.kit.datamanager.idoris.rules.logic.Visitor; import jakarta.validation.constraints.NotNull; @@ -32,21 +31,21 @@ @RequiredArgsConstructor @Getter @Setter -public class Attribute extends GenericIDORISEntity { +public class Attribute extends AdministrativeMetadata { private String defaultValue; private String constantValue; private Integer lowerBoundCardinality = 0; private Integer upperBoundCardinality; - @Relationship(value = "dataType", direction = Relationship.Direction.OUTGOING) + // Keep only the target ID to avoid cross-module dependency. Relationship managed via DAO methods. @NotNull - private DataType dataType; + private String dataTypeId; @Relationship(value = "override", direction = Relationship.Direction.OUTGOING) private Attribute override; @Override - protected > T accept(Visitor visitor, Object... args) { + public > 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/ORCiDUser.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/DataType.java similarity index 70% rename from src/main/java/edu/kit/datamanager/idoris/domain/entities/ORCiDUser.java rename to idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/DataType.java index 44e58dc..6a95abe 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/ORCiDUser.java +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/DataType.java @@ -14,21 +14,21 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.entities; +package edu.kit.datamanager.idoris.core.domain; -import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; +import lombok.Setter; import org.springframework.data.neo4j.core.schema.Node; -import java.net.URL; - +@Node("DataType") @Getter +@Setter @AllArgsConstructor -@NoArgsConstructor -@Node("ORCiDUser") -public final class ORCiDUser extends User { - @JsonProperty("orcid") - private URL orcid; +@RequiredArgsConstructor +public abstract class DataType extends AdministrativeMetadata { + private String defaultValue; + + public abstract boolean inheritsFrom(DataType dataType); } diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/Operation.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/Operation.java similarity index 86% rename from src/main/java/edu/kit/datamanager/idoris/domain/entities/Operation.java rename to idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/Operation.java index 6afc8ec..4434250 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/Operation.java +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/Operation.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.entities; +package edu.kit.datamanager.idoris.core.domain; -import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import edu.kit.datamanager.idoris.core.domain.valueObjects.OperationStep; import edu.kit.datamanager.idoris.rules.logic.RuleOutput; import edu.kit.datamanager.idoris.rules.logic.Visitor; import lombok.AllArgsConstructor; @@ -34,7 +34,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) @@ -46,7 +46,7 @@ public class Operation extends GenericIDORISEntity { private List execution; @Override - protected > T accept(Visitor visitor, Object... args) { + public > T accept(Visitor visitor, Object... args) { return visitor.visit(this, args); } } diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/TechnologyInterface.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/TechnologyInterface.java similarity index 84% rename from src/main/java/edu/kit/datamanager/idoris/domain/entities/TechnologyInterface.java rename to idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/TechnologyInterface.java index e311354..caae7d5 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/TechnologyInterface.java +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/TechnologyInterface.java @@ -14,9 +14,8 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.entities; +package edu.kit.datamanager.idoris.core.domain; -import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; import edu.kit.datamanager.idoris.rules.logic.RuleOutput; import edu.kit.datamanager.idoris.rules.logic.Visitor; import lombok.AllArgsConstructor; @@ -33,7 +32,7 @@ @Setter @RequiredArgsConstructor @AllArgsConstructor -public class TechnologyInterface extends GenericIDORISEntity { +public class TechnologyInterface extends AdministrativeMetadata { @Relationship(value = "attributes", direction = Relationship.Direction.INCOMING) private Set attributes; @@ -44,7 +43,7 @@ public class TechnologyInterface extends GenericIDORISEntity { private Set adapters = Set.of(); @Override - protected > T accept(Visitor visitor, Object... args) { + public > 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/TypeProfile.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/TypeProfile.java similarity index 88% rename from src/main/java/edu/kit/datamanager/idoris/domain/entities/TypeProfile.java rename to idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/TypeProfile.java index f2870f2..f4ef4da 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/TypeProfile.java +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/TypeProfile.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.entities; +package edu.kit.datamanager.idoris.core.domain; -import edu.kit.datamanager.idoris.domain.enums.CombinationOptions; +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 +33,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; @@ -47,7 +47,7 @@ public final class TypeProfile extends DataType { private CombinationOptions validationPolicy = CombinationOptions.ALL; @Override - protected > T accept(Visitor visitor, Object... args) { + public > T accept(Visitor visitor, Object... args) { return visitor.visit(this, args); } diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/User.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/User.java similarity index 72% rename from src/main/java/edu/kit/datamanager/idoris/domain/entities/User.java rename to idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/User.java index a3c69ea..a50bb57 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/User.java +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/User.java @@ -14,16 +14,18 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.entities; +package edu.kit.datamanager.idoris.core.domain; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; +import edu.kit.datamanager.idoris.core.domain.valueObjects.EmailAddress; +import edu.kit.datamanager.idoris.core.domain.valueObjects.Name; +import edu.kit.datamanager.idoris.core.domain.valueObjects.ORCiD; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.neo4j.core.schema.GeneratedValue; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.support.UUIDStringGenerator; @@ -36,17 +38,17 @@ @Setter @AllArgsConstructor @RequiredArgsConstructor -@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(value = ORCiDUser.class, name = "orcid"), - @JsonSubTypes.Type(value = TextUser.class, name = "text") -}) -public abstract sealed class User implements Serializable permits ORCiDUser, TextUser { +public class User implements Serializable { @CreatedDate Instant createdAt; + + @LastModifiedDate + Instant updatedAt; + @Id @GeneratedValue(UUIDStringGenerator.class) private String internalId; - private String type; + private Name name; + private EmailAddress email; + private ORCiD orcid; } - diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationResult.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/ValidationResult.java similarity index 76% rename from src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationResult.java rename to idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/ValidationResult.java index d74a37c..baf9f03 100644 --- a/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationResult.java +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/ValidationResult.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024-2025 Karlsruhe Institute of Technology + * Copyright (c) 2024-2026 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,9 +14,10 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.rules.validation; +package edu.kit.datamanager.idoris.core.domain; import edu.kit.datamanager.idoris.rules.logic.OutputMessage; +import edu.kit.datamanager.idoris.rules.logic.Rule; import edu.kit.datamanager.idoris.rules.logic.RuleOutput; import lombok.Getter; import lombok.Setter; @@ -68,6 +69,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, Rule rule, Object element) { + return new ValidationResult().addMessage(message, element, rule, OutputMessage.MessageSeverity.ERROR); + } + /** * Adds a validation message to this result. * @@ -76,11 +93,23 @@ public class ValidationResult implements RuleOutput { * @param type the severity type of the message * @return this result instance for method chaining */ - public ValidationResult addMessage(String message, Object element, OutputMessage.MessageSeverity type) { - messages.add(new OutputMessage(message, type, element)); + public ValidationResult addMessage(String message, Object element, Rule rule, OutputMessage.MessageSeverity type) { + messages.add(new OutputMessage(message, type, rule, element)); return this; } + public static ValidationResult warning(String message, Rule rule, Object element) { + return new ValidationResult().addMessage(message, element, rule, WARNING); + } + + public static ValidationResult info(String message, Rule rule, Object element) { + return new ValidationResult().addMessage(message, element, rule, 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. @@ -175,20 +204,36 @@ public ValidationResult merge(ValidationResult... others) { try { // Process each ValidationResult individually for (ValidationResult other : others) { - if (other == null) continue; - - if (!other.isEmpty()) { - this.addChild(other); - } + // Delegate to the single-argument merge method to avoid classloader issues with varargs + this.merge(other); // Call the new single-argument merge } } catch (Exception e) { // Catch any exception to prevent the application from crashing - log.error("Error in ValidationResult.merge: {}", e.getMessage(), e); + log.error("Error in ValidationResult.merge (varargs): {}", e.getMessage(), e); } return this; } + /** + * Merges a single other ValidationResult instance into this one. + * This overloaded method is introduced to mitigate ClassCastExceptions + * that can occur with varargs and Spring Boot DevTools' class loading + * mechanism. + * + * @param other The other ValidationResult instance to merge. + * @return This instance after merging. + */ + @Override // Implement the new single-argument merge method + public ValidationResult merge(ValidationResult other) { + if (other == null) return this; + + if (!other.isEmpty()) { + this.addChild(other); + } + return this; + } + @Override public ValidationResult empty() { return new ValidationResult(); @@ -199,7 +244,7 @@ public ValidationResult addMessage(OutputMessage message) { // Convert OutputMessage to OutputMessage OutputMessage.MessageSeverity severity = convertSeverity(message.severity()); Object element = message.element().length > 0 ? message.element()[0] : null; - messages.add(new OutputMessage(message.message(), severity, element)); + messages.add(new OutputMessage(message.message(), severity, message.rule(), element)); return this; } diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/ValidationVisitor.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/ValidationVisitor.java new file mode 100644 index 0000000..9202012 --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/ValidationVisitor.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2025-2026 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; + +import edu.kit.datamanager.idoris.rules.logic.IRule; +import edu.kit.datamanager.idoris.rules.logic.VisitableElement; +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; +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.aop.support.AopUtils; + +@Observed +public abstract class ValidationVisitor extends Visitor implements IRule { + /** + * Constructor for ValidationVisitor. + * Initializes the visitor with a factory that creates new ValidationResult instances. + */ + public ValidationVisitor() { + super(ValidationResult::new); + } + + /** + * Process method required by IRule interface. + * Delegates to the appropriate validate method based on element type. + * + * @param input the element to process + * @param output the output to update with processing results + */ + @Override + @WithSpan(kind = SpanKind.INTERNAL) + public void process(@SpanAttribute VisitableElement input, @SpanAttribute ValidationResult output) { + // Fix for Spring AOP / Visitor Pattern conflict: + // We must pass the ACTUAL object (Target) to the visitor logic, not the CGLIB Proxy. + // If we pass the Proxy, the reflective dispatch in the base Visitor class may fail + // to find the specific visit() methods on the generated proxy class. + Visitor visitorInstance = this; + + if (AopUtils.isAopProxy(this)) { + Object target = AopProxyUtils.getSingletonTarget(this); + if (target instanceof Visitor vtarget) { + //noinspection unchecked + visitorInstance = (Visitor) vtarget; + } + } + + ValidationResult result = input.execute(visitorInstance); + output.merge(result); + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/enums/CombinationOptions.java b/idoris/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 idoris/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/idoris/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/domain/enums/ExecutionMode.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/enums/ExecutionMode.java similarity index 92% rename from src/main/java/edu/kit/datamanager/idoris/domain/enums/ExecutionMode.java rename to idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/enums/ExecutionMode.java index cc91fdd..71c2da8 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/enums/ExecutionMode.java +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/enums/ExecutionMode.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.enums; +package edu.kit.datamanager.idoris.core.domain.enums; public enum ExecutionMode { sync, async -} +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/enums/PrimitiveDataTypes.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/enums/PrimitiveDataTypes.java similarity index 97% rename from src/main/java/edu/kit/datamanager/idoris/domain/enums/PrimitiveDataTypes.java rename to idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/enums/PrimitiveDataTypes.java index e3e2bcf..35e9778 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/enums/PrimitiveDataTypes.java +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/enums/PrimitiveDataTypes.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; @@ -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/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/valueObjects/AbstractValueObject.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/valueObjects/AbstractValueObject.java new file mode 100644 index 0000000..aeb2c63 --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/valueObjects/AbstractValueObject.java @@ -0,0 +1,47 @@ +/* + * 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.valueObjects; + + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public abstract class AbstractValueObject { + private final T unvalidated; + + @JsonCreator + public AbstractValueObject(T unvalidated) { + if (unvalidated == null) { + throw new NullPointerException("Value object cannot be null"); + } + this.unvalidated = unvalidated; + } + + protected T getUnvalidated() { + return this.unvalidated; + } + + @Override + public abstract int hashCode(); + + @Override + public abstract boolean equals(Object o); + + @Override + @JsonValue + public abstract String toString(); +} diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/AttributeMapping.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/valueObjects/AttributeMapping.java similarity index 70% rename from src/main/java/edu/kit/datamanager/idoris/domain/entities/AttributeMapping.java rename to idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/valueObjects/AttributeMapping.java index 75ba923..b7527c4 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/AttributeMapping.java +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/valueObjects/AttributeMapping.java @@ -14,15 +14,18 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.entities; +package edu.kit.datamanager.idoris.core.domain.valueObjects; -import edu.kit.datamanager.idoris.domain.VisitableElement; +import edu.kit.datamanager.idoris.core.domain.Attribute; import edu.kit.datamanager.idoris.rules.logic.RuleOutput; +import edu.kit.datamanager.idoris.rules.logic.VisitableElement; import edu.kit.datamanager.idoris.rules.logic.Visitor; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Relationship; @@ -33,7 +36,11 @@ @Getter @AllArgsConstructor @NoArgsConstructor -public class AttributeMapping extends VisitableElement implements Serializable { +public class AttributeMapping implements VisitableElement, Serializable { + @Id + @GeneratedValue(GeneratedValue.UUIDGenerator.class) + private String internalId; + private String name; @Relationship(value = "input", direction = Relationship.Direction.INCOMING) @@ -47,7 +54,12 @@ public class AttributeMapping extends VisitableElement implements Serializable { private Attribute output; @Override - protected > T accept(Visitor visitor, Object... args) { + public String getId() { + return ""; + } + + @Override + public > T accept(Visitor visitor, Object... args) { return visitor.visit(this, args); } -} +} \ No newline at end of file diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/valueObjects/EmailAddress.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/valueObjects/EmailAddress.java new file mode 100644 index 0000000..c8a8dd9 --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/valueObjects/EmailAddress.java @@ -0,0 +1,73 @@ +/* + * 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.valueObjects; + +/** + * Value object representing an email address with validation. + * Validates the email format and length upon instantiation. + * Uses a regex pattern to ensure the email adheres to standard formats. + * Note: This regex is a simplified version and may not cover all edge cases. + */ +public final class EmailAddress extends AbstractValueObject { + private static final String EMAIL_REGEX = "^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])$"; + private final String email; + + /** + * Constructor accepting an email string and validating it. + * + * @param email The email address to validate. + * @throws IllegalArgumentException if the email is null, blank, too short, too long, or invalid. + */ + public EmailAddress(String email) { + super(email); + if (email == null || email.isBlank()) { + throw new IllegalArgumentException("Email cannot be null or blank"); + } + + email = email.trim(); + + if (email.length() < 5) { + throw new IllegalArgumentException("Email must be at least 5 characters long"); + } + if (email.length() > 255) { + throw new IllegalArgumentException("Email cannot be longer than 255 characters"); + } + + if (!email.matches(EMAIL_REGEX)) { + throw new IllegalArgumentException("Invalid email format"); + } + + this.email = email; + } + + @Override + public int hashCode() { + return email.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof EmailAddress that)) return false; + + return email.equals(that.email); + } + + @Override + public String toString() { + return email; + } +} diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/valueObjects/Name.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/valueObjects/Name.java new file mode 100644 index 0000000..0aeaaad --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/valueObjects/Name.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.core.domain.valueObjects; + +public final class Name extends AbstractValueObject { + private final String name; + + public Name(String name) { + super(name); + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("Name cannot be null or blank"); + } + + // Allow harmless characters, remove others + name = name.replaceAll("[^\\p{L}\\p{N} .,\\-_#@'&:;€$£¥()]", ""); + + name = name.trim(); + + if (name.length() < 3) { + throw new IllegalArgumentException("Name must be at least 3 characters long"); + } + if (name.length() > 255) { + throw new IllegalArgumentException("Name cannot be longer than 255 characters"); + } + + this.name = name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Name name1)) return false; + + return name.equals(name1.name); + } + + @Override + public String toString() { + return name; + } +} \ No newline at end of file diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/valueObjects/ORCiD.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/valueObjects/ORCiD.java new file mode 100644 index 0000000..6072e1c --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/valueObjects/ORCiD.java @@ -0,0 +1,194 @@ +/* + * 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.valueObjects; + +import lombok.extern.slf4j.Slf4j; + +import java.net.URI; +import java.net.URL; +import java.util.regex.Pattern; + +/** + * Value object representing a validated ORCiD. + * Ensures the ORCiD is in the correct format and has a valid check digit. + * Note: This class does not verify the existence of the ORCiD in the ORCiD registry. + */ +@Slf4j +public final class ORCiD extends AbstractValueObject { + private static final String ORCID_REGEX = "^(\\d{4}-\\d{4}-\\d{4}-[(\\d{3}\\dX)(\\d{4})])$"; + private static final String ORCID_URL_REGEX = "^(https?://)?((.+\\.)?orcid\\.org)/(\\d{4}-\\d{4}-\\d{4}-[(\\d{3}\\dX)(\\d{4})])$"; + + private final URL orcid; + + /** + * Constructor accepting an unvalidated URL. + * + * @param unvalidated The unvalidated ORCiD URL. + * @throws IllegalArgumentException if the URL is null or invalid. + */ + public ORCiD(URL unvalidated) { + super(unvalidated.toString()); + if (unvalidated == null) { + throw new IllegalArgumentException("ORCiD URL cannot be null"); + } + this.orcid = this.getValidORCiDURL(unvalidated.getHost(), unvalidated.getPath()); + } + + /** + * Validates the ORCiD components and constructs a URL. + * + * @param host The host part of the ORCiD URL. + * @param path The path part of the ORCiD URL. + * @return A validated ORCiD URL. + * @throws IllegalArgumentException if validation fails. + */ + private URL getValidORCiDURL(String host, String path) { + String orcidId = validate(host, path); + URI uri = URI.create(String.format("https://%s/%s", host, orcidId)); + try { + return uri.toURL(); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to create ORCiD URL", e); + } + } + + /** + * Validates the ORCiD format and check digit. + * + * @param host The ORCiD registry used (e.g., orcid.org or sandbox.orcid.org). Must end with orcid.org. + * @param path The ORCiD identifier path (e.g., 0000-0002-1825-0097). + * @return The validated ORCiD identifier. + * @throws IllegalArgumentException if validation fails. + */ + private String validate(String host, String path) { + if (host == null || host.isBlank() || !host.endsWith("orcid.org")) { + throw new IllegalArgumentException("ORCiD URL must have a valid host ending with orcid.org"); + } + if (path == null || path.isBlank()) { + throw new IllegalArgumentException("ORCiD URL must have a valid path"); + } + String orcidId = path.startsWith("/") ? path.substring(1) : path; + if (!orcidId.matches(ORCID_REGEX)) { + throw new IllegalArgumentException("Invalid ORCiD format"); + } + + String[] parts = orcidId.split("-"); + String baseDigits = String.join("", parts).substring(0, 15); + char expectedCheckDigit = generateCheckDigit(baseDigits.toCharArray()); + char actualCheckDigit = parts[3].substring(3).charAt(0); + if (expectedCheckDigit != actualCheckDigit) { + throw new IllegalArgumentException("Invalid ORCiD check digit"); + } + + return orcidId; + } + + /** + * Generates the check digit for the given base digits using the ISO 7064 Mod 11-2 algorithm. + * Provided in the ORCiD specification: Structure of the ORCID Identifier + * + * @param baseDigits The first 15 digits of the ORCiD identifier. Without dashes and check digit. + * @return The calculated check digit as a char ("0"-"9" or "X"). + */ + private Character generateCheckDigit(char[] baseDigits) { + if (baseDigits == null || baseDigits.length != 15) { + throw new IllegalArgumentException("Base digits must be exactly 15 characters long"); + } + + int total = 0; + for (int i = 0; i < 15; i++) { + if (baseDigits[i] < '0' || baseDigits[i] > '9') { + throw new IllegalArgumentException("Base digits must be numeric"); + } + int digit = Character.getNumericValue(baseDigits[i]); + total = (total + digit) * 2; + } + int remainder = total % 11; + int result = (12 - remainder) % 11; + return result == 10 ? 'X' : (char) result; + } + + /** + * Constructor accepting an unvalidated string. + * The string can be either a full ORCiD URL or just the ORCiD ID. + * + * @param unvalidatedInput The unvalidated ORCiD input string. + * @throws IllegalArgumentException if the input is invalid. + */ +// @JsonCreator + public ORCiD(String unvalidatedInput) { + super(unvalidatedInput); + if (unvalidatedInput == null || unvalidatedInput.isBlank()) { + throw new IllegalArgumentException("ORCiD input cannot be null or blank"); + } + + // Use the grouping in the regex to extract host and path + Pattern pattern = Pattern.compile(ORCID_URL_REGEX); + var matcher = pattern.matcher(unvalidatedInput); + if (matcher.matches()) { + // If a full URL is provided, extract host and path + String host = matcher.group(2); + String path = matcher.group(4); + this.orcid = this.getValidORCiDURL(host, path); + } else if (unvalidatedInput.matches(ORCID_REGEX)) { + // If only the ORCiD ID is provided, construct the full URL using the default host + String host = "orcid.org"; + this.orcid = this.getValidORCiDURL(host, unvalidatedInput); + } else { + throw new IllegalArgumentException("Invalid ORCiD input format: Either full URL or ORCiD ID string must be provided"); + } + } + + /** + * Returns the validated ORCiD URL. + * e.g., https://orcid.org/0000-0002-1825-0097 + * + * @return The ORCiD URL. + */ + public URL get() { + return orcid; + } + + /** + * Returns the ORCiD identifier without the URL part. + * + * @return The ORCiD ID string (e.g., 0000-0002-1825-0097). + */ + public String getORCiDIDSubstring() { + String path = orcid.getPath(); + return path.startsWith("/") ? path.substring(1) : path; + } + + @Override + public int hashCode() { + return orcid.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ORCiD orCiD)) return false; + + return orcid.equals(orCiD.orcid); + } + + @Override +// @JsonValue + public String toString() { + return orcid.toExternalForm(); + } + +} diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/OperationStep.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/valueObjects/OperationStep.java similarity index 71% rename from src/main/java/edu/kit/datamanager/idoris/domain/entities/OperationStep.java rename to idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/valueObjects/OperationStep.java index 3b141a4..800aca2 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/OperationStep.java +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/valueObjects/OperationStep.java @@ -14,16 +14,20 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.entities; +package edu.kit.datamanager.idoris.core.domain.valueObjects; -import edu.kit.datamanager.idoris.domain.VisitableElement; -import edu.kit.datamanager.idoris.domain.enums.ExecutionMode; +import edu.kit.datamanager.idoris.core.domain.Operation; +import edu.kit.datamanager.idoris.core.domain.TechnologyInterface; +import edu.kit.datamanager.idoris.core.domain.enums.ExecutionMode; import edu.kit.datamanager.idoris.rules.logic.RuleOutput; +import edu.kit.datamanager.idoris.rules.logic.VisitableElement; import edu.kit.datamanager.idoris.rules.logic.Visitor; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; +import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Relationship; @@ -35,7 +39,11 @@ @RequiredArgsConstructor @Getter @Setter -public class OperationStep extends VisitableElement implements Serializable { +public class OperationStep implements VisitableElement, Serializable { + @Id + @GeneratedValue(GeneratedValue.UUIDGenerator.class) + private String internalId; + private Integer index; private String name; @@ -58,7 +66,12 @@ public class OperationStep extends VisitableElement implements Serializable { private List outputMappings; @Override - protected > T accept(Visitor visitor, Object... args) { + public String getId() { + return internalId; + } + + @Override + public > T accept(Visitor visitor, Object... args) { return visitor.visit(this, args); } -} +} \ No newline at end of file diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/valueObjects/PID.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/valueObjects/PID.java new file mode 100644 index 0000000..a121786 --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/valueObjects/PID.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2025-2026 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.valueObjects; + +import com.fasterxml.jackson.annotation.JsonCreator; + +import java.util.regex.Pattern; + +/** + * Value object representing a persistent identifier (PID) in the form of a handle. + * A valid PID must match the pattern: /, where: + * - consists of alphanumeric characters and dots (e.g., "12345.6789") + * - consists of printable ASCII characters (from '!' to '~') + * Note: This class does not verify the existence of the PID in any registry. + */ +public final class PID extends AbstractValueObject { + private static final String HANDLE_REGEX = "^([0-9A-Za-z]+(\\.[0-9A-Za-z]+)*)/([!-~]+)$"; + private final String prefix; + private final String suffix; + + /** + * Constructor accepting an unvalidated PID string. + * + * @param unvalidated The unvalidated PID string. + * @throws IllegalArgumentException if the PID is null, blank, or invalid. + */ + @JsonCreator + public PID(String unvalidated) { + super(unvalidated); + if (unvalidated.isBlank()) { + throw new IllegalArgumentException("PID cannot be null or blank."); + } + + Pattern pattern = Pattern.compile(HANDLE_REGEX); + var matcher = pattern.matcher(unvalidated); + if (!matcher.matches()) { + throw new IllegalArgumentException("PID must be of the form / with valid characters."); + } + + if (matcher.groupCount() < 2) { + throw new IllegalArgumentException("PID must contain both a prefix and a suffix."); + } + + // Split into prefix and suffix using the regex groups + this.prefix = matcher.group(1); // The prefix part is the second capturing group + this.suffix = matcher.group(3); // The suffix part is the last capturing group + } + + /** + * This method is only necessary for the neo4j OGM to work properly. + * It returns a valid PID string. + * + * @return The validated PID string. + */ + @Override + public String getUnvalidated() { + return get(); + } + + @Override + public int hashCode() { + int result = getPrefix().hashCode(); + result = 31 * result + getSuffix().hashCode(); + return result; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof PID pid)) return false; + + return getPrefix().equals(pid.getPrefix()) && getSuffix().equals(pid.getSuffix()); + } + + @Override + public String toString() { + return get(); + } + + /** + * Returns the prefix part of the PID. + * + * @return The prefix. + */ + public String getPrefix() { + return prefix; + } + + /** + * Returns the suffix part of the PID. + * + * @return The suffix. + */ + public String getSuffix() { + return suffix; + } + + /** + * Returns the full PID string in the format /. + * + * @return The full PID. + */ + public String get() { + return String.format("%s/%s", prefix, suffix); + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/Reference.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/valueObjects/Reference.java similarity index 83% rename from src/main/java/edu/kit/datamanager/idoris/domain/entities/Reference.java rename to idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/valueObjects/Reference.java index b16cef5..af9e450 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/Reference.java +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/domain/valueObjects/Reference.java @@ -14,8 +14,8 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.entities; +package edu.kit.datamanager.idoris.core.domain.valueObjects; -public record Reference(String relationType, String targetPID) { -} +public record Reference(PID relationType, PID targetPID) { +} \ No newline at end of file diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/events/AbstractDomainEvent.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/events/AbstractDomainEvent.java new file mode 100644 index 0000000..5edc964 --- /dev/null +++ b/idoris/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/idoris/src/main/java/edu/kit/datamanager/idoris/core/events/DomainEvent.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/events/DomainEvent.java new file mode 100644 index 0000000..e8dc25c --- /dev/null +++ b/idoris/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/idoris/src/main/java/edu/kit/datamanager/idoris/core/events/EntityCreatedEvent.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/events/EntityCreatedEvent.java new file mode 100644 index 0000000..009255f --- /dev/null +++ b/idoris/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.core.domain.AdministrativeMetadata; +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 AdministrativeMetadata + */ +@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; + } +} diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/events/EntityDeletedEvent.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/events/EntityDeletedEvent.java new file mode 100644 index 0000000..fad22c4 --- /dev/null +++ b/idoris/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.core.domain.AdministrativeMetadata; +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 AdministrativeMetadata + */ +@Getter +@ToString(callSuper = true) +public class EntityDeletedEvent extends AbstractDomainEvent { + private final T entity; + private final String entityType; + private final String entityInternalId; + + /** + * 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.entityInternalId = entity.getInternalId(); + } + + /** + * 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 internal ID of the entity that was deleted. + * + * @return the entity internal ID + */ + public String getEntityInternalId() { + return entityInternalId; + } +} diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/events/EntityImportedEvent.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/events/EntityImportedEvent.java new file mode 100644 index 0000000..6bdb74b --- /dev/null +++ b/idoris/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.core.domain.AdministrativeMetadata; +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 AdministrativeMetadata + */ +@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 + } +} diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/events/EntityPatchedEvent.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/events/EntityPatchedEvent.java new file mode 100644 index 0000000..5fada1f --- /dev/null +++ b/idoris/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/idoris/src/main/java/edu/kit/datamanager/idoris/core/events/EntityUpdatedEvent.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/events/EntityUpdatedEvent.java new file mode 100644 index 0000000..96ecc94 --- /dev/null +++ b/idoris/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.core.domain.AdministrativeMetadata; +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 AdministrativeMetadata + */ +@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; + } +} diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/events/EventPublisherService.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/events/EventPublisherService.java new file mode 100644 index 0000000..254c1a8 --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/events/EventPublisherService.java @@ -0,0 +1,152 @@ +/* + * 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 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; + +/** + * Service for publishing domain events. + * This logic wraps Spring's ApplicationEventPublisher to provide a more domain-specific API. + */ +@Service +@Slf4j +@Observed(contextualName = "eventPublisherService") +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 + */ + @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)); + } + + + /** + * 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 + */ + @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)); + } + + /** + * 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 + */ + @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. + * + * @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 + */ + @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)); + } + + /** + * 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 + */ + @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)); + } + + + /** + * Publishes a generic domain event. + * + * @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); + } + + /** + * Publishes an entity deleted event. + * + * @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)); + } +} diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/events/PIDGeneratedEvent.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/events/PIDGeneratedEvent.java new file mode 100644 index 0000000..5bfa456 --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/events/PIDGeneratedEvent.java @@ -0,0 +1,109 @@ +/* + * 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; +import lombok.ToString; + +/** + * 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 ID was generated, must extend AdministrativeMetadata + */ +@Getter +@ToString(callSuper = true) +public class PIDGeneratedEvent extends AbstractDomainEvent { + private final T entity; + 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 ID. + * Assumes that the ID is newly generated. + * + * @param entity the entity for which the ID was generated + * @param id the generated ID + */ + public PIDGeneratedEvent(T entity, String id) { + this(entity, id, true); + } + + /** + * Creates a new PIDGeneratedEvent for the given entity and ID. + * + * @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 id, boolean isNewID) { + this.entity = entity; + this.id = id; + this.isNewID = isNewID; + this.entityInternalId = entity.getInternalId(); + this.entityType = entity.getClass().getSimpleName(); + } + + /** + * Gets the entity for which the ID was generated. + * + * @return the entity + */ + public T getEntity() { + return entity; + } + + /** + * Gets the generated ID. + * + * @return the ID + */ + public String getId() { + return id; + } + + /** + * Indicates whether this is a newly generated ID or an existing one. + * + * @return true if the ID was newly generated, false if it already existed + */ + public boolean isNewID() { + return isNewID; + } + + /** + * Gets the internal ID of the entity for which the ID was generated. + * + * @return the entity internal ID + */ + public String getEntityInternalId() { + return entityInternalId; + } + + /** + * Gets the type of the entity for which the ID was generated. + * + * @return the entity type + */ + public String getEntityType() { + return entityType; + } +} diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/events/SchemaGeneratedEvent.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/events/SchemaGeneratedEvent.java new file mode 100644 index 0000000..1259ce6 --- /dev/null +++ b/idoris/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.core.domain.AdministrativeMetadata; +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 AdministrativeMetadata 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(AdministrativeMetadata 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 AdministrativeMetadata 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; + } +} diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/events/VersionCreatedEvent.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/events/VersionCreatedEvent.java new file mode 100644 index 0000000..5aee872 --- /dev/null +++ b/idoris/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.core.domain.AdministrativeMetadata; +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 AdministrativeMetadata + */ +@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; + } +} diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/exceptions/ValidationException.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/exceptions/ValidationException.java new file mode 100644 index 0000000..3ae3e48 --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/exceptions/ValidationException.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2025-2026 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.exceptions; + +import edu.kit.datamanager.idoris.core.domain.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; + } +} \ No newline at end of file diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/exceptions/ValidationExceptionHandler.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/exceptions/ValidationExceptionHandler.java new file mode 100644 index 0000000..4523eb0 --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/exceptions/ValidationExceptionHandler.java @@ -0,0 +1,35 @@ +/* + * 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.exceptions; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; + +@ControllerAdvice +public class ValidationExceptionHandler { + + @ExceptionHandler(ValidationException.class) + public ProblemDetail handleValidationException(ValidationException ex, WebRequest request) { + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getMessage()); + problemDetail.setTitle("Validation failed"); + problemDetail.setProperty("validationResult", ex.getValidationResult().getOutputMessages()); + return problemDetail; + } +} \ No newline at end of file diff --git a/idoris/src/main/java/edu/kit/datamanager/idoris/core/package-info.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/package-info.java new file mode 100644 index 0000000..fa1b4b3 --- /dev/null +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/package-info.java @@ -0,0 +1,31 @@ +/* + * 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.

    + */ +@ApplicationModule( + displayName = "IDORIS Core", + type = ApplicationModule.Type.OPEN +) +package edu.kit.datamanager.idoris.core; + +import org.springframework.modulith.ApplicationModule; \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleService.java b/idoris/src/main/java/edu/kit/datamanager/idoris/core/services/RuleService.java similarity index 56% rename from src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleService.java rename to idoris/src/main/java/edu/kit/datamanager/idoris/core/services/RuleService.java index d44f4d5..091b492 100644 --- a/src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleService.java +++ b/idoris/src/main/java/edu/kit/datamanager/idoris/core/services/RuleService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Karlsruhe Institute of Technology + * Copyright (c) 2025-2026 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,68 +14,33 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.rules.logic; +package edu.kit.datamanager.idoris.core.services; +import 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.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.annotations.WithSpan; import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.ListableBeanFactory; -import org.springframework.stereotype.Component; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.stereotype.Service; -import java.lang.reflect.Array; +import java.util.Arrays; 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.regex.Matcher; 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 +@Service @Slf4j -public class RuleService { +@Observed(contextualName = "ruleService") +public class RuleService implements IRuleService { /** * Provides access to all Spring-managed beans for rule discovery. @@ -83,8 +48,11 @@ public class RuleService { * Used to selectively load only the rule implementations that are actually referenced * in the precomputed dependency graph, avoiding the instantiation of unused rules. */ - private final ListableBeanFactory beanFactory; +// private final ListableBeanFactory beanFactory; +// +// private final ApplicationContext context; + private final ObjectProvider>> allRules; /** * Thread-safe registry mapping fully qualified class names to rule instances. *

    @@ -105,6 +73,10 @@ public class RuleService { */ private PrecomputedRuleGraph precomputedGraph; + public RuleService(ObjectProvider>> allRules) { + this.allRules = allRules; + } + /** * Initializes the rule engine by loading the precomputed dependency graph and * discovering required rule implementations. @@ -159,6 +131,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 +166,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..."); @@ -212,19 +186,19 @@ private void discoverRequiredRules() { log.debug("Precomputed graph references {} unique rule classes", requiredRuleClasses.size()); // Find and register only the required rule beans - beanFactory.getBeansOfType(IRule.class) - .forEach((beanName, ruleBean) -> { - String className = ruleBean.getClass().getName(); - - // Only register if this rule is referenced in the precomputed graph - if (requiredRuleClasses.containsKey(className)) { - ruleRegistry.put(className, ruleBean); - requiredRuleClasses.put(className, true); // mark as found - log.debug("Registered required rule: {}", className); - } else { - log.debug("Skipping unreferenced rule: {}", className); - } - }); + Objects.requireNonNull(allRules.getIfAvailable()).forEach(rule -> { + String className = rule.getClass().getName() + .replaceAll(Matcher.quoteReplacement("$$.*") + "$", "") + .replaceAll("@.+$", ""); + // Only register if this rule is referenced in the precomputed graph + if (requiredRuleClasses.containsKey(className)) { + ruleRegistry.put(className, rule); + requiredRuleClasses.put(className, true); // mark as found + log.debug("Registered required rule: {}", className); + } else { + log.debug("Skipping unreferenced rule: {}", className); + } + }); // Log any missing rules long missingRules = requiredRuleClasses.values().stream() @@ -249,35 +223,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 @@ -288,8 +244,12 @@ private void discoverRequiredRules() { * @throws RuntimeException if a critical error occurs during rule execution that prevents * completion of the operation */ + @Override + @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 ) { @@ -306,23 +266,35 @@ 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); + } + + @WithSpan(kind = SpanKind.INTERNAL) + @Override + public > R executeSpecificRule(String ruleName, T element, Supplier resultFactory) throws RuntimeException { + return executeRulesSequentially(List.of(ruleName), element, resultFactory); + } + + @WithSpan(kind = SpanKind.INTERNAL) + @Override + public > R executeSpecificRule(IRule rule, T element, Supplier resultFactory) throws RuntimeException { + return executeRule(rule, 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 @@ -336,41 +308,50 @@ public > R executeRules( * @return the merged result of all executed rules * @throws RuntimeException if rule execution fails critically */ - private > R executeRulesInParallel( - List ruleClassNames, + @SuppressWarnings("unchecked") + @WithSpan(kind = SpanKind.INTERNAL) + 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, ruleRegistry.keySet()); + } + 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: *