diff --git a/.gitattributes b/.gitattributes index 790494d..dfe0770 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,2 @@ # Auto detect text files and perform LF normalization * text=auto -neo4j-data//** filter=lfs diff=lfs merge=lfs -text -neo4j-data/** filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 2d693f5..40b5b59 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -32,7 +32,7 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'java', 'javascript' ] + language: [ 'java' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://git.io/codeql-language-support diff --git a/.gitignore b/.gitignore index d6ad0d3..d1a70ec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ .idea/ db/ +neo4j-data/ +neo4j-data-new/ # Ignore Gradle project-specific cache directory nbproject diff --git a/README.md b/README.md index 0eb4ee5..b9b46c6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # IDORIS +[![Build with Gradle](https://github.com/maximiliani/idoris/actions/workflows/gradle.yml/badge.svg)](https://github.com/maximiliani/idoris/actions/workflows/gradle.yml) + IDORIS is an **Integrated Data Type and Operations Registry with Inheritance System**. ## Cloning this repository @@ -24,7 +26,6 @@ The password for the Neo4j user `neo4j` is `superSecret`. You can access the Neo4j browser at http://localhost:7474. ``` - docker run \ -p 7474:7474 -p 7687:7687 \ --name neo4j-apoc-gds-idoris \ @@ -38,15 +39,20 @@ docker run \ -e NEO4J_PLUGINS=\[\"apoc\",\"graph-data-science\"\,\"bloom\"] \ -e NEO4J_dbms_security_procedures_unrestricted=apoc.\\\*,gds.\\\* \ neo4j:5.22 - ``` ## Running IDORIS -Configure the application.properties file to contain the Neo4j API credentials. +You need to have JDK 21 installed on your system. -``` +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 +credentials. + +``` spring.application.name=idoris logging.level.root=INFO spring.neo4j.uri=bolt://localhost:7687 @@ -56,13 +62,12 @@ spring.data.rest.basePath=/api server.port=8095 idoris.validation-level=info idoris.validation-policy=strict - ``` When Neo4j is running, start IDORIS with the following command: ``` - ./gradlew bootRun +``` -``` \ No newline at end of file +You can access the IDORIS API at http://localhost:8095/api. diff --git a/build.gradle b/build.gradle index 46a6cae..8046799 100644 --- a/build.gradle +++ b/build.gradle @@ -1,31 +1,50 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.text.SimpleDateFormat + plugins { id "java" - id 'org.springframework.boot' version '3.2.1' - id 'io.spring.dependency-management' version '1.1.4' - id 'io.freefair.lombok' version '8.4' - id 'io.freefair.maven-publish-java' version '8.4' - id 'org.owasp.dependencycheck' version '9.0.8' - id 'org.asciidoctor.jvm.convert' version '4.0.1' - id 'net.ltgt.errorprone' version '3.1.0' - id 'net.researchgate.release' version '3.0.2' - id 'com.gorylenko.gradle-git-properties' version '2.4.1' - id 'jacoco' + id "org.springframework.boot" version "3.5.0" // 3.5.0-M* + 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 "net.researchgate.release" version "3.1.0" + id "com.gorylenko.gradle-git-properties" version "2.5.0" + id "jacoco" + id "com.github.ben-manes.versions" version "0.52.0" } description = 'IDORIS - An Integrated Data Type and Operations Registry with Inheritance System' group = 'edu.kit.datamanager' -version = '0.0.1-SNAPSHOT' - -println "Running gradle version: $gradle.gradleVersion" -println "Building ${name} version: ${version}" -println "JDK version: ${JavaVersion.current()}" +version = '0.0.2-SNAPSHOT' +// Update source/target compatibility syntax java { - sourceCompatibility = '21' - targetCompatibility = '21' + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } } configurations { +// annotationProcessorPath + compileOnly { extendsFrom annotationProcessor } @@ -34,165 +53,150 @@ configurations { repositories { mavenLocal() mavenCentral() + maven { + url = uri("https://repo.spring.io/milestone") + } } ext { - // versions of dependencies - springDocVersion = '2.3.0' - javersVersion = '7.3.7' - errorproneVersion = '2.24.1' - // directory for generated code snippets during tests - set('snippetsDir', file("build/generated-snippets")) + 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) + set("snippetsDir", file("build/generated-snippets")) } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-actuator' - implementation 'org.springframework.boot:spring-boot-starter-data-neo4j' -// implementation 'org.springframework.boot:spring-boot-starter-graphql' - implementation 'org.springframework.boot:spring-boot-starter-hateoas' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.data:spring-data-rest-hal-explorer' - implementation 'org.springframework.boot:spring-boot-starter-data-rest' -// implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'org.springframework.boot:spring-boot-starter-validation:2.7.3' - implementation 'org.springframework.boot:spring-boot-starter-web:2.7.3' - implementation 'org.springdoc:springdoc-openapi-ui:1.6.11' - implementation 'org.springdoc:springdoc-openapi-data-rest:1.6.11' - implementation 'org.springdoc:springdoc-openapi-webmvc-core:1.6.11' - implementation 'org.apache.httpcomponents:httpclient:4.5.13' -// implementation 'com.netflix.graphql.dgs.codegen:graphql-dgs-codegen-gradle:6.0.3' -// testImplementation 'org.springframework:spring-webflux' - - compileOnly 'org.projectlombok:lombok' - annotationProcessor 'org.projectlombok:lombok' - developmentOnly 'org.springframework.boot:spring-boot-devtools' - - testImplementation 'org.springframework.boot:spring-boot-starter-test' -// testImplementation 'org.springframework.graphql:spring-graphql-test' - testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' - testImplementation 'org.springframework.security:spring-security-test' + /* Spring BOM – drives every spring-boot starter below */ + implementation platform("org.springframework.boot:spring-boot-dependencies:${springBootVersion}") - errorprone "com.google.errorprone:error_prone_core:${errorproneVersion}" -} + /* Rules API */ + implementation project(':rules-api') -compileJava { - options.errorprone.disableWarningsInGeneratedCode = true - options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" << "-Xmaxwarns" << "200" -} + /* Spring Boot starters (version comes from the BOM) */ + implementation "org.springframework.boot:spring-boot-starter-web" + implementation "org.springframework.boot:spring-boot-starter-data-neo4j" + implementation "org.springframework.boot:spring-boot-starter-data-rest" + implementation "org.springframework.boot:spring-boot-starter-actuator" + implementation "org.springframework.boot:spring-boot-starter-hateoas" + implementation "org.springframework.boot:spring-boot-starter-validation" + implementation "org.springframework:spring-web" + implementation "org.springframework.data:spring-data-rest-hal-explorer:5.0.0-M3" -compileTestJava { - // disable errorprone for tests - options.errorprone.disableWarningsInGeneratedCode = true - options.errorprone.enabled = false // change it to true to enable -} + /* 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}" -tasks.named('test') { - outputs.dir snippetsDir - finalizedBy jacocoTestReport - environment "spring.config.location", "optional:classpath:/test-config/" + /* HTTP client */ + implementation "org.apache.httpcomponents.client5:httpclient5:${httpClientVersion}" - maxHeapSize = "8192m" - testLogging { - outputs.upToDateWhen { false } - showStandardStreams = true - } - useJUnitPlatform() -} + /* Development helpers */ + implementation "org.springframework.boot:spring-boot-configuration-processor" + developmentOnly "org.springframework.boot:spring-boot-devtools" -tasks.named('asciidoctor') { - inputs.dir snippetsDir - dependsOn test -} + /* Lombok */ + compileOnly "org.projectlombok:lombok:1.18.38" + annotationProcessor "org.projectlombok:lombok:1.18.38" -test { - finalizedBy jacocoTestReport - environment "spring.config.location", "optional:classpath:/test-config/" + /* JavaX Annotations */ + implementation 'javax.annotation:javax.annotation-api:1.3.2' - maxHeapSize = "8192m" - testLogging { - outputs.upToDateWhen { false } - showStandardStreams = true - } + // Add the processor module + annotationProcessor project(':rules-processor') + + /* Error-prone */ + errorprone "com.google.errorprone:error_prone_core:${errorproneVersion}" + annotationProcessor "com.google.errorprone:error_prone_core:${errorproneVersion}" + + /* Tests */ + testImplementation "org.springframework.boot:spring-boot-starter-test" + testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc:3.0.3" + testImplementation "org.springframework.security:spring-security-test" + testImplementation "org.junit.jupiter:junit-jupiter:5.13.0" } -tasks.withType(Test).configureEach { - testLogging { - events 'started', 'passed' +// Modify JavaCompile tasks configuration +tasks.withType(JavaCompile).configureEach { + if (name.toLowerCase().contains('test')) { + options.errorprone.enabled = false } -} -jacoco { - toolVersion = "0.8.10" + options.compilerArgs += [ + '-Xlint:unchecked', + '-Xlint:deprecation', + '-Xmaxwarns', '200' + ] + + // 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') { - outputs.dir snippetsDir useJUnitPlatform() -} + outputs.dir snippetsDir + finalizedBy tasks.named('jacocoTestReport') -tasks.named('asciidoctor') { - inputs.dir snippetsDir - dependsOn test -} + jvmArgs = ['-Xmx4g'] // Replace maxHeapSize -import java.text.SimpleDateFormat + environment('spring.config.location', 'optional:classpath:/test-config/') -tasks.register('testForSnippetsDir') { - doFirst { - println 'snippetsDir exists: ' + snippetsDir.exists() - if (!snippetsDir.exists()) { - println 'Create snippets dir...' - println 'WARNING: Don\'t skip tests for building production ready jar file!' - snippetsDir.mkdirs() - } + testLogging { + events = ['started', 'passed', 'skipped', 'failed'] + showStandardStreams = true + outputs.upToDateWhen { false } } } -asciidoctor { - dependsOn testForSnippetsDir - attributes "snippets": snippetsDir, - "version": jar.archiveVersion, - "date": new SimpleDateFormat("yyyy-MM-dd").format(new Date()) +tasks.named('asciidoctor') { inputs.dir snippetsDir dependsOn test - sourceDir "docs/" - outputDir "build/docs/html5" - executionMode = JAVA_EXEC + + attributes = [ + 'snippets': snippetsDir, + 'version' : jar.archiveVersion, + 'date' : new SimpleDateFormat("yyyy-MM-dd").format(new Date()) + ] + + sourceDir file("docs/") + outputDir = file("build/generated-docs") + + options doctype: 'book' + forkOptions { - jvmArgs "--add-opens", "java.base/sun.nio.ch=ALL-UNNAMED", "--add-opens", "java.base/java.io=ALL-UNNAMED" + jvmArgs = [ + "--add-opens", "java.base/sun.nio.ch=ALL-UNNAMED", + "--add-opens", "java.base/java.io=ALL-UNNAMED" + ] } } -jar { - // disable plain jar file +// Update jar configuration +tasks.named('jar') { enabled = false } -springBoot { - buildInfo() -} - +// Update bootJar configuration bootJar { - println 'Create bootable jar...' - duplicatesStrategy = DuplicatesStrategy.EXCLUDE manifest { - attributes 'Main-Class': 'org.springframework.boot.loader.launch.PropertiesLauncher' + attributes('Main-Class': 'org.springframework.boot.loader.launch.PropertiesLauncher') } dependsOn asciidoctor from("${asciidoctor.outputDir}") { into 'static/docs' } launchScript() -} - -release { - tagTemplate = 'v${version}' -} - -// task for printing project name. -tasks.register('printProjectName') { - doLast { - println "${project.name}" - } } \ No newline at end of file diff --git a/gradle/gradle.properties b/gradle/gradle.properties new file mode 100644 index 0000000..a3c114a --- /dev/null +++ b/gradle/gradle.properties @@ -0,0 +1,17 @@ +# +# 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.gradle.caching=true +org.gradle.parallel=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b82aa23..1819666 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,22 @@ +# +# Copyright (c) 2025 Karlsruhe Institute of Technology +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/neo4j-data/databases/neo4j/neostore b/neo4j-data/databases/neo4j/neostore deleted file mode 100644 index 8ec2561..0000000 --- a/neo4j-data/databases/neo4j/neostore +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4b0c121e1f02e4a37b2065ef143ffb4cc37810fed816ee8d7f6fc6ad5835908e -size 8192 diff --git a/neo4j-data/databases/neo4j/neostore.counts.db b/neo4j-data/databases/neo4j/neostore.counts.db deleted file mode 100644 index 1dc50f9..0000000 --- a/neo4j-data/databases/neo4j/neostore.counts.db +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8028007971571d558fb35e815b5b5b890dd20e1067e735a0959b810606bb40e7 -size 49152 diff --git a/neo4j-data/databases/neo4j/neostore.indexstats.db b/neo4j-data/databases/neo4j/neostore.indexstats.db deleted file mode 100644 index 58e8008..0000000 --- a/neo4j-data/databases/neo4j/neostore.indexstats.db +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5e2d6130ce0b5cf47c2fdfca00278a2894324de061f6c3f35e2d12a462804917 -size 49152 diff --git a/neo4j-data/databases/neo4j/neostore.labeltokenstore.db b/neo4j-data/databases/neo4j/neostore.labeltokenstore.db deleted file mode 100644 index ae8ff25..0000000 --- a/neo4j-data/databases/neo4j/neostore.labeltokenstore.db +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9153f4f419730b55c593e66833a0b6b066ad861655b0312680d24a3b75a38c29 -size 8192 diff --git a/neo4j-data/databases/neo4j/neostore.labeltokenstore.db.id b/neo4j-data/databases/neo4j/neostore.labeltokenstore.db.id deleted file mode 100644 index 4a0dfec..0000000 --- a/neo4j-data/databases/neo4j/neostore.labeltokenstore.db.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ec341d26a2a0f9e0da4e580c140a2fb49016049adf6837dd9d4c04b45bee1412 -size 40960 diff --git a/neo4j-data/databases/neo4j/neostore.labeltokenstore.db.names b/neo4j-data/databases/neo4j/neostore.labeltokenstore.db.names deleted file mode 100644 index ceb94dc..0000000 --- a/neo4j-data/databases/neo4j/neostore.labeltokenstore.db.names +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:66e18bd17ce21ad061721dab75b1d19f2b04350d09c07ebda325fba0d7a2952b -size 8192 diff --git a/neo4j-data/databases/neo4j/neostore.labeltokenstore.db.names.id b/neo4j-data/databases/neo4j/neostore.labeltokenstore.db.names.id deleted file mode 100644 index 1100862..0000000 --- a/neo4j-data/databases/neo4j/neostore.labeltokenstore.db.names.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e4df5e6ad667ca32302c8cf44ac6b3a2852ee62822808f1a6674379a71d04046 -size 40960 diff --git a/neo4j-data/databases/neo4j/neostore.nodestore.db b/neo4j-data/databases/neo4j/neostore.nodestore.db deleted file mode 100644 index 14b7246..0000000 --- a/neo4j-data/databases/neo4j/neostore.nodestore.db +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b7b1bc06e759c4bdc427829208de883022ead4db4675ce8536653d5eeecfaf93 -size 8192 diff --git a/neo4j-data/databases/neo4j/neostore.nodestore.db.id b/neo4j-data/databases/neo4j/neostore.nodestore.db.id deleted file mode 100644 index 6fffe0e..0000000 --- a/neo4j-data/databases/neo4j/neostore.nodestore.db.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:27fb89a56efb50d0aeb8e185f7cfbb501e658e1df18d60788560c9048797f43e -size 49152 diff --git a/neo4j-data/databases/neo4j/neostore.nodestore.db.labels b/neo4j-data/databases/neo4j/neostore.nodestore.db.labels deleted file mode 100644 index 2476cf2..0000000 --- a/neo4j-data/databases/neo4j/neostore.nodestore.db.labels +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7f2042493c9cce61a88d11b0b905b57129b31e45821a104c3dc0536cee030a7c -size 8192 diff --git a/neo4j-data/databases/neo4j/neostore.nodestore.db.labels.id b/neo4j-data/databases/neo4j/neostore.nodestore.db.labels.id deleted file mode 100644 index 0f41e34..0000000 --- a/neo4j-data/databases/neo4j/neostore.nodestore.db.labels.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:139ecd3d2f5549d747f0844de8944250718e4814f8810f407cc8efbe2034cfab -size 40960 diff --git a/neo4j-data/databases/neo4j/neostore.propertystore.db b/neo4j-data/databases/neo4j/neostore.propertystore.db deleted file mode 100644 index 294c762..0000000 --- a/neo4j-data/databases/neo4j/neostore.propertystore.db +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e4c6dfeb0d5be00fdfcd51fda9e2f5de7c38cc4a21d11392ff21879e1666d27e -size 16384 diff --git a/neo4j-data/databases/neo4j/neostore.propertystore.db.arrays b/neo4j-data/databases/neo4j/neostore.propertystore.db.arrays deleted file mode 100644 index d337351..0000000 --- a/neo4j-data/databases/neo4j/neostore.propertystore.db.arrays +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b81417dc15666013a02b3c6ac5690ac59852f0142fdbcdcc9f164cc7bc6a4419 -size 8192 diff --git a/neo4j-data/databases/neo4j/neostore.propertystore.db.arrays.id b/neo4j-data/databases/neo4j/neostore.propertystore.db.arrays.id deleted file mode 100644 index 919c93b..0000000 --- a/neo4j-data/databases/neo4j/neostore.propertystore.db.arrays.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:33744838c4f00f522c2eabfb97be35484d46458d13af5288fd3004414db4347a -size 40960 diff --git a/neo4j-data/databases/neo4j/neostore.propertystore.db.id b/neo4j-data/databases/neo4j/neostore.propertystore.db.id deleted file mode 100644 index 7652c16..0000000 --- a/neo4j-data/databases/neo4j/neostore.propertystore.db.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:65199501403ce14c779c4d36b2cc63034566303e7623d39df1f98c724c0e096d -size 49152 diff --git a/neo4j-data/databases/neo4j/neostore.propertystore.db.index b/neo4j-data/databases/neo4j/neostore.propertystore.db.index deleted file mode 100644 index f386aca..0000000 --- a/neo4j-data/databases/neo4j/neostore.propertystore.db.index +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:779dde76879cd524c9f054ba051943de111b3ef7397992409e6b036485568d1b -size 8192 diff --git a/neo4j-data/databases/neo4j/neostore.propertystore.db.index.id b/neo4j-data/databases/neo4j/neostore.propertystore.db.index.id deleted file mode 100644 index e95a305..0000000 --- a/neo4j-data/databases/neo4j/neostore.propertystore.db.index.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:472e5cef620ff975d1b78664f7be95a2587a0d557c75c8207f79de00e1765fe3 -size 49152 diff --git a/neo4j-data/databases/neo4j/neostore.propertystore.db.index.keys b/neo4j-data/databases/neo4j/neostore.propertystore.db.index.keys deleted file mode 100644 index 19eef8c..0000000 --- a/neo4j-data/databases/neo4j/neostore.propertystore.db.index.keys +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:13dfdbc4bfbc4881bd09ba428240b0606137130fc6843e349c1c3c2e5bde23f0 -size 8192 diff --git a/neo4j-data/databases/neo4j/neostore.propertystore.db.index.keys.id b/neo4j-data/databases/neo4j/neostore.propertystore.db.index.keys.id deleted file mode 100644 index 4cb24da..0000000 --- a/neo4j-data/databases/neo4j/neostore.propertystore.db.index.keys.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:50ecca4e215be21649b0a3458df66909f55d1675713e1b301f36ec0c2a4333e1 -size 40960 diff --git a/neo4j-data/databases/neo4j/neostore.propertystore.db.strings b/neo4j-data/databases/neo4j/neostore.propertystore.db.strings deleted file mode 100644 index dfa6c99..0000000 --- a/neo4j-data/databases/neo4j/neostore.propertystore.db.strings +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6ffb2ba489b75b274a477f292542cb314eea88a8168b5dcdbd5eb5dbc172df3f -size 8192 diff --git a/neo4j-data/databases/neo4j/neostore.propertystore.db.strings.id b/neo4j-data/databases/neo4j/neostore.propertystore.db.strings.id deleted file mode 100644 index a14cf2d..0000000 --- a/neo4j-data/databases/neo4j/neostore.propertystore.db.strings.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7c140bb4ba83b07b0f9f0396959e4a49b29d698c3d1f6fed4d9f6c52a0dca54e -size 49152 diff --git a/neo4j-data/databases/neo4j/neostore.relationshipgroupstore.db b/neo4j-data/databases/neo4j/neostore.relationshipgroupstore.db deleted file mode 100644 index 7d75db0..0000000 --- a/neo4j-data/databases/neo4j/neostore.relationshipgroupstore.db +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:630313872cd3b597d34cb2d5699058a9959a9a6bf5ee97685e32b2b5d14830a8 -size 8192 diff --git a/neo4j-data/databases/neo4j/neostore.relationshipgroupstore.db.id b/neo4j-data/databases/neo4j/neostore.relationshipgroupstore.db.id deleted file mode 100644 index 0f41e34..0000000 --- a/neo4j-data/databases/neo4j/neostore.relationshipgroupstore.db.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:139ecd3d2f5549d747f0844de8944250718e4814f8810f407cc8efbe2034cfab -size 40960 diff --git a/neo4j-data/databases/neo4j/neostore.relationshipgroupstore.degrees.db b/neo4j-data/databases/neo4j/neostore.relationshipgroupstore.degrees.db deleted file mode 100644 index 787c6f3..0000000 --- a/neo4j-data/databases/neo4j/neostore.relationshipgroupstore.degrees.db +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d27a270512537321e425b55540bbba3ab53e8dc2ac135c63459a56cb34801d8b -size 40960 diff --git a/neo4j-data/databases/neo4j/neostore.relationshipstore.db b/neo4j-data/databases/neo4j/neostore.relationshipstore.db deleted file mode 100644 index 2620d67..0000000 --- a/neo4j-data/databases/neo4j/neostore.relationshipstore.db +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d468a3fb1bcc75cf3523aaf941ba308f2398f9bcb957b73286b56e8c8918bf02 -size 8192 diff --git a/neo4j-data/databases/neo4j/neostore.relationshipstore.db.id b/neo4j-data/databases/neo4j/neostore.relationshipstore.db.id deleted file mode 100644 index ea64323..0000000 --- a/neo4j-data/databases/neo4j/neostore.relationshipstore.db.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b5688cc7c9db2242b21d86c3a79c1c629e47111d093a0c544f692b75cf93b90d -size 49152 diff --git a/neo4j-data/databases/neo4j/neostore.relationshiptypestore.db b/neo4j-data/databases/neo4j/neostore.relationshiptypestore.db deleted file mode 100644 index cc88536..0000000 --- a/neo4j-data/databases/neo4j/neostore.relationshiptypestore.db +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1a811ef7f382291570bcfc69038288b47093d75bc1817fdcad5d28494edf4e3b -size 8192 diff --git a/neo4j-data/databases/neo4j/neostore.relationshiptypestore.db.id b/neo4j-data/databases/neo4j/neostore.relationshiptypestore.db.id deleted file mode 100644 index 6ea5160..0000000 --- a/neo4j-data/databases/neo4j/neostore.relationshiptypestore.db.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d4caa6fac048275dfb5912a04b88388c813c9171f345c52cc20881bf9d46037e -size 40960 diff --git a/neo4j-data/databases/neo4j/neostore.relationshiptypestore.db.names b/neo4j-data/databases/neo4j/neostore.relationshiptypestore.db.names deleted file mode 100644 index 18b9639..0000000 --- a/neo4j-data/databases/neo4j/neostore.relationshiptypestore.db.names +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:019e9420a9994d24e397a802477f13cbb6c825e7545ef4ad7eaa57d5ca8c7282 -size 8192 diff --git a/neo4j-data/databases/neo4j/neostore.relationshiptypestore.db.names.id b/neo4j-data/databases/neo4j/neostore.relationshiptypestore.db.names.id deleted file mode 100644 index b138a1f..0000000 --- a/neo4j-data/databases/neo4j/neostore.relationshiptypestore.db.names.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1e2cd4ec430a62a38f82cc314e88a27d5c748d5ef0461ea8889d470d9010b0fa -size 40960 diff --git a/neo4j-data/databases/neo4j/neostore.schemastore.db b/neo4j-data/databases/neo4j/neostore.schemastore.db deleted file mode 100644 index ab09c85..0000000 --- a/neo4j-data/databases/neo4j/neostore.schemastore.db +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f11940a784ea62b1b8fe199231ccd3e24a6acae1176ef1f59cd74c4f7d4ecc23 -size 8192 diff --git a/neo4j-data/databases/neo4j/neostore.schemastore.db.id b/neo4j-data/databases/neo4j/neostore.schemastore.db.id deleted file mode 100644 index af9ee44..0000000 --- a/neo4j-data/databases/neo4j/neostore.schemastore.db.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f7239d47b570b4502bfd330dc2a0179145db77edce7527e3959fd2cc9278e130 -size 40960 diff --git a/neo4j-data/databases/neo4j/schema/index/token-lookup-1.0/1/index-1 b/neo4j-data/databases/neo4j/schema/index/token-lookup-1.0/1/index-1 deleted file mode 100644 index 20e3628..0000000 --- a/neo4j-data/databases/neo4j/schema/index/token-lookup-1.0/1/index-1 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1fe2d4b5adbc254c5460ece3af541a12adeca1acfcb2cd425a42764a96cf8bef -size 49152 diff --git a/neo4j-data/databases/neo4j/schema/index/token-lookup-1.0/2/index-2 b/neo4j-data/databases/neo4j/schema/index/token-lookup-1.0/2/index-2 deleted file mode 100644 index 25ca1a8..0000000 --- a/neo4j-data/databases/neo4j/schema/index/token-lookup-1.0/2/index-2 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4eb1974b196e48f44e770e04981168f1f82ff076e95b48e75c6c54ec6f7fa8fd -size 49152 diff --git a/neo4j-data/databases/system/neostore b/neo4j-data/databases/system/neostore deleted file mode 100644 index 2268ce5..0000000 --- a/neo4j-data/databases/system/neostore +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6dbdc27aa14bb07dbda0f2d0915dae01fa2cdc205efcd568cac520f477e52613 -size 8192 diff --git a/neo4j-data/databases/system/neostore.counts.db b/neo4j-data/databases/system/neostore.counts.db deleted file mode 100644 index aab9002..0000000 --- a/neo4j-data/databases/system/neostore.counts.db +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1152dd589b06d1a5c57076f1a498ec6ef4850b4d511b645965860f9622243c3c -size 49152 diff --git a/neo4j-data/databases/system/neostore.indexstats.db b/neo4j-data/databases/system/neostore.indexstats.db deleted file mode 100644 index 65e0fb1..0000000 --- a/neo4j-data/databases/system/neostore.indexstats.db +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e61af55681ef40ef9252c0fcc9223acabf928785f79aeae6657a638f51bc56d2 -size 49152 diff --git a/neo4j-data/databases/system/neostore.labeltokenstore.db b/neo4j-data/databases/system/neostore.labeltokenstore.db deleted file mode 100644 index 31d3883..0000000 --- a/neo4j-data/databases/system/neostore.labeltokenstore.db +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c0f52d05276a925adcbe2d66d36b79f6eac57f822426276aed67fd8d2fedce52 -size 8192 diff --git a/neo4j-data/databases/system/neostore.labeltokenstore.db.id b/neo4j-data/databases/system/neostore.labeltokenstore.db.id deleted file mode 100644 index 86e1f3a..0000000 --- a/neo4j-data/databases/system/neostore.labeltokenstore.db.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9fc37cda86525acc0c1a9ba52430c31b463d3af4b70b7148894cd424db00eda5 -size 40960 diff --git a/neo4j-data/databases/system/neostore.labeltokenstore.db.names b/neo4j-data/databases/system/neostore.labeltokenstore.db.names deleted file mode 100644 index 7e3f25c..0000000 --- a/neo4j-data/databases/system/neostore.labeltokenstore.db.names +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:94e4c2dbf7cd298ab058c61ca9d27a381254363f6aa28923d7f1e7c12a70338a -size 8192 diff --git a/neo4j-data/databases/system/neostore.labeltokenstore.db.names.id b/neo4j-data/databases/system/neostore.labeltokenstore.db.names.id deleted file mode 100644 index 8680c25..0000000 --- a/neo4j-data/databases/system/neostore.labeltokenstore.db.names.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9d167de504dfaaa56ddc2086e11cda61fcb5e3e64eac1672cd590a994b08d0f7 -size 40960 diff --git a/neo4j-data/databases/system/neostore.nodestore.db b/neo4j-data/databases/system/neostore.nodestore.db deleted file mode 100644 index e8652b2..0000000 --- a/neo4j-data/databases/system/neostore.nodestore.db +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:caf8b489c5872d6762db0af0042930208bda38bc94f410a4e289db01df2d4aef -size 8192 diff --git a/neo4j-data/databases/system/neostore.nodestore.db.id b/neo4j-data/databases/system/neostore.nodestore.db.id deleted file mode 100644 index 498bddc..0000000 --- a/neo4j-data/databases/system/neostore.nodestore.db.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:559013c4634d634a1ab3e5aa8b257424df478aae7ddd10c52f5c8389e088eb7e -size 40960 diff --git a/neo4j-data/databases/system/neostore.nodestore.db.labels b/neo4j-data/databases/system/neostore.nodestore.db.labels deleted file mode 100644 index 2476cf2..0000000 --- a/neo4j-data/databases/system/neostore.nodestore.db.labels +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7f2042493c9cce61a88d11b0b905b57129b31e45821a104c3dc0536cee030a7c -size 8192 diff --git a/neo4j-data/databases/system/neostore.nodestore.db.labels.id b/neo4j-data/databases/system/neostore.nodestore.db.labels.id deleted file mode 100644 index 02b1226..0000000 --- a/neo4j-data/databases/system/neostore.nodestore.db.labels.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:90414ad38927345e7f3e824bc7bfc93e1f0573184e36487919ed3c2b339f29d1 -size 40960 diff --git a/neo4j-data/databases/system/neostore.propertystore.db b/neo4j-data/databases/system/neostore.propertystore.db deleted file mode 100644 index e1f75c9..0000000 --- a/neo4j-data/databases/system/neostore.propertystore.db +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f93bdf36ecbefd07966ae8fb4896eda80bef946533c683f1ad5841c24e980dcd -size 8192 diff --git a/neo4j-data/databases/system/neostore.propertystore.db.arrays b/neo4j-data/databases/system/neostore.propertystore.db.arrays deleted file mode 100644 index b248d05..0000000 --- a/neo4j-data/databases/system/neostore.propertystore.db.arrays +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:de9503376631524967b116f3d144a869a4ba5867f16b64a6869535c589434316 -size 8192 diff --git a/neo4j-data/databases/system/neostore.propertystore.db.arrays.id b/neo4j-data/databases/system/neostore.propertystore.db.arrays.id deleted file mode 100644 index 02b1226..0000000 --- a/neo4j-data/databases/system/neostore.propertystore.db.arrays.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:90414ad38927345e7f3e824bc7bfc93e1f0573184e36487919ed3c2b339f29d1 -size 40960 diff --git a/neo4j-data/databases/system/neostore.propertystore.db.id b/neo4j-data/databases/system/neostore.propertystore.db.id deleted file mode 100644 index 2086c51..0000000 --- a/neo4j-data/databases/system/neostore.propertystore.db.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d1a8cc1dc09598a74e6b6aefdf87a5b842c11bc28f51ea2d83dc88208a0fb6cf -size 40960 diff --git a/neo4j-data/databases/system/neostore.propertystore.db.index b/neo4j-data/databases/system/neostore.propertystore.db.index deleted file mode 100644 index aabbb75..0000000 --- a/neo4j-data/databases/system/neostore.propertystore.db.index +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c43b8c1d2eff89a707ad303d689edb4c6ac5126f19186bc5e28cd7bf4a24b802 -size 8192 diff --git a/neo4j-data/databases/system/neostore.propertystore.db.index.id b/neo4j-data/databases/system/neostore.propertystore.db.index.id deleted file mode 100644 index f7d29ad..0000000 --- a/neo4j-data/databases/system/neostore.propertystore.db.index.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9b31a9c302c76d5d9c68c9f64243af0565a9e8f0ee9edf07ccb36db4cb05198b -size 40960 diff --git a/neo4j-data/databases/system/neostore.propertystore.db.index.keys b/neo4j-data/databases/system/neostore.propertystore.db.index.keys deleted file mode 100644 index 2f1b095..0000000 --- a/neo4j-data/databases/system/neostore.propertystore.db.index.keys +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:72d3d2dd7c66bcca27459c7bba4595a67705c5ad8b40bbc8145194fdf76575fc -size 8192 diff --git a/neo4j-data/databases/system/neostore.propertystore.db.index.keys.id b/neo4j-data/databases/system/neostore.propertystore.db.index.keys.id deleted file mode 100644 index c819e78..0000000 --- a/neo4j-data/databases/system/neostore.propertystore.db.index.keys.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1214a40d3da37d4a3fcff01b58bcdef09a46a182d7484826edcc35127f94d715 -size 40960 diff --git a/neo4j-data/databases/system/neostore.propertystore.db.strings b/neo4j-data/databases/system/neostore.propertystore.db.strings deleted file mode 100644 index 31fa632..0000000 --- a/neo4j-data/databases/system/neostore.propertystore.db.strings +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:80ea4cad37683975236a8d996447852d7c08201add2789295635b9efea0a4769 -size 8192 diff --git a/neo4j-data/databases/system/neostore.propertystore.db.strings.id b/neo4j-data/databases/system/neostore.propertystore.db.strings.id deleted file mode 100644 index fe89e58..0000000 --- a/neo4j-data/databases/system/neostore.propertystore.db.strings.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:de72e5b73cc13550288fcdca9809410ec06e7fea70ab956ca7058d12a00c6d3c -size 40960 diff --git a/neo4j-data/databases/system/neostore.relationshipgroupstore.db b/neo4j-data/databases/system/neostore.relationshipgroupstore.db deleted file mode 100644 index 7d75db0..0000000 --- a/neo4j-data/databases/system/neostore.relationshipgroupstore.db +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:630313872cd3b597d34cb2d5699058a9959a9a6bf5ee97685e32b2b5d14830a8 -size 8192 diff --git a/neo4j-data/databases/system/neostore.relationshipgroupstore.db.id b/neo4j-data/databases/system/neostore.relationshipgroupstore.db.id deleted file mode 100644 index 02b1226..0000000 --- a/neo4j-data/databases/system/neostore.relationshipgroupstore.db.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:90414ad38927345e7f3e824bc7bfc93e1f0573184e36487919ed3c2b339f29d1 -size 40960 diff --git a/neo4j-data/databases/system/neostore.relationshipgroupstore.degrees.db b/neo4j-data/databases/system/neostore.relationshipgroupstore.degrees.db deleted file mode 100644 index 76bc156..0000000 --- a/neo4j-data/databases/system/neostore.relationshipgroupstore.degrees.db +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:246db5ee023319de567f63e1bf5b6f790545e099d264beb9b2dfb4daedb5bdc7 -size 40960 diff --git a/neo4j-data/databases/system/neostore.relationshipstore.db b/neo4j-data/databases/system/neostore.relationshipstore.db deleted file mode 100644 index d46cd72..0000000 --- a/neo4j-data/databases/system/neostore.relationshipstore.db +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c058a0773a317ba91efdc15761d979f07bae3789d99b3fd32d505d77ca61f4a8 -size 8192 diff --git a/neo4j-data/databases/system/neostore.relationshipstore.db.id b/neo4j-data/databases/system/neostore.relationshipstore.db.id deleted file mode 100644 index fe89e58..0000000 --- a/neo4j-data/databases/system/neostore.relationshipstore.db.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:de72e5b73cc13550288fcdca9809410ec06e7fea70ab956ca7058d12a00c6d3c -size 40960 diff --git a/neo4j-data/databases/system/neostore.relationshiptypestore.db b/neo4j-data/databases/system/neostore.relationshiptypestore.db deleted file mode 100644 index 1e9ea03..0000000 --- a/neo4j-data/databases/system/neostore.relationshiptypestore.db +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:50e9f532825f44719a33fc017e82487c1b7d0adf5872ac4d2d0fc18b5d3a5cd8 -size 8192 diff --git a/neo4j-data/databases/system/neostore.relationshiptypestore.db.id b/neo4j-data/databases/system/neostore.relationshiptypestore.db.id deleted file mode 100644 index 4332494..0000000 --- a/neo4j-data/databases/system/neostore.relationshiptypestore.db.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:93f409955aaeffb6e2ccd6d1eb44ee220829b1ec6c772bbf035fbb19fdf3feeb -size 40960 diff --git a/neo4j-data/databases/system/neostore.relationshiptypestore.db.names b/neo4j-data/databases/system/neostore.relationshiptypestore.db.names deleted file mode 100644 index a62599b..0000000 --- a/neo4j-data/databases/system/neostore.relationshiptypestore.db.names +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:47f0c4417e2acb24e7505c269f05666c975bebf9a649f57a46d7a18c47e62f9e -size 8192 diff --git a/neo4j-data/databases/system/neostore.relationshiptypestore.db.names.id b/neo4j-data/databases/system/neostore.relationshiptypestore.db.names.id deleted file mode 100644 index fe89e58..0000000 --- a/neo4j-data/databases/system/neostore.relationshiptypestore.db.names.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:de72e5b73cc13550288fcdca9809410ec06e7fea70ab956ca7058d12a00c6d3c -size 40960 diff --git a/neo4j-data/databases/system/neostore.schemastore.db b/neo4j-data/databases/system/neostore.schemastore.db deleted file mode 100644 index 2c74800..0000000 --- a/neo4j-data/databases/system/neostore.schemastore.db +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8c58606b17311d82a7b546136fecf41ca0dc65fdd97c4928001c6ea5c542b9c5 -size 8192 diff --git a/neo4j-data/databases/system/neostore.schemastore.db.id b/neo4j-data/databases/system/neostore.schemastore.db.id deleted file mode 100644 index 0bfa172..0000000 --- a/neo4j-data/databases/system/neostore.schemastore.db.id +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ca6be1c974916f22defb666363937278d14be0213b9ab9175a714e4f094e7dc7 -size 40960 diff --git a/neo4j-data/databases/system/schema/index/range-1.0/3/index-3 b/neo4j-data/databases/system/schema/index/range-1.0/3/index-3 deleted file mode 100644 index 1d399df..0000000 --- a/neo4j-data/databases/system/schema/index/range-1.0/3/index-3 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e4b37f2edaacd338bc7bbe81f2e1360300fba971edd7e61dd443634591323663 -size 49152 diff --git a/neo4j-data/databases/system/schema/index/range-1.0/4/index-4 b/neo4j-data/databases/system/schema/index/range-1.0/4/index-4 deleted file mode 100644 index d4d7842..0000000 --- a/neo4j-data/databases/system/schema/index/range-1.0/4/index-4 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ac229680d8696a015b832252a59515af3772e01a495d744fd0c83444ff1b8b8c -size 49152 diff --git a/neo4j-data/databases/system/schema/index/range-1.0/7/index-7 b/neo4j-data/databases/system/schema/index/range-1.0/7/index-7 deleted file mode 100644 index 36f13d6..0000000 --- a/neo4j-data/databases/system/schema/index/range-1.0/7/index-7 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4ff7fb64e8d8ea17a19c9a6c4cd7d04351cb5857c0c5bf39d72a153d4c41f397 -size 49152 diff --git a/neo4j-data/databases/system/schema/index/range-1.0/8/index-8 b/neo4j-data/databases/system/schema/index/range-1.0/8/index-8 deleted file mode 100644 index 05b6623..0000000 --- a/neo4j-data/databases/system/schema/index/range-1.0/8/index-8 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c6cdfdd4e8f03af474951aa927fc1a81e9c926373e757d39cee03ae70ae9d0e4 -size 49152 diff --git a/neo4j-data/databases/system/schema/index/range-1.0/9/index-9 b/neo4j-data/databases/system/schema/index/range-1.0/9/index-9 deleted file mode 100644 index e2e251f..0000000 --- a/neo4j-data/databases/system/schema/index/range-1.0/9/index-9 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c29bace62d430fac7e0b5123c1fa33d46629bb9dde63d5e284ad4c0e28f31283 -size 49152 diff --git a/neo4j-data/databases/system/schema/index/token-lookup-1.0/1/index-1 b/neo4j-data/databases/system/schema/index/token-lookup-1.0/1/index-1 deleted file mode 100644 index 32b0388..0000000 --- a/neo4j-data/databases/system/schema/index/token-lookup-1.0/1/index-1 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a454b86bdf2400d89c501e690a034a10db4500d3b376a05c7204844d349dc174 -size 49152 diff --git a/neo4j-data/databases/system/schema/index/token-lookup-1.0/2/index-2 b/neo4j-data/databases/system/schema/index/token-lookup-1.0/2/index-2 deleted file mode 100644 index ebe8722..0000000 --- a/neo4j-data/databases/system/schema/index/token-lookup-1.0/2/index-2 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b81ad90f638c566eb0600ce4547982b0837d2f7b266a85986985f621b0045709 -size 49152 diff --git a/neo4j-data/transactions/neo4j/checkpoint.0 b/neo4j-data/transactions/neo4j/checkpoint.0 deleted file mode 100644 index 3c88d1b..0000000 --- a/neo4j-data/transactions/neo4j/checkpoint.0 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c113bfcba9d52ca3946bac6e3155298c191c9d727c69d82d25ba4da07a84f4e8 -size 1048576 diff --git a/neo4j-data/transactions/neo4j/neostore.transaction.db.0 b/neo4j-data/transactions/neo4j/neostore.transaction.db.0 deleted file mode 100644 index a0f4a71..0000000 --- a/neo4j-data/transactions/neo4j/neostore.transaction.db.0 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b531cec58e6b38c0c74b5f3492de3b63bbb549593deb2448b6efa466eed6bb13 -size 268435456 diff --git a/neo4j-data/transactions/system/checkpoint.0 b/neo4j-data/transactions/system/checkpoint.0 deleted file mode 100644 index 914832e..0000000 --- a/neo4j-data/transactions/system/checkpoint.0 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c4d56814147d05a2b93c4b30aebd0058e5136675ed9c2c45768f0c3c09567a59 -size 1048576 diff --git a/neo4j-data/transactions/system/neostore.transaction.db.0 b/neo4j-data/transactions/system/neostore.transaction.db.0 deleted file mode 100644 index 5bbc761..0000000 --- a/neo4j-data/transactions/system/neostore.transaction.db.0 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7f520a892c832560cf69ec7608b5ce988f34b9bcf5763212bf17be989506c758 -size 268435456 diff --git a/openapi.json b/openapi.json new file mode 100644 index 0000000..3b4c1d2 --- /dev/null +++ b/openapi.json @@ -0,0 +1,655 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Typed PID Maker - RESTful API", + "description": "The Typed PID Maker is a service for creating, updating, obtaining and validating PID record information using Kernel Information Profiles, as defined by the Research Data Alliance.", + "contact": { + "name": "KIT Data Manager Support", + "url": "https://github.com/kit-data-manager", + "email": "support@datamanager.kit.edu" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "2.0.0" + }, + "servers": [ + { + "url": "http://localhost:8090", + "description": "Generated server url" + } + ], + "paths": { + "/api/v1/pit/pid/**": { + "get": { + "tags": [ + "typing-rest-resource-impl" + ], + "summary": "Get the record of the given PID.", + "description": "Get the record to the given PID, if it exists. No validation is performed by default.", + "operationId": "getRecord", + "parameters": [ + { + "name": "validation", + "in": "query", + "description": "If true, validation will be run on the resolved PID. On failure, an error will be returned. On success, the PID will be resolved.", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "400": { + "description": "Validation failed. See body for details.", + "content": { + "application/json": {} + } + }, + "500": { + "description": "Server error. See body for details.", + "content": { + "application/json": {} + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": {} + } + }, + "503": { + "description": "Communication to required external service failed.", + "content": { + "application/json": {} + } + }, + "200": { + "description": "Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PIDRecord" + } + }, + "application/vnd.datamanager.pid.simple+json": { + "schema": { + "$ref": "#/components/schemas/SimplePidRecord" + } + } + } + } + } + }, + "put": { + "tags": [ + "typing-rest-resource-impl" + ], + "summary": "Update an existing PID record", + "description": "Update an existing PID record using the record information from the request body.", + "operationId": "updatePID", + "requestBody": { + "description": "The body containing all PID record values as they should be after the update.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PIDRecord" + } + }, + "application/vnd.datamanager.pid.simple+json": { + "schema": { + "$ref": "#/components/schemas/SimplePidRecord" + } + } + }, + "required": true + }, + "responses": { + "400": { + "description": "Validation failed. See body for details.", + "content": { + "application/json": {} + } + }, + "406": { + "description": "Provided input is invalid with regard to the supported accept header (Not acceptable)", + "content": { + "application/json": {} + } + }, + "500": { + "description": "Server error. See body for details.", + "content": { + "application/json": {} + } + }, + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PIDRecord" + } + }, + "application/vnd.datamanager.pid.simple+json": { + "schema": { + "$ref": "#/components/schemas/SimplePidRecord" + } + } + } + }, + "503": { + "description": "Communication to required external service failed.", + "content": { + "application/json": {} + } + }, + "415": { + "description": "Provided input is invalid with regard to the supported content types. (Unsupported Mediatype)", + "content": { + "application/json": {} + } + }, + "428": { + "description": "No ETag given in If-Match header (Precondition required)", + "content": { + "application/json": {} + } + }, + "412": { + "description": "ETag comparison failed (Precondition failed)", + "content": { + "application/json": {} + } + } + } + } + }, + "/api/v1/search": { + "post": { + "tags": [ + "search-controller" + ], + "summary": "Search for resources.", + "description": "Search for resources using the configured Elastic backend. This endpoint serves as direct proxy to the RESTful endpoint of Elastic. In the body, a query document following the Elastic query format has to be provided. Format errors are returned directly from Elastic. This endpoint also supports authentication and authorization. User information obtained via JWT is applied to the provided query as post filter. If a post filter was already provided with the query it will be replaced. Furthermore, this endpoint supports pagination. 'page' and 'size' query parameters are translated into the Elastic attributes 'from' and 'size' automatically, if not already provided within the query by the caller.", + "operationId": "search", + "parameters": [ + { + "name": "proxy", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/ProxyExchangeByte[]" + } + }, + { + "name": "page", + "in": "query", + "description": "Zero-based page index (0..N)", + "schema": { + "type": "integer", + "default": 0 + } + }, + { + "name": "size", + "in": "query", + "description": "The size of the page to be returned", + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "sort", + "in": "query", + "description": "Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JsonNode" + } + } + }, + "required": true + }, + "responses": { + "400": { + "description": "Bad Request", + "content": { + "application/hal+json": { + "schema": { + "type": "object" + } + } + } + }, + "200": { + "description": "OK", + "content": { + "application/hal+json": { + "schema": { + "type": "object" + } + } + } + } + }, + "security": [ + { + "bearer-jwt": [] + } + ] + } + }, + "/api/v1/pit/pid/": { + "post": { + "tags": [ + "typing-rest-resource-impl" + ], + "summary": "Create a new PID record", + "description": "Create a new PID record using the record information from the request body.", + "operationId": "createPID", + "parameters": [ + { + "name": "dryrun", + "in": "query", + "description": "If true, only validation will be done and no PID will be created. No data will be changed and no services will be notified.", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + } + ], + "requestBody": { + "description": "The body containing all PID record values as they should be in the new PIDs record.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PIDRecord" + } + }, + "application/vnd.datamanager.pid.simple+json": { + "schema": { + "$ref": "#/components/schemas/SimplePidRecord" + } + } + }, + "required": true + }, + "responses": { + "400": { + "description": "Validation failed. See body for details. Contains also the validated record.", + "content": { + "application/json": {} + } + }, + "406": { + "description": "Provided input is invalid with regard to the supported accept header (Not acceptable)", + "content": { + "application/json": {} + } + }, + "500": { + "description": "Server error. See body for details.", + "content": { + "application/json": {} + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PIDRecord" + } + }, + "application/vnd.datamanager.pid.simple+json": { + "schema": { + "$ref": "#/components/schemas/SimplePidRecord" + } + } + } + }, + "503": { + "description": "Communication to required external service failed.", + "content": { + "application/json": {} + } + }, + "409": { + "description": "If providing an own PID is enabled 409 indicates, that the PID already exists.", + "content": { + "application/json": {} + } + }, + "415": { + "description": "Provided input is invalid with regard to the supported content types. (Unsupported Mediatype)", + "content": { + "application/json": {} + } + } + } + } + }, + "/api/v1/pit/known-pid": { + "get": { + "tags": [ + "typing-rest-resource-impl" + ], + "summary": "Returns all known PIDs. Supports paging, filtering criteria, and different formats.", + "description": "Returns all known PIDs, limited by the given page size and number. Several filtering criteria are also available. Known PIDs are defined as being stored in a local store. This store is not a cache! Instead, the service remembers every PID which it created (and resolved, depending on the configuration parameter `pit.storage.strategy` of the service) on request. Use the Accept header to adjust the format.", + "operationId": "findAll_1", + "parameters": [ + { + "name": "created_after", + "in": "query", + "description": "The UTC time of the earliest creation timestamp of a returned PID.", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "created_before", + "in": "query", + "description": "The UTC time of the latest creation timestamp of a returned PID.", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "modified_after", + "in": "query", + "description": "The UTC time of the earliest modification timestamp of a returned PID.", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "modified_before", + "in": "query", + "description": "The UTC time of the latest modification timestamp of a returned PID.", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "page", + "in": "query", + "description": "Zero-based page index (0..N)", + "schema": { + "type": "integer", + "default": 0 + } + }, + { + "name": "size", + "in": "query", + "description": "The size of the page to be returned", + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "sort", + "in": "query", + "description": "Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "Accept", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "application/tabulator+json" + ] + } + } + ], + "responses": { + "400": { + "description": "Bad Request", + "content": { + "application/hal+json": { + "schema": { + "type": "object" + } + } + } + }, + "200": { + "description": "If the request was valid. May return an empty list.", + "content": { + "application/hal+json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/KnownPid" + } + } + }, + "application/tabulator+json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/TabulatorPaginationFormatKnownPid" + }, + { + "$ref": "#/components/schemas/TabulatorPaginationFormat" + } + ] + } + } + } + }, + "500": { + "description": "Server error. See body for details.", + "content": { + "application/json": {}, + "application/tabulator+json": { + "schema": { + "$ref": "#/components/schemas/TabulatorPaginationFormatKnownPid" + } + } + } + } + } + } + }, + "/api/v1/pit/known-pid/**": { + "get": { + "tags": [ + "typing-rest-resource-impl" + ], + "summary": "Returns a PID and its timestamps from the local store, if available.", + "description": "Returns a PID from the local store. This store is not a cache! Instead, the service remembers every PID which it created (and resolved, depending on the configuration parameter `pit.storage.strategy` of the service) on request. If this PID is known, it will be returned together with the timestamps of creation and modification executed on this PID by this service.", + "operationId": "findByPid", + "responses": { + "400": { + "description": "Bad Request", + "content": { + "application/hal+json": { + "schema": { + "type": "object" + } + } + } + }, + "200": { + "description": "If the PID is known and its information was returned.", + "content": { + "application/hal+json": { + "schema": { + "$ref": "#/components/schemas/KnownPid" + } + } + } + }, + "404": { + "description": "If the PID is unknown.", + "content": { + "application/json": {} + } + }, + "500": { + "description": "Server error. See body for details.", + "content": { + "application/json": {} + } + } + } + } + } + }, + "components": { + "schemas": { + "PIDRecord": { + "type": "object", + "properties": { + "pid": { + "type": "string" + }, + "entries": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PIDRecordEntry" + } + } + } + } + }, + "PIDRecordEntry": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "SimplePair": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "SimplePidRecord": { + "type": "object", + "properties": { + "pid": { + "type": "string" + }, + "record": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SimplePair" + } + } + } + }, + "JsonNode": { + "type": "object" + }, + "ProxyExchangeByte[]": { + "type": "object" + }, + "KnownPid": { + "required": [ + "created", + "modified", + "pid" + ], + "type": "object", + "properties": { + "pid": { + "type": "string" + }, + "created": { + "type": "string", + "format": "date-time" + }, + "modified": { + "type": "string", + "format": "date-time" + } + } + }, + "TabulatorPaginationFormat": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object" + } + }, + "last_page": { + "type": "integer", + "format": "int32" + } + } + }, + "TabulatorPaginationFormatKnownPid": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/KnownPid" + } + }, + "last_page": { + "type": "integer", + "format": "int32" + } + } + } + }, + "securitySchemes": { + "bearer-jwt": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + } + } + } +} \ No newline at end of file diff --git a/rules-api/build.gradle b/rules-api/build.gradle new file mode 100644 index 0000000..a00a6fb --- /dev/null +++ b/rules-api/build.gradle @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id 'java' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +repositories { + mavenCentral() +} + +dependencies { + // Minimal dependencies for the API + compileOnly 'javax.annotation:javax.annotation-api:1.3.2' + + // Add validation annotations + implementation 'jakarta.validation:jakarta.validation-api:3.0.2' + + // Add Lombok for the enum + compileOnly 'org.projectlombok:lombok:1.18.38' + annotationProcessor 'org.projectlombok:lombok:1.18.38' + + // Add Spring annotations as optional (for @Nullable and @Component) + compileOnly 'org.springframework:spring-core:6.2.7' + compileOnly 'org.springframework:spring-context:6.2.7' +} \ No newline at end of file diff --git a/rules-api/src/main/java/edu/kit/datamanager/idoris/rules/logic/IRule.java b/rules-api/src/main/java/edu/kit/datamanager/idoris/rules/logic/IRule.java new file mode 100644 index 0000000..c56d2bc --- /dev/null +++ b/rules-api/src/main/java/edu/kit/datamanager/idoris/rules/logic/IRule.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.rules.logic; + +/** + * Interface for rule processors that can be used to process visitable elements. + * Each rule processor is responsible for a specific task and element type. + * + * @param the type of visitable element this processor can handle + * @param the type of rule output produced by this processor + */ +public interface IRule> { + + /** + * Process the given input element and update the provided output. + * + * @param input the element to process + * @param output the output to update with processing results + */ + void process(T input, R output); +} diff --git a/rules-api/src/main/java/edu/kit/datamanager/idoris/rules/logic/OutputMessage.java b/rules-api/src/main/java/edu/kit/datamanager/idoris/rules/logic/OutputMessage.java new file mode 100644 index 0000000..38ea65b --- /dev/null +++ b/rules-api/src/main/java/edu/kit/datamanager/idoris/rules/logic/OutputMessage.java @@ -0,0 +1,85 @@ +/* + * 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.rules.logic; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.lang.Nullable; + + +/** + * Immutable record representing a message produced during rule processing. + * Each message has a text content, a severity level, and optional related elements. + * + *

This record is used to communicate validation results, warnings, and informational messages + * from rule processors to the calling code.

+ * + * @param message the text content of the message, must not be null + * @param severity the severity level of the message, must not be null + * @param element optional elements related to this message (e.g., the objects that caused the message) + */ +public record OutputMessage(@NotNull String message, @NotNull MessageSeverity severity, @Nullable Object... element) { + + /** + * Enumeration of possible message severity levels. + * Severities are ordered from least severe (INFO) to most severe (ERROR). + */ + @AllArgsConstructor + @Getter + public enum MessageSeverity { + /** + * Informational message that doesn't indicate a problem. + * Typically used for providing context or additional information. + */ + INFO("INFO"), // ordinal 0 + + /** + * Warning message that indicates a potential issue but not a critical problem. + * The system can continue to function, but the user should be aware of the warning. + */ + WARNING("WARNING"), // ordinal 1 + + /** + * Error message that indicates a critical problem. + * The system may not function correctly due to this error. + */ + ERROR("ERROR"); // ordinal 2 + + /** + * The string representation of this severity level + */ + private final String value; + + /** + * Checks if this severity is higher than or equal to the other severity. + * Assumes severities are ordered by their declaration (ordinal value): + * INFO < WARNING < ERROR + * + * @param other The severity to compare against. + * @return true if this severity is higher than or equal to other; false otherwise. + */ + @SuppressWarnings("EnumOrdinal") + public boolean isHigherOrEqualTo(MessageSeverity other) { + if (other == null) { + // Or throw new IllegalArgumentException("Cannot compare with a null severity"); + return false; // Or handle as per specific requirements for null inputs + } + return this.ordinal() >= other.ordinal(); + } + } +} \ No newline at end of file diff --git a/rules-api/src/main/java/edu/kit/datamanager/idoris/rules/logic/PrecomputedRuleGraph.java b/rules-api/src/main/java/edu/kit/datamanager/idoris/rules/logic/PrecomputedRuleGraph.java new file mode 100644 index 0000000..eed8a31 --- /dev/null +++ b/rules-api/src/main/java/edu/kit/datamanager/idoris/rules/logic/PrecomputedRuleGraph.java @@ -0,0 +1,67 @@ +/* + * 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.rules.logic; + +import java.util.List; +import java.util.Map; + +/** + * Represents a precomputed rule dependency graph generated at compile time. + * + *

This class stores the results of the topological sort performed by the + * RuleAnnotationProcessor for use at runtime, eliminating the need + * for duplicate validation and sorting in the RuleService. + */ +public class PrecomputedRuleGraph { + + /** + * Map of rule tasks to their target types and sorted rule classes. + * Structure: RuleTask → TargetType → Topologically sorted rule class names + */ + private final Map>> sortedRulesByTaskAndType; + + /** + * Constructs a new precomputed rule graph with the given sorted rules. + * + * @param sortedRulesByTaskAndType map of rule tasks to their target types and sorted rule classes + */ + public PrecomputedRuleGraph(Map>> sortedRulesByTaskAndType) { + this.sortedRulesByTaskAndType = sortedRulesByTaskAndType; + } + + /** + * Gets the sorted rule class names for the given task and target type. + * + * @param task the rule task + * @param targetType the fully qualified name of the target type + * @return list of fully qualified rule class names in topological order + */ + public List getSortedRuleClasses(RuleTask task, String targetType) { + return sortedRulesByTaskAndType + .getOrDefault(task, Map.of()) + .getOrDefault(targetType, List.of()); + } + + /** + * Gets the complete map of sorted rules by task and target type. + * + * @return map of rule tasks to their target types and sorted rule classes + */ + public Map>> getSortedRulesByTaskAndType() { + return sortedRulesByTaskAndType; + } +} diff --git a/rules-api/src/main/java/edu/kit/datamanager/idoris/rules/logic/Rule.java b/rules-api/src/main/java/edu/kit/datamanager/idoris/rules/logic/Rule.java new file mode 100644 index 0000000..1ce99c3 --- /dev/null +++ b/rules-api/src/main/java/edu/kit/datamanager/idoris/rules/logic/Rule.java @@ -0,0 +1,100 @@ +/* + * 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.rules.logic; + +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +/** + * Annotation to define a rule that can be applied to elements in the system. + * This annotation is used to specify the types of elements the rule applies to, + * the processors to be executed before, after, and on error, and the events that trigger the rule. + * It also includes metadata such as the name, description, and tasks associated with the rule. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +@Component +public @interface Rule { + /** + * Specifies the types of elements this rule applies to. + * This is used to filter which rules are applicable to a given element. + * + * @return an array of classes that extend VisitableElement + */ + Class[] appliesTo(); + + /** + * Specifies the processors that should be executed before the rule execution. + * These processors can be used to prepare the input for the rule or to perform preliminary checks. + * These processors can modify the input or output of the rule. + * + * @return arrays of classes that implement IRule + */ + Class[] dependsOn() default {}; + + /** + * Specifies the processors that should be executed after the main rule processing. + * This can be used for additional validation, transformation, or other post-processing tasks. + * + * @return an array of classes that implement IRule + */ + Class[] executeBefore() default {}; + + /** + * Specifies the processors that should be executed in case of an error during rule processing. + * This can be used for error handling, logging, or alternative processing paths. + * + * @return an array of classes that implement IRule + */ + Class[] onError() default {}; + + /** + * Specifies the events that trigger the execution of this rule. + * This allows the rule to be executed in response to specific events in the system. + * + * @return an array of RuleEvent enums that define the events + */ + RuleEvent[] executeOnEvent() default {}; + + /** + * Specifies the name of the rule. + * This is used for identification and logging purposes. + * + * @return the name of the rule + */ + String name(); // e.g. "PIDValidationRule" + + /** + * Provides a description of the rule. + * This can be used to explain the purpose and functionality of the rule. + * + * @return a string description of the rule + */ + String description() default ""; + + /** + * Specifies the tasks that this rule is responsible for. + * This allows the rule to be categorized and executed based on the specific task it handles. + * + * @return an array of RuleTask enums that define the tasks + */ + RuleTask[] tasks() default {RuleTask.VALIDATE}; // e.g. "VALIDATE", "CONSUME", "OPTIMIZE", "GENERATE_SCHEMA" +} + diff --git a/rules-api/src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleEvent.java b/rules-api/src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleEvent.java new file mode 100644 index 0000000..bf0e9dd --- /dev/null +++ b/rules-api/src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleEvent.java @@ -0,0 +1,68 @@ +/* + * 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.rules.logic; + +/** + * Enumeration of events that can trigger rule execution. + * These events represent different lifecycle phases or operations on domain elements. + * + *

Rules can be configured to execute in response to specific events using the + * {@link Rule#executeOnEvent()} attribute.

+ */ +public enum RuleEvent { + /** + * Triggered when a new element is being created in the system. + * Rules that execute on this event typically validate or enhance newly created elements. + */ + CREATE, + + /** + * Triggered when an existing element is being updated. + * Rules that execute on this event typically validate changes or maintain consistency. + */ + UPDATE, + + /** + * Triggered when an element is being deleted from the system. + * Rules that execute on this event typically validate that deletion is allowed or perform cleanup. + */ + DELETE, + + /** + * Triggered when an element is being consumed by an external system or process. + * Rules that execute on this event typically prepare the element for consumption. + */ + CONSUME, + + /** + * Triggered when an element is being explicitly validated. + * Rules that execute on this event perform various validation checks. + */ + VALIDATE, + + /** + * Triggered when an element is being published to external systems. + * Rules that execute on this event typically prepare the element for publication. + */ + PUBLISH, + + /** + * Catch-all for other events not covered by the more specific values. + * Rules that execute on this event should be prepared to handle various contexts. + */ + OTHERS +} diff --git a/rules-api/src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleOutput.java b/rules-api/src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleOutput.java new file mode 100644 index 0000000..349e4e9 --- /dev/null +++ b/rules-api/src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleOutput.java @@ -0,0 +1,58 @@ +/* + * 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.rules.logic; + + +public interface RuleOutput> { + + /** + * Merges the current output with others of the same type. + * This method should be implemented to define how outputs are combined. + * + * @param others the other outputs to merge with + * @return the merged output + */ + T merge(T... others); + + /** + * Generates an empty output of the same type. + * This method should be implemented to create a new instance of the output type. + * + * @return a new empty instance of the same type + */ + T empty(); + + /** + * Adds a message to the output with additional elements. + * + * @param message the message to add + * @param severity the severity of the message + * @param elements additional elements related to the message + * @return the updated output with the added message + */ + default T addMessage(String message, OutputMessage.MessageSeverity severity, Object... elements) { + return addMessage(new OutputMessage(message, severity, elements)); + } + + /** + * Adds a message to the output. + * + * @param message the message to add + * @return the updated output with the added message + */ + T addMessage(OutputMessage message); +} \ No newline at end of file diff --git a/rules-api/src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleTask.java b/rules-api/src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleTask.java new file mode 100644 index 0000000..810ce2e --- /dev/null +++ b/rules-api/src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleTask.java @@ -0,0 +1,24 @@ +/* + * 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.rules.logic; + +public enum RuleTask { + VALIDATE, + CONSUME, + OPTIMIZE, + GENERATE_SCHEMA +} diff --git a/rules-api/src/main/java/edu/kit/datamanager/idoris/rules/logic/VisitableElement.java b/rules-api/src/main/java/edu/kit/datamanager/idoris/rules/logic/VisitableElement.java new file mode 100644 index 0000000..098505b --- /dev/null +++ b/rules-api/src/main/java/edu/kit/datamanager/idoris/rules/logic/VisitableElement.java @@ -0,0 +1,20 @@ +/* + * 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.rules.logic; + +public interface VisitableElement { +} diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/IOperationTypeProfileDao.java b/rules-processor/build.gradle similarity index 52% rename from src/main/java/edu/kit/datamanager/idoris/dao/IOperationTypeProfileDao.java rename to rules-processor/build.gradle index 282549c..8afc114 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/IOperationTypeProfileDao.java +++ b/rules-processor/build.gradle @@ -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,11 +14,25 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.dao; +plugins { + id 'java' +} -import edu.kit.datamanager.idoris.domain.entities.OperationTypeProfile; -import org.springframework.data.rest.core.annotation.RepositoryRestResource; +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} -@RepositoryRestResource(collectionResourceRel = "operationTypeProfiles", path = "operationTypeProfiles") -public interface IOperationTypeProfileDao extends IAbstractRepo { +repositories { + mavenCentral() } + +dependencies { + implementation project(':rules-api') + implementation 'com.google.auto.service:auto-service:1.1.1' + annotationProcessor 'com.google.auto.service:auto-service:1.1.1' + + // Add minimal dependencies needed for the processors + compileOnly 'javax.annotation:javax.annotation-api:1.3.2' +} \ No newline at end of file diff --git a/rules-processor/src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleAnnotationProcessor.java b/rules-processor/src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleAnnotationProcessor.java new file mode 100644 index 0000000..a2df9c2 --- /dev/null +++ b/rules-processor/src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleAnnotationProcessor.java @@ -0,0 +1,1078 @@ +/* + * 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.rules.logic; + +import com.google.auto.service.AutoService; + +import javax.annotation.processing.*; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.*; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; +import javax.tools.Diagnostic; +import javax.tools.JavaFileObject; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Compile-time annotation processor for validating {@link Rule} annotated classes. + * + *

This processor ensures that rule classes are properly structured and that their + * dependencies form a valid directed acyclic graph (DAG). It performs comprehensive + * validation at compile time to catch configuration errors early in the development cycle. + * + *

Validation Features

+ *
    + *
  • Structural Validation: Ensures rule classes implement {@link IRule} + * and are properly annotated
  • + *
  • Type Safety: Validates that target types extend {@link VisitableElement} + * and are not abstract classes
  • + *
  • Dependency Validation: Detects circular dependencies between rules + * using topological sorting
  • + *
  • Task Compatibility: Ensures dependencies exist within the same + * task and target type context
  • + *
+ * + *

Processing Algorithm

+ *

The processor operates in two distinct phases: + *

    + *
  1. Collection Phase: Scans all {@code @Rule} annotated classes across + * multiple compilation rounds, extracting and validating their metadata
  2. + *
  3. Validation Phase: After all source files are processed, validates + * the complete dependency graph for cycles and inconsistencies
  4. + *
+ * + *

Dependency Graph Structure

+ *

Rules are organized into separate dependency graphs based on: + *

    + *
  • Task Type: Rules for different tasks (VALIDATE, CONSUME, etc.) + * are validated independently
  • + *
  • Target Type: Rules targeting different visitable element types + * are validated in separate graphs
  • + *
+ * + *

This separation ensures that dependency validation is contextually appropriate + * and prevents false positives from cross-context dependencies. + * + *

Error Reporting

+ *

The processor reports compilation errors for: + *

    + *
  • Classes that don't implement {@link IRule}
  • + *
  • Rules targeting non-{@link VisitableElement} types
  • + *
  • Rules targeting abstract classes
  • + *
  • Circular dependencies between rules
  • + *
  • Missing required annotation attributes
  • + *
+ * + * @see Rule + * @see IRule + * @see VisitableElement + * @see RuleTask + * @since 1.0 + */ +@SupportedAnnotationTypes("edu.kit.datamanager.idoris.rules.logic.Rule") +@SupportedSourceVersion(SourceVersion.RELEASE_21) +@AutoService(Processor.class) +public final class RuleAnnotationProcessor extends AbstractProcessor { + + /** + * Three-level nested map structure organizing collected rule metadata. + * + *

Structure: {@code Task → TargetType → RuleClassName → RuleMetadata} + * + *

This organization allows for independent validation of dependency graphs + * within each (task, target-type) combination, ensuring that rules can only + * depend on other rules that: + *

    + *
  • Execute in the same task context
  • + *
  • Process the same target type
  • + *
+ * + *

Example structure: + *

+     * VALIDATE → {
+     *   "com.example.MyEntity" → {
+     *     "com.example.ValidationRule1" → RuleMetadata(...),
+     *     "com.example.ValidationRule2" → RuleMetadata(...)
+     *   }
+     * }
+     * CONSUME → {
+     *   "com.example.MyEntity" → {
+     *     "com.example.ConsumptionRule" → RuleMetadata(...)
+     *   }
+     * }
+     * 
+ */ + private final Map>> rulesByTaskAndTarget = new HashMap<>(); + /** + * Utility for performing type-related operations during annotation processing. + * Used for type assignability checks and type comparisons. + */ + private Types typeUtilities; + /** + * Utility for working with program elements during annotation processing. + * Used for looking up type elements and accessing element metadata. + */ + private Elements elementUtilities; + + /** + * Initializes the annotation processor with necessary utilities from the processing environment. + * + *

This method is called once by the compiler before any processing begins. + * It sets up the type and element utilities needed for reflection-like operations + * during annotation processing. + * + * @param processingEnvironment the processing environment provided by the compiler, + * containing utilities and configuration for this processing session + */ + @Override + public synchronized void init(ProcessingEnvironment processingEnvironment) { + super.init(processingEnvironment); + // Initialize utilities for type operations (assignability, erasure, etc.) + this.typeUtilities = processingEnvironment.getTypeUtils(); + // Initialize utilities for element operations (lookup, metadata access, etc.) + this.elementUtilities = processingEnvironment.getElementUtils(); + } + + /** + * Main processing method implementing the two-phase validation strategy. + * + *

This method is called by the compiler for each round of annotation processing. + * The compiler may invoke this method multiple times as it processes different + * source files and generates new ones. + * + *

Phase 1: Collection (Normal Rounds)

+ *

During normal processing rounds ({@code processingRound.processingOver() == false}): + *

    + *
  • Scan all {@code @Rule} annotated classes found in this round
  • + *
  • Validate basic structural requirements (implements IRule, etc.)
  • + *
  • Extract and store rule metadata for dependency validation
  • + *
+ * + *

Phase 2: Validation (Final Round)

+ *

During the final round ({@code processingRound.processingOver() == true}): + *

    + *
  • All source files have been processed
  • + *
  • Complete rule dependency graphs are available
  • + *
  • Perform topological sort to detect cycles
  • + *
  • Report any circular dependencies as compilation errors
  • + *
+ * + * @param annotationTypes the set of annotation types requested to be processed + * @param processingRound the environment for this round of processing + * @return {@code false} to allow other annotation processors to process the same annotations + */ + @Override + public boolean process(Set annotationTypes, RoundEnvironment processingRound) { + if (processingRound.processingOver()) { + // Final round: all source files processed, validate complete dependency graphs + // This is executed after all validation rounds by the compiler are complete + validateAllRuleDependencyGraphs(); + return false; + } + + // Normal round: collect rule metadata from newly processed source files + collectRuleMetadataFromAnnotatedClasses(processingRound); + return false; // Allow other processors to see the same annotations + } + + /** + * Collects metadata from all {@code @Rule} annotated classes in the current processing round. + * + *

This method iterates through all elements annotated with {@code @Rule} that were + * discovered in the current compilation round. For each valid rule class, it: + *

    + *
  1. Validates the class structure (implements IRule, is a class, etc.)
  2. + *
  3. Extracts rule metadata (dependencies, target types, tasks)
  4. + *
  5. Stores the metadata for later dependency validation
  6. + *
+ * + *

Invalid elements (non-classes) are skipped with a warning, allowing processing + * to continue for valid rules. + * + * @param processingRound the current processing round containing newly discovered annotated elements + */ + private void collectRuleMetadataFromAnnotatedClasses(RoundEnvironment processingRound) { + // Get all elements annotated with @Rule in this processing round + // Phase 1: Collection - scan all @Rule annotated elements in this compilation round + Set ruleAnnotatedElements = processingRound.getElementsAnnotatedWith(Rule.class); + + for (Element annotatedElement : ruleAnnotatedElements) { + // Only process type elements (classes, interfaces, enums) + // Skip other elements like methods, fields, etc. + if (annotatedElement instanceof TypeElement ruleClass) { + // Validate that the class meets structural requirements + validateRuleClassStructure(ruleClass); + + // Extract and store rule metadata for dependency validation + collectRuleMetadata(ruleClass); + } + // Non-type elements are silently skipped - they'll be caught by other validation + } + } + + /** + * Validates that a rule class meets all structural requirements. + * + *

This method performs comprehensive validation of a rule class to ensure it: + *

    + *
  • Is actually a class (not an interface, enum, or annotation)
  • + *
  • Implements the required {@link IRule} interface
  • + *
+ * + *

Any violations are reported as compilation errors, preventing the build + * from succeeding with invalid rule configurations. + * + * @param ruleClass the type element representing the rule class to validate + */ + private void validateRuleClassStructure(TypeElement ruleClass) { + // Ensure the annotated element is a class + validateIsClass(ruleClass); + + // Ensure the class implements the IRule interface + validateImplementsIRule(ruleClass); + } + + /** + * Validates that the annotated element is a class. + * + *

The {@code @Rule} annotation should only be applied to classes, not to + * interfaces, enums, annotations, or other element types. This validation + * catches misuse of the annotation early in the compilation process. + * + * @param typeElement the type element to validate + */ + private void validateIsClass(TypeElement typeElement) { + // Check if the annotated element is actually a class + if (typeElement.getKind() != ElementKind.CLASS) { + reportError(typeElement, + "Rule annotation can only be applied to classes, not %s. " + + "Found annotation on %s which is a %s.", + typeElement.getKind(), + typeElement.getQualifiedName(), + typeElement.getKind()); + } + } + + /** + * Validates that the rule class implements the {@link IRule} interface. + * + *

All rule classes must implement the {@code IRule} interface to provide + * the required {@code process} method. This validation ensures type safety + * and proper integration with the rule execution framework. + * + *

The validation uses type assignability checking to handle generic types + * and inheritance hierarchies correctly. + * + * @param ruleClass the rule class to validate + */ + private void validateImplementsIRule(TypeElement ruleClass) { + if (!implementsIRule(ruleClass)) { + reportError(ruleClass, + "Rule class %s must implement IRule interface. " + + "Add 'implements IRule' to the class declaration.", + ruleClass.getQualifiedName()); + } + } + + /** + * Checks if a type implements the {@link IRule} interface. + * + *

This method uses the type utilities to perform assignability checking, + * which correctly handles: + *

    + *
  • Direct implementation of IRule
  • + *
  • Implementation through inheritance
  • + *
  • Generic type parameters
  • + *
  • Raw types and type erasure
  • + *
+ * + * @param candidateType the type to check for IRule implementation + * @return {@code true} if the type implements IRule, {@code false} otherwise + */ + private boolean implementsIRule(TypeElement candidateType) { + // Look up the IRule interface in the current compilation context + TypeElement iRuleInterface = elementUtilities.getTypeElement("edu.kit.datamanager.idoris.rules.logic.IRule"); + + if (iRuleInterface == null) { + // IRule interface not found in classpath - this suggests a configuration problem + // Return false to trigger validation error + return false; + } + + // Use assignability checking to handle generics and inheritance + // erasure() is used to handle raw types correctly + // Check if the type is assignable to IRule (handles inheritance and generics) + return typeUtilities.isAssignable( + candidateType.asType(), + typeUtilities.erasure(iRuleInterface.asType()) + ); + } + + /** + * Extracts and stores comprehensive metadata from a rule class. + * + *

This method performs the core metadata extraction process: + *

    + *
  1. Annotation Access: Safely retrieves the {@code @Rule} annotation + * using annotation mirrors to avoid {@code MirroredTypesException}
  2. + *
  3. Attribute Extraction: Extracts all annotation attributes + * (tasks, appliesTo, dependsOn, executeBefore)
  4. + *
  5. Validation: Validates target types and dependencies
  6. + *
  7. Storage: Organizes the metadata for efficient dependency validation
  8. + *
+ * + *

The extracted metadata is stored in the {@link #rulesByTaskAndTarget} structure, + * organized by task and target type to enable independent validation of each + * dependency graph. + * + * @param ruleClass the rule class to extract metadata from + */ + private void collectRuleMetadata(TypeElement ruleClass) { + // Safely retrieve the @Rule annotation mirror to avoid MirroredTypesException + AnnotationMirror ruleAnnotation = findRuleAnnotation(ruleClass); + if (ruleAnnotation == null) { + // This should not happen since we're processing @Rule annotated elements + // but we handle it defensively + return; + } + + // Extract all annotation attributes as a map for easy access + Map annotationAttributes = extractAnnotationAttributes(ruleAnnotation); + + // Extract and validate the tasks this rule applies to + RuleTask[] applicableTasks = extractApplicableTasks(annotationAttributes); + + // Extract and validate the target types this rule can process + List targetTypes = extractAndValidateTargetTypes(ruleClass, annotationAttributes); + + // Extract dependency information for DAG validation + List dependencyRules = extractDependencyRules(annotationAttributes, "dependsOn"); + List predecessorRules = extractDependencyRules(annotationAttributes, "executeBefore"); + + // Create metadata record containing all extracted information + RuleMetadata metadata = new RuleMetadata( + ruleClass.getQualifiedName().toString(), + dependencyRules, + predecessorRules + ); + + // Store metadata organized by task and target type for validation + storeRuleMetadata(applicableTasks, targetTypes, metadata); + } + + /** + * Finds the {@code @Rule} annotation mirror for safe attribute access. + * + *

Annotation mirrors are used instead of direct annotation access to avoid + * {@code MirroredTypesException} when the annotation contains {@code Class} values. + * This exception occurs because the referenced classes might not be available + * in the current compilation context. + * + *

The annotation mirror provides a safe way to access annotation attributes + * as {@code TypeMirror} objects instead of {@code Class} objects. + * + * @param ruleClass the rule class to search for the annotation + * @return the {@code @Rule} annotation mirror, or {@code null} if not found + */ + private AnnotationMirror findRuleAnnotation(TypeElement ruleClass) { + // Search through all annotation mirrors on the class + return ruleClass.getAnnotationMirrors().stream() + .filter(mirror -> { + // Match by fully qualified annotation type name + String annotationType = mirror.getAnnotationType().toString(); + return annotationType.equals("edu.kit.datamanager.idoris.rules.logic.Rule"); + }) + .findFirst() + .orElse(null); + } + + /** + * Extracts all annotation attributes from a {@code @Rule} annotation mirror. + * + *

This method creates a convenient map of attribute names to their values, + * making it easy to access specific annotation attributes in subsequent processing + * steps. + * + *

The map keys are the simple names of the annotation methods (e.g., "appliesTo", + * "dependsOn"), and the values are the corresponding {@code AnnotationValue} objects + * that can be safely processed without triggering {@code MirroredTypesException}. + * + * @param ruleAnnotation the annotation mirror to extract attributes from + * @return a map of attribute names to their annotation values + */ + private Map extractAnnotationAttributes(AnnotationMirror ruleAnnotation) { + Map attributes = new HashMap<>(); + + // Iterate through all annotation attributes (method-value pairs) + ruleAnnotation.getElementValues().forEach((method, value) -> { + // Use the simple name of the method as the key (e.g., "appliesTo") + String attributeName = method.getSimpleName().toString(); + attributes.put(attributeName, value); + }); + + return attributes; + } + + /** + * Extracts the tasks this rule applies to from the annotation attributes. + * + *

The {@code tasks} attribute specifies which rule tasks (VALIDATE, CONSUME, etc.) + * this rule should be executed for. If not specified, the rule applies to all tasks. + * + *

This method handles: + *

    + *
  • Missing tasks attribute (defaults to all tasks)
  • + *
  • Single task specification
  • + *
  • Multiple task specification
  • + *
  • Enum value extraction from string representations
  • + *
+ * + * @param attributes the map of annotation attributes + * @return array of {@code RuleTask} enums this rule applies to + */ + private RuleTask[] extractApplicableTasks(Map attributes) { + AnnotationValue tasksAttribute = attributes.get("tasks"); + + if (tasksAttribute == null) { + // If tasks not specified, default to all available tasks + return RuleTask.values(); + } + + // The tasks attribute contains an array of enum values + @SuppressWarnings("unchecked") + List taskValues = (List) tasksAttribute.getValue(); + + // Convert each enum value to its corresponding RuleTask + return taskValues.stream() + .map(this::extractEnumName) // Extract enum name from fully qualified string + .map(RuleTask::valueOf) // Convert to RuleTask enum + .toArray(RuleTask[]::new); + } + + /** + * Extracts the simple enum name from its fully qualified string representation. + * + *

During annotation processing, enum values are represented as strings like + * "com.example.EnumClass.ENUM_VALUE". This method extracts just the "ENUM_VALUE" + * part for use with {@code Enum.valueOf()}. + * + * @param enumValue the annotation value containing the enum reference + * @return the simple enum name (e.g., "VALIDATE" from "...RuleTask.VALIDATE") + */ + private String extractEnumName(AnnotationValue enumValue) { + String fullEnumString = enumValue.getValue().toString(); + // Extract everything after the last dot + return fullEnumString.substring(fullEnumString.lastIndexOf('.') + 1); + } + + /** + * Extracts and validates the target types this rule can process. + * + *

This method processes the {@code appliesTo} attribute which specifies + * which types of visitable elements this rule can handle. It performs + * comprehensive validation to ensure type safety: + * + *

    + *
  • Presence Check: Ensures the appliesTo attribute is specified
  • + *
  • Type Validation: Validates each target type extends VisitableElement
  • + *
  • Concreteness Check: Ensures target types are not abstract
  • + *
+ * + * @param ruleClass the rule class being processed (for error reporting) + * @param attributes the map of annotation attributes + * @return list of fully qualified target type names + */ + private List extractAndValidateTargetTypes(TypeElement ruleClass, Map attributes) { + AnnotationValue appliesToAttribute = attributes.get("appliesTo"); + + if (appliesToAttribute == null) { + // appliesTo is a required attribute + reportError(ruleClass, + "Rule %s must specify the 'appliesTo' attribute. " + + "Add 'appliesTo = {YourTargetType.class}' to the @Rule annotation.", + ruleClass.getQualifiedName()); + return Collections.emptyList(); + } + + // The appliesTo attribute contains an array of Class references + @SuppressWarnings("unchecked") + List typeValues = (List) appliesToAttribute.getValue(); + + // Process each target type: validate and convert to string + return typeValues.stream() + .map(typeValue -> (TypeMirror) typeValue.getValue()) // Get TypeMirror from AnnotationValue + .peek(targetType -> validateTargetType(ruleClass, targetType)) // Validate each type + .map(TypeMirror::toString) // Convert to string representation + .collect(Collectors.toList()); + } + + /** + * Validates a single target type for compliance with rule requirements. + * + *

This method performs two critical validations: + *

    + *
  1. Inheritance Check: Ensures the type extends VisitableElement
  2. + *
  3. Concreteness Check: Ensures the type is not abstract
  4. + *
+ * + *

These validations prevent runtime errors and ensure that rules can only + * be applied to valid, instantiable visitable elements. + * + * @param ruleClass the rule class being processed (for error context) + * @param targetType the target type to validate + */ + private void validateTargetType(TypeElement ruleClass, TypeMirror targetType) { + // Ensure the target type extends VisitableElement + validateExtendsVisitableElement(ruleClass, targetType); + + // Ensure the target type is not abstract + validateNotAbstractClass(ruleClass, targetType); + } + + /** + * Validates that a target type extends {@link VisitableElement}. + * + *

All rule target types must extend VisitableElement to be compatible + * with the rule execution framework. This validation uses type assignability + * checking to handle inheritance hierarchies correctly. + * + * @param ruleClass the rule class being processed (for error context) + * @param targetType the target type to validate + */ + private void validateExtendsVisitableElement(TypeElement ruleClass, TypeMirror targetType) { + // Look up the interface VisitableElement + TypeElement visitableElement = elementUtilities.getTypeElement("edu.kit.datamanager.idoris.rules.logic.VisitableElement"); + + if (visitableElement != null && !typeUtilities.isAssignable(targetType, visitableElement.asType())) { + reportError(ruleClass, + "Rule %s targets type %s which does not extend VisitableElement. " + + "Ensure your target type extends VisitableElement or one of its subclasses.", + ruleClass.getQualifiedName(), + targetType); + } + } + + /** + * Validates that a target type is not an abstract class. + * + *

Rules cannot be applied to abstract classes because abstract classes + * cannot be instantiated. This validation prevents configuration errors + * that would cause runtime failures. + * + * @param ruleClass the rule class being processed (for error context) + * @param targetType the target type to validate + */ + private void validateNotAbstractClass(TypeElement ruleClass, TypeMirror targetType) { + if (targetType instanceof DeclaredType declaredType) { + Element typeElement = declaredType.asElement(); + + // Check if the type element is a TypeElement with abstract modifier + if (typeElement instanceof TypeElement te && + te.getModifiers().contains(Modifier.ABSTRACT)) { + reportError(ruleClass, + "Rule %s cannot target abstract type %s. " + + "Rules can only be applied to concrete (non-abstract) classes.", + ruleClass.getQualifiedName(), + targetType); + } + } + } + + /** + * Extracts dependency rule class names from annotation attributes. + * + *

This method extracts dependency information from either the {@code dependsOn} + * or {@code executeBefore} attributes. Both attributes contain arrays of Class + * references that specify rule dependencies. + * + *

The extracted dependencies are used to build dependency graphs for + * cycle detection and execution ordering. + * + * @param attributes the map of annotation attributes + * @param attributeName the name of the attribute to extract ("dependsOn" or "executeBefore") + * @return list of fully qualified dependency rule class names + */ + private List extractDependencyRules(Map attributes, String attributeName) { + AnnotationValue dependencyAttribute = attributes.get(attributeName); + + if (dependencyAttribute == null) { + // Dependency attributes are optional + return Collections.emptyList(); + } + + // The dependency attribute contains an array of Class references + @SuppressWarnings("unchecked") + List dependencyValues = (List) dependencyAttribute.getValue(); + + // Convert each Class reference to its fully qualified name + return dependencyValues.stream() + .map(dependency -> dependency.getValue().toString()) + .collect(Collectors.toList()); + } + + /** + * Stores rule metadata in the organized collection structure. + * + *

This method stores the extracted rule metadata in the three-level nested + * map structure that organizes rules by task and target type. This organization + * enables independent validation of dependency graphs within each context. + * + *

For a rule that applies to multiple tasks or target types, the metadata + * is stored in multiple locations to ensure proper validation in each context. + * + * @param applicableTasks the tasks this rule applies to + * @param targetTypes the target types this rule can process + * @param metadata the complete rule metadata to store + */ + private void storeRuleMetadata(RuleTask[] applicableTasks, List targetTypes, RuleMetadata metadata) { + // Store the rule metadata for each task-target combination + for (RuleTask task : applicableTasks) { + for (String targetType : targetTypes) { + // Navigate the three-level nested map structure + rulesByTaskAndTarget + .computeIfAbsent(task, k -> new HashMap<>()) // Task level + .computeIfAbsent(targetType, k -> new HashMap<>()) // Target type level + .put(metadata.className(), metadata); // Rule level + } + } + } + + /** + * Validates all collected rule dependency graphs for cycles and consistency. + * + *

This method is called during the final processing round when all source + * files have been processed and the complete dependency graphs are available. + * It validates each (task, target-type) combination independently to detect + * any circular dependencies that would cause infinite loops during execution. + * + *

The validation process: + *

    + *
  1. Iterates through each task type (VALIDATE, CONSUME, etc.)
  2. + *
  3. For each task, iterates through each target type
  4. + *
  5. Validates the dependency graph for that specific context
  6. + *
  7. Reports any cycles found as compilation errors
  8. + *
  9. If all validations pass, generates a precomputed rule graph class
  10. + *
+ */ + private void validateAllRuleDependencyGraphs() { + // Map to store topologically sorted rules for each task and target type + Map>> sortedRulesByTaskAndType = new HashMap<>(); + + // Validate each task-target combination independently + rulesByTaskAndTarget.forEach((task, rulesByTarget) -> { + Map> sortedRulesByTarget = new HashMap<>(); + sortedRulesByTaskAndType.put(task, sortedRulesByTarget); + + rulesByTarget.forEach((targetType, rulesInGraph) -> { + try { + // Validate and get topologically sorted rules + List sortedRules = validateDependencyGraph(task, targetType, rulesInGraph); + + // Store the class names of sorted rules + List sortedClassNames = sortedRules.stream() + .map(RuleMetadata::className) + .collect(Collectors.toList()); + + sortedRulesByTarget.put(targetType, sortedClassNames); + } catch (CyclicDependencyException e) { + // Error already reported in validateDependencyGraph + } + }); + }); + +// // Generate the precomputed rule graph class if no errors occurred +// if (!processingEnv.getMessager().isErrorRaised()) { + generatePrecomputedRuleGraphClass(sortedRulesByTaskAndType); +// } + } + + /** + * Validates a single dependency graph for cycles using topological sorting. + * + *

This method validates the dependency graph for a specific (task, target-type) + * combination. It uses topological sorting with cycle detection to ensure that + * the rules can be executed in a valid order without infinite loops. + * + *

If a cycle is detected, a compilation error is reported with details + * about the problematic dependencies. + * + * @param task the task context being validated + * @param targetType the target type context being validated + * @param rulesInGraph the rules in this specific dependency graph + * @return list of rule metadata in topological order, or empty list if validation fails + * @throws CyclicDependencyException if a cycle is detected in the dependency graph + */ + private List validateDependencyGraph(RuleTask task, String targetType, Map rulesInGraph) { + try { + // Attempt to topologically sort the dependency graph + List sortedRules = performTopologicalSort(rulesInGraph); + + // If successful, the graph is acyclic and valid + return sortedRules; + + } catch (CyclicDependencyException exception) { + // Cycle detected - report as compilation error + reportError(null, + "Cyclic dependency detected in %s rules for target type %s: %s. " + + "Review the 'dependsOn' and 'executeBefore' attributes to eliminate the cycle.", + task, + targetType, + exception.getMessage()); + + // Re-throw the exception to signal validation failure + throw exception; + } + } + + /** + * Performs topological sort with cycle detection on a rule dependency graph. + * + *

This method implements Kahn's algorithm for topological sorting with + * explicit cycle detection. It builds a directed graph from the rule dependencies + * and attempts to sort the nodes topologically. + * + *

The algorithm: + *

    + *
  1. Builds an adjacency list representation of the dependency graph
  2. + *
  3. Performs depth-first search from each unvisited node
  4. + *
  5. Detects cycles by tracking node visit states
  6. + *
  7. Returns topologically sorted rules if no cycles exist
  8. + *
+ * + * @param rulesInGraph the rules to sort topologically + * @return list of rules in topological order + * @throws CyclicDependencyException if a cycle is detected in the dependency graph + */ + private List performTopologicalSort(Map rulesInGraph) { + // Build adjacency list representation of the dependency graph + Map> dependencyGraph = buildDependencyGraph(rulesInGraph); + + // Track visit states for cycle detection + Map visitStates = new HashMap<>(); + + // Collect topologically sorted rules + List sortedRules = new ArrayList<>(); + + // Perform depth-first search from each unvisited node + for (String ruleName : dependencyGraph.keySet()) { + if (visitStates.get(ruleName) == null) { + performDepthFirstSearch(ruleName, dependencyGraph, visitStates, sortedRules, rulesInGraph); + } + } + + return sortedRules; + } + + /** + * Builds a directed graph representation of rule dependencies. + * + *

This method constructs an adjacency list representation of the dependency + * graph from the rule metadata. It handles both types of dependencies: + *

    + *
  • dependsOn: Current rule depends on other rules
  • + *
  • executeBefore: Current rule must execute before other rules
  • + *
+ * + *

The resulting graph has edges pointing from dependent rules to their dependencies, + * allowing for standard topological sorting algorithms. + * + * @param rulesInGraph the rules to build a dependency graph for + * @return adjacency list representation of the dependency graph + */ + private Map> buildDependencyGraph(Map rulesInGraph) { + Map> dependencyGraph = new HashMap<>(); + + // Initialize adjacency lists for all rules + rulesInGraph.keySet().forEach(ruleName -> dependencyGraph.put(ruleName, new ArrayList<>())); + + // Build edges from rule dependencies + for (RuleMetadata rule : rulesInGraph.values()) { + String currentRule = rule.className(); + + // Add edges for "dependsOn" relationships + // Current rule depends on these rules (edges: current -> dependency) + rule.dependsOn().stream() + .filter(dependencyGraph::containsKey) // Only include dependencies in this graph + .forEach(dependency -> dependencyGraph.get(currentRule).add(dependency)); + + // Add edges for "executeBefore" relationships + // Current rule executes before these rules (edges: predecessor -> current) + rule.executeBefore().stream() + .filter(dependencyGraph::containsKey) // Only include rules in this graph + .forEach(predecessor -> dependencyGraph.get(predecessor).add(currentRule)); + } + + return dependencyGraph; + } + + /** + * Performs depth-first search for topological sorting with cycle detection. + * + *

This method implements the core DFS algorithm for topological sorting. + * It uses a three-color scheme to detect cycles: + *

    + *
  • White (null): Unvisited node
  • + *
  • Gray (VISITING): Currently being processed
  • + *
  • Black (VISITED): Completely processed
  • + *
+ * + *

A cycle is detected when we encounter a gray node during traversal, + * indicating a back edge in the dependency graph. + * + * @param currentRule the current rule being processed + * @param dependencyGraph the adjacency list representation of dependencies + * @param visitStates the visit state tracking for cycle detection + * @param sortedRules the accumulator for topologically sorted rules + * @param rulesInGraph the complete rule metadata map + * @throws CyclicDependencyException if a cycle is detected + */ + private void performDepthFirstSearch(String currentRule, + Map> dependencyGraph, + Map visitStates, + List sortedRules, + Map rulesInGraph) { + // Mark current node as being visited (gray) + visitStates.put(currentRule, VisitState.VISITING); + + // Process all dependencies of the current rule + for (String dependencyRule : dependencyGraph.getOrDefault(currentRule, Collections.emptyList())) { + VisitState dependencyState = visitStates.get(dependencyRule); + + if (dependencyState == VisitState.VISITING) { + // Found a back edge - cycle detected! + throw new CyclicDependencyException( + String.format("Cycle detected: %s depends on %s", currentRule, dependencyRule)); + } + + if (dependencyState == null) { + // Unvisited node - recursively process it + performDepthFirstSearch(dependencyRule, dependencyGraph, visitStates, sortedRules, rulesInGraph); + } + + // If dependencyState == VISITED, we've already processed this node + } + + // Mark current node as completely processed (black) + visitStates.put(currentRule, VisitState.VISITED); + + // Add to sorted list (post-order traversal gives reverse topological order) + sortedRules.add(rulesInGraph.get(currentRule)); + } + + /** + * Reports a compilation error with formatted message. + * + *

This utility method provides a consistent way to report compilation errors + * with proper formatting and optional element context. The errors are reported + * through the compiler's messaging system and will cause the compilation to fail. + * + * @param element the program element associated with the error (may be null) + * @param messageFormat the format string for the error message + * @param arguments the arguments for the format string + */ + private void reportError(Element element, String messageFormat, Object... arguments) { + processingEnv.getMessager().printMessage( + Diagnostic.Kind.ERROR, + String.format(messageFormat, arguments), + element + ); + } + + /** + * Generates a Java source file containing the precomputed rule graph. + * + *

This method creates a class that stores the topologically sorted rules + * for each task and target type combination. The generated class can be used + * at runtime to avoid duplicate validation and sorting in the RuleService. + * + * @param sortedRulesByTaskAndType map of task to target type to sorted rule class names + */ + private void generatePrecomputedRuleGraphClass( + Map>> sortedRulesByTaskAndType) { + + try { + // Create a fully qualified class name for the generated class + String packageName = "edu.kit.datamanager.idoris.rules.logic"; + String className = "PrecomputedRuleGraphImpl"; + String fullClassName = packageName + "." + className; + + // Create a new Java file in the specified package + JavaFileObject sourceFile = processingEnv.getFiler() + .createSourceFile(fullClassName); + + try (PrintWriter writer = new PrintWriter(sourceFile.openWriter())) { + // Write package declaration and imports + writer.println("package " + packageName + ";"); + writer.println(); + writer.println("import java.util.List;"); + writer.println("import java.util.Map;"); + writer.println("import java.util.HashMap;"); + writer.println("import java.util.ArrayList;"); + writer.println("import java.util.Collections;"); + writer.println(); + + // Write class javadoc and declaration + writer.println("/**"); + writer.println(" * Generated implementation of PrecomputedRuleGraph."); + writer.println(" *

"); + writer.println(" * This class was automatically generated by RuleAnnotationProcessor"); + writer.println(" * and contains the precomputed, topologically sorted rule dependency graphs."); + writer.println(" *

"); + writer.println(" * DO NOT MODIFY THIS FILE - it will be regenerated during compilation."); + writer.println(" */"); + writer.println("public class " + className + " extends PrecomputedRuleGraph {"); + writer.println(); + + // Write constructor + writer.println(" /**"); + writer.println(" * Constructs the precomputed rule graph with validated and sorted rule dependencies."); + writer.println(" */"); + writer.println(" public " + className + "() {"); + writer.println(" super(initRuleGraph());"); + writer.println(" }"); + writer.println(); + + // Write initialization method + writer.println(" /**"); + writer.println(" * Initializes the rule graph with precomputed, topologically sorted rules."); + writer.println(" *"); + writer.println(" * @return map of task to target type to sorted rule class names"); + writer.println(" */"); + writer.println(" private static Map>> initRuleGraph() {"); + writer.println(" Map>> graph = new HashMap<>();"); + writer.println(); + + // Write task-level entries + for (Map.Entry>> taskEntry : sortedRulesByTaskAndType.entrySet()) { + RuleTask task = taskEntry.getKey(); + Map> targetMap = taskEntry.getValue(); + + if (!targetMap.isEmpty()) { + writer.println(" // Rules for task " + task.name()); + writer.println(" Map> " + task.name().toLowerCase() + "Rules = new HashMap<>();"); + writer.println(" graph.put(RuleTask." + task.name() + ", " + task.name().toLowerCase() + "Rules);"); + writer.println(); + + // Write target-level entries + for (Map.Entry> targetEntry : targetMap.entrySet()) { + String targetType = targetEntry.getKey(); + List ruleClasses = targetEntry.getValue(); + + if (!ruleClasses.isEmpty()) { + String targetVarName = "rules_" + task.name().toLowerCase() + "_" + + targetType.replaceAll("[^a-zA-Z0-9]", "_"); + + writer.println(" // Rules for " + targetType); + writer.println(" List " + targetVarName + " = new ArrayList<>();"); + writer.println(" " + task.name().toLowerCase() + "Rules.put(\"" + targetType + "\", " + targetVarName + ");"); + + // Write individual rule class names + for (String ruleClass : ruleClasses) { + writer.println(" " + targetVarName + ".add(\"" + ruleClass + "\");"); + } + writer.println(); + } + } + } + } + + writer.println(" return graph;"); + writer.println(" }"); + writer.println("}"); + } + + processingEnv.getMessager().printMessage( + Diagnostic.Kind.NOTE, + "Generated precomputed rule graph class: " + fullClassName + ); + + } catch (IOException e) { + processingEnv.getMessager().printMessage( + Diagnostic.Kind.ERROR, + "Failed to generate precomputed rule graph class: " + e.getMessage() + ); + } + } + + /** + * Enumeration representing the visit state of a node during depth-first search. + * + *

This three-color scheme is used for cycle detection in the dependency graph: + *

    + *
  • VISITING (Gray): Node is currently being processed
  • + *
  • VISITED (Black): Node has been completely processed
  • + *
  • Unvisited (White): Represented by null in the visit state map
  • + *
+ */ + private enum VisitState { + /** + * Node is currently being processed (gray in graph theory) + */ + VISITING, + /** + * Node has been completely processed (black in graph theory) + */ + VISITED + } + + /** + * Exception thrown when a cyclic dependency is detected in the rule dependency graph. + * + *

This exception is used internally during topological sorting to signal + * that a cycle has been detected. It's caught and converted to a compilation + * error for user-friendly reporting. + */ + private static class CyclicDependencyException extends RuntimeException { + /** + * Constructs a new cyclic dependency exception with the specified detail message. + * + * @param message the detail message describing the cycle + */ + public CyclicDependencyException(String message) { + super(message); + } + } + + /** + * Immutable record containing metadata about a rule class. + * + *

This record encapsulates all the information needed for dependency validation: + *

    + *
  • className: Fully qualified name of the rule class
  • + *
  • dependsOn: List of rule classes this rule depends on
  • + *
  • executeBefore: List of rule classes this rule must execute before
  • + *
+ * + *

The record is used to build dependency graphs and perform topological sorting + * for cycle detection. + * + * @param className the fully qualified name of the rule class + * @param dependsOn list of fully qualified names of rules this rule depends on + * @param executeBefore list of fully qualified names of rules this rule must execute before + */ + private record RuleMetadata( + String className, + List dependsOn, + List executeBefore + ) { + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index f5f3219..cbff5d0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,21 @@ +/* + * 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. + */ + rootProject.name = 'idoris' + +//include 'build:generated:sources:annotationProcessor:java:test:annotationProcessors' +include 'rules-processor' +include 'rules-api' \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/IdorisApplication.java b/src/main/java/edu/kit/datamanager/idoris/IdorisApplication.java index 32fb56f..b442ae6 100644 --- a/src/main/java/edu/kit/datamanager/idoris/IdorisApplication.java +++ b/src/main/java/edu/kit/datamanager/idoris/IdorisApplication.java @@ -39,6 +39,10 @@ public class IdorisApplication { public static void main(String[] args) { SpringApplication.run(IdorisApplication.class, args); + System.out.println(); + System.out.println("---------------------------------"); + System.out.println("IDORIS started successfully."); + System.out.println("---------------------------------"); } @Bean diff --git a/src/main/java/edu/kit/datamanager/idoris/configuration/ApplicationProperties.java b/src/main/java/edu/kit/datamanager/idoris/configuration/ApplicationProperties.java index 0210532..521d66c 100644 --- a/src/main/java/edu/kit/datamanager/idoris/configuration/ApplicationProperties.java +++ b/src/main/java/edu/kit/datamanager/idoris/configuration/ApplicationProperties.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. @@ -15,17 +15,14 @@ */ package edu.kit.datamanager.idoris.configuration; -import edu.kit.datamanager.idoris.validators.ValidationMessage; +import edu.kit.datamanager.idoris.rules.logic.OutputMessage; import jakarta.validation.constraints.NotNull; -import lombok.Data; -import lombok.EqualsAndHashCode; import lombok.Getter; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; +import org.springframework.context.annotation.Configuration; import org.springframework.validation.annotation.Validated; -import static edu.kit.datamanager.idoris.validators.ValidationMessage.MessageSeverity.INFO; +import static edu.kit.datamanager.idoris.rules.logic.OutputMessage.MessageSeverity.INFO; /** * This class is used to configure the application. @@ -33,31 +30,51 @@ * * @author maximiliani */ -@ConfigurationProperties(prefix = "idoris") -@Component -@Data +@Configuration @Getter @Validated -@EqualsAndHashCode 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}") + @Value("${idoris.validation-policy:LAX}") @NotNull private ValidationPolicy validationPolicy = ValidationPolicy.LAX; /** * The lowest severity level that is shown to the user. * - * @see ValidationMessage.MessageSeverity + * @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 */ - @Value("${idoris.validation-level}") + @Value("${idoris.pid-generation}") @NotNull - private ValidationMessage.MessageSeverity validationLevel = INFO; + private PIDGeneration pidGeneration = PIDGeneration.LOCAL; /** * The policy to use for validating the input. @@ -69,4 +86,9 @@ public class ApplicationProperties { public enum ValidationPolicy { STRICT, LAX } + + public enum PIDGeneration { + LOCAL, + TYPED_PID_MAKER, + } } diff --git a/src/main/java/edu/kit/datamanager/idoris/configuration/RepositoryRestConfig.java b/src/main/java/edu/kit/datamanager/idoris/configuration/RepositoryRestConfig.java index a8d1484..118ac84 100644 --- a/src/main/java/edu/kit/datamanager/idoris/configuration/RepositoryRestConfig.java +++ b/src/main/java/edu/kit/datamanager/idoris/configuration/RepositoryRestConfig.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. @@ -20,13 +20,10 @@ import edu.kit.datamanager.idoris.dao.ITypeProfileDao; import edu.kit.datamanager.idoris.domain.VisitableElement; import edu.kit.datamanager.idoris.domain.entities.*; -import edu.kit.datamanager.idoris.validators.ValidationMessage; -import edu.kit.datamanager.idoris.validators.ValidationResult; -import edu.kit.datamanager.idoris.validators.VisitableElementValidator; -import edu.kit.datamanager.idoris.visitors.InheritanceValidator; -import edu.kit.datamanager.idoris.visitors.SubSchemaRelationValidator; -import edu.kit.datamanager.idoris.visitors.SyntaxValidator; -import edu.kit.datamanager.idoris.visitors.Visitor; +import edu.kit.datamanager.idoris.rules.logic.OutputMessage; +import edu.kit.datamanager.idoris.rules.logic.RuleService; +import edu.kit.datamanager.idoris.rules.logic.RuleTask; +import edu.kit.datamanager.idoris.rules.validation.ValidationResult; import io.netty.util.Attribute; import lombok.extern.java.Log; import org.springframework.beans.factory.annotation.Autowired; @@ -41,6 +38,7 @@ import org.springframework.hateoas.server.LinkBuilder; import org.springframework.hateoas.server.RepresentationModelProcessor; import org.springframework.stereotype.Component; +import org.springframework.validation.Errors; import org.springframework.web.servlet.config.annotation.CorsRegistry; import java.util.List; @@ -55,19 +53,22 @@ @Log public class RepositoryRestConfig implements RepositoryRestConfigurer { private final ApplicationProperties applicationProperties; + private final RuleService ruleService; @Autowired - public RepositoryRestConfig(ApplicationProperties applicationProperties) { + public RepositoryRestConfig(ApplicationProperties applicationProperties, RuleService ruleService) { this.applicationProperties = applicationProperties; + this.ruleService = ruleService; } + @Override public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) { config.exposeIdsFor( Attribute.class, - BasicDataType.class, + AtomicDataType.class, Operation.class, - OperationTypeProfile.class, + TechnologyInterface.class, TypeProfile.class ); @@ -78,9 +79,10 @@ public void configureRepositoryRestConfiguration(RepositoryRestConfiguration con @Override public void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener v) { - v.addValidator("beforeSave", new VisitableElementValidator(applicationProperties)); - v.addValidator("beforeCreate", new VisitableElementValidator(applicationProperties)); - v.addValidator("afterLinkSave", new VisitableElementValidator(applicationProperties)); + RuleBasedVisitableElementValidator validator = new RuleBasedVisitableElementValidator(ruleService, applicationProperties); + v.addValidator("beforeSave", validator); + v.addValidator("beforeCreate", validator); + v.addValidator("afterLinkSave", validator); } @Bean @@ -116,35 +118,108 @@ public EntityModel process(EntityModel model) { } @Bean - public RepresentationModelProcessor> validatorProcessor() { + public RepresentationModelProcessor> validatorProcessor(RuleService ruleService) { return model -> { if (model instanceof EntityModel && ((EntityModel) model).getContent() instanceof VisitableElement element) { - Set> validators = Set.of( - new SubSchemaRelationValidator(), - new SyntaxValidator(), - new InheritanceValidator() + // Use RuleService to process validation with the VALIDATE task + ValidationResult validationResult = ruleService.executeRules( + RuleTask.VALIDATE, + element, + ValidationResult::new ); - Map>> results = validators.stream() - .peek(visitor -> log.info("Executing validation for " + visitor.getClass().getSimpleName())) - .map(visitor -> Map.entry(visitor.getClass().getSimpleName(), element.execute(visitor))) - .peek(entry -> log.info("Validation result for " + entry.getKey() + ": " + entry.getValue())) - .map(entry -> Map.entry( - entry.getKey(), - entry.getValue() - .getValidationMessages() - .entrySet() - .stream() - .filter(e -> e.getKey().isHigherOrEqualTo(applicationProperties.getValidationLevel())) - .filter(e -> !e.getValue().isEmpty()) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))) - .peek(entry -> log.info("Filtered validation result for " + entry.getKey() + ": " + entry.getValue())) - .filter(entry -> !entry.getValue().isEmpty()) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - - if (!results.isEmpty()) return CollectionModel.of(Set.of(results, model)); + // Convert ValidationResult to the expected format for the response + if (!validationResult.isEmpty()) { + Map> filteredMessages = + validationResult.getOutputMessages() + .entrySet() + .stream() + .filter(entry -> entry.getKey().isHigherOrEqualTo(applicationProperties.getValidationLevel())) + .filter(entry -> !entry.getValue().isEmpty()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + if (!filteredMessages.isEmpty()) { + Map results = Map.of( + "validationResult", filteredMessages, + "originalModel", model + ); + return CollectionModel.of(Set.of(results)); + } + } } return model; }; } -} + + /** + * Spring Data REST validator that uses the new rule-based validation system. + * This validator integrates with Spring's validation framework and executes + * all applicable validation rules through the RuleService. + */ + private static class RuleBasedVisitableElementValidator implements org.springframework.validation.Validator { + private final RuleService ruleService; + private final ApplicationProperties applicationProperties; + + public RuleBasedVisitableElementValidator(RuleService ruleService, ApplicationProperties applicationProperties) { + this.ruleService = ruleService; + this.applicationProperties = applicationProperties; + } + + @Override + public boolean supports(Class clazz) { + return VisitableElement.class.isAssignableFrom(clazz); + } + + @Override + public void validate(Object target, Errors errors) { + if (!(target instanceof VisitableElement element)) { + return; + } + + try { + // Execute validation rules using RuleService + ValidationResult result = ruleService.executeRules( + RuleTask.VALIDATE, + element, + ValidationResult::new + ); + + // Convert ValidationResult to Spring validation errors + convertToSpringErrors(result, errors); + + } catch (Exception e) { + log.severe("Error during rule-based validation: " + e.getMessage()); + errors.reject("validation.error", "Validation failed due to internal error"); + } + } + + /** + * Converts ValidationResult messages to Spring validation errors. + * Only includes messages that meet the configured validation level threshold. + */ + private void convertToSpringErrors(ValidationResult result, org.springframework.validation.Errors errors) { + if (result == null || result.isEmpty()) { + return; + } + + result.getOutputMessages().entrySet().stream() + .filter(entry -> entry.getKey().isHigherOrEqualTo(applicationProperties.getValidationLevel())) + .forEach(entry -> { + OutputMessage.MessageSeverity severity = entry.getKey(); + List messages = entry.getValue(); + + for (OutputMessage message : messages) { + String errorCode = "validation." + severity.name().toLowerCase(); + String defaultMessage = message.message(); + + if (severity == OutputMessage.MessageSeverity.ERROR) { + errors.reject(errorCode, defaultMessage); + } else { + // For warnings and info, we can still add them but they won't fail validation + errors.reject("validation.warning", defaultMessage); + } + } + }); + } + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/configuration/TypedPIDMakerConfig.java b/src/main/java/edu/kit/datamanager/idoris/configuration/TypedPIDMakerConfig.java new file mode 100644 index 0000000..8990149 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/configuration/TypedPIDMakerConfig.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.configuration; + +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +@Component +@ConfigurationProperties("idoris.typed-pid-maker") +@Validated +@AutoConfigureAfter(value = ApplicationProperties.class) +@ConditionalOnBean(value = ApplicationProperties.class) +@ConditionalOnExpression( + "#{ '${idoris.pid-generation}' eq T(edu.kit.datamanager.idoris.configuration.ApplicationProperties.PIDGeneration).TYPED_PID_MAKER.name() }" +) +@Getter +@Setter +public class TypedPIDMakerConfig { + /** + * Put metadata of the GenericIDORISEntity into the PID record. + * + * @see edu.kit.datamanager.idoris.domain.GenericIDORISEntity + */ + private boolean meaningfulPIDRecords = true; + + /** + * Update existing PID records with the latest metadata from the GenericIDORISEntity. + * If set to false, existing PID records will not be updated, + * but new records will still be created with the latest metadata. + */ + private boolean updatePIDRecords = true; + + /** + * The base URL for the Typed PID Maker service. + * This is required when the service is enabled. + */ + @NotNull(message = "Base URL is required when enabled is true") + private String baseUrl; + + /** + * The timeout in milliseconds for requests to the Typed PID Maker service. + */ + private int timeout = 5000; +} diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/IBasicDataTypeDao.java b/src/main/java/edu/kit/datamanager/idoris/dao/IAtomicDataTypeDao.java similarity index 64% rename from src/main/java/edu/kit/datamanager/idoris/dao/IBasicDataTypeDao.java rename to src/main/java/edu/kit/datamanager/idoris/dao/IAtomicDataTypeDao.java index f4207d3..eb73c84 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/IBasicDataTypeDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/dao/IAtomicDataTypeDao.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,14 +16,14 @@ package edu.kit.datamanager.idoris.dao; -import edu.kit.datamanager.idoris.domain.entities.BasicDataType; +import edu.kit.datamanager.idoris.domain.entities.AtomicDataType; import org.springframework.data.neo4j.repository.query.Query; import org.springframework.data.rest.core.annotation.RepositoryRestResource; import org.springframework.data.rest.core.annotation.RestResource; -@RepositoryRestResource(collectionResourceRel = "basicDataTypes", path = "basicDataTypes") -public interface IBasicDataTypeDao extends IAbstractRepo { +@RepositoryRestResource(collectionResourceRel = "atomicDataTypes", path = "atomicDataTypes") +public interface IAtomicDataTypeDao extends IGenericRepo { @RestResource(exported = false) - @Query("MATCH (d:BasicDataType {pid: $pid})-[:inheritsFrom*]->(d2:BasicDataType) RETURN d2") - Iterable findAllInInheritanceChain(String pid); + @Query("MATCH (d:AtomicDataType {pid: $pid})-[:inheritsFrom*]->(d2:AtomicDataType) RETURN d2") + Iterable findAllInInheritanceChain(String pid); } diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/IAttributeDao.java b/src/main/java/edu/kit/datamanager/idoris/dao/IAttributeDao.java index 4f97dca..70c530b 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/IAttributeDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/dao/IAttributeDao.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. @@ -21,7 +21,7 @@ import org.springframework.data.rest.core.annotation.RepositoryRestResource; @RepositoryRestResource(collectionResourceRel = "attributes", path = "attributes") -public interface IAttributeDao extends IAbstractRepo { +public interface IAttributeDao extends IGenericRepo { @Query("MATCH (n:Attribute)" + " WHERE size([(n)-[:dataType]->() | 1]) = 1 AND NOT (n)<-[]-()" + " WITH n" + diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/IDataTypeDao.java b/src/main/java/edu/kit/datamanager/idoris/dao/IDataTypeDao.java index 4ad37cc..e6704e5 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/IDataTypeDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/dao/IDataTypeDao.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. @@ -22,9 +22,7 @@ import org.springframework.data.rest.core.annotation.RepositoryRestResource; @RepositoryRestResource(collectionResourceRel = "dataTypes", path = "dataTypes") -public interface IDataTypeDao extends IAbstractRepo { - Iterable findAllByLicenseUrl(String licenseUrl); - +public interface IDataTypeDao extends IGenericRepo { @Query("MATCH (d:DataType {pid: $pid})-[:inheritsFrom*]->(d2:DataType) RETURN d2") Iterable findAllInInheritanceChain(String pid); diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/IAbstractRepo.java b/src/main/java/edu/kit/datamanager/idoris/dao/IGenericRepo.java similarity index 62% rename from src/main/java/edu/kit/datamanager/idoris/dao/IAbstractRepo.java rename to src/main/java/edu/kit/datamanager/idoris/dao/IGenericRepo.java index 2f3461e..96d6707 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/IAbstractRepo.java +++ b/src/main/java/edu/kit/datamanager/idoris/dao/IGenericRepo.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,11 +16,17 @@ package edu.kit.datamanager.idoris.dao; +import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.repository.ListCrudRepository; import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.PagingAndSortingRepository; @NoRepositoryBean -public interface IAbstractRepo extends Neo4jRepository, ListCrudRepository, PagingAndSortingRepository { +public interface IGenericRepo extends Neo4jRepository, ListCrudRepository, PagingAndSortingRepository { + // This interface serves as a marker for generic repositories. + // It can be extended by specific repositories to inherit common methods. + // Additional methods can be defined here if needed. + + T findByPid(String pid); } diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/IOperationDao.java b/src/main/java/edu/kit/datamanager/idoris/dao/IOperationDao.java index 4728eb0..b616514 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/IOperationDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/dao/IOperationDao.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. @@ -21,7 +21,7 @@ import org.springframework.data.rest.core.annotation.RepositoryRestResource; @RepositoryRestResource(collectionResourceRel = "operations", path = "operations") -public interface IOperationDao extends IAbstractRepo { +public interface IOperationDao extends IGenericRepo { // @Query("optional MATCH (:DataType {pid: $pid})-[:attributes|inheritsFrom*]->(:DataType)<-[:dataType]-(:Attribute)<-[:executableOn]-(o:Operation) RETURN o") @Query(""" MATCH (d:DataType {pid: $pid})<-[:dataType]-(:Attribute)<-[:executableOn]-(o:Operation) RETURN o diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/IStandardsDao.java b/src/main/java/edu/kit/datamanager/idoris/dao/ITechnologyInterfaceDao.java similarity index 68% rename from src/main/java/edu/kit/datamanager/idoris/dao/IStandardsDao.java rename to src/main/java/edu/kit/datamanager/idoris/dao/ITechnologyInterfaceDao.java index 85adfee..b47a188 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/IStandardsDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/dao/ITechnologyInterfaceDao.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,9 +16,9 @@ package edu.kit.datamanager.idoris.dao; -import edu.kit.datamanager.idoris.domain.entities.Standard; +import edu.kit.datamanager.idoris.domain.entities.TechnologyInterface; import org.springframework.data.rest.core.annotation.RepositoryRestResource; -@RepositoryRestResource(collectionResourceRel = "standards", path = "standards") -public interface IStandardsDao extends IAbstractRepo { +@RepositoryRestResource(collectionResourceRel = "technologyInterfaces", path = "technologyInterfaces") +public interface ITechnologyInterfaceDao extends IGenericRepo { } diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/ITypeProfileDao.java b/src/main/java/edu/kit/datamanager/idoris/dao/ITypeProfileDao.java index be43a58..c996067 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/ITypeProfileDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/dao/ITypeProfileDao.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. @@ -23,7 +23,7 @@ @OpenAPIDefinition @RepositoryRestResource(collectionResourceRel = "typeProfiles", path = "typeProfiles") -public interface ITypeProfileDao extends IAbstractRepo { +public interface ITypeProfileDao extends IGenericRepo { @Query("MATCH (d:TypeProfile {pid: $pid})-[i:inheritsFrom*]->(d2:TypeProfile)-[profileAttribute:attributes]->(dataType:DataType) RETURN i, d2, collect(profileAttribute), collect(dataType)") Iterable findAllTypeProfilesWithTheirAttributesInInheritanceChain(String pid); diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/IUserDao.java b/src/main/java/edu/kit/datamanager/idoris/dao/IUserDao.java index ae5b48b..5c51f60 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/IUserDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/dao/IUserDao.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. @@ -17,11 +17,14 @@ package edu.kit.datamanager.idoris.dao; import edu.kit.datamanager.idoris.domain.entities.User; +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.rest.core.annotation.RepositoryRestResource; @RepositoryRestResource(collectionResourceRel = "users", path = "users") -public interface IUserDao extends IAbstractRepo { +public interface IUserDao extends Neo4jRepository, ListCrudRepository, PagingAndSortingRepository { @Query("MATCH (u:ORCiDUser) RETURN u") Iterable findAllORCiDUsers(); diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/GenericIDORISEntity.java b/src/main/java/edu/kit/datamanager/idoris/domain/GenericIDORISEntity.java index d6c6028..ec29207 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/GenericIDORISEntity.java +++ b/src/main/java/edu/kit/datamanager/idoris/domain/GenericIDORISEntity.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,17 +16,15 @@ package edu.kit.datamanager.idoris.domain; -import edu.kit.datamanager.idoris.domain.entities.License; +import edu.kit.datamanager.idoris.domain.entities.Reference; import edu.kit.datamanager.idoris.domain.entities.User; -import edu.kit.datamanager.idoris.domain.relationships.StandardApplicability; +import edu.kit.datamanager.idoris.pids.ConfigurablePIDGenerator; import lombok.*; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.annotation.Version; import org.springframework.data.neo4j.core.schema.GeneratedValue; -import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Relationship; -import org.springframework.data.neo4j.core.support.UUIDStringGenerator; import java.io.Serializable; import java.time.Instant; @@ -35,13 +33,16 @@ @Getter @Setter @EqualsAndHashCode(callSuper = true) -@RequiredArgsConstructor -@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) public abstract class GenericIDORISEntity extends VisitableElement implements Serializable { - @Id - @GeneratedValue(UUIDStringGenerator.class) + @GeneratedValue(ConfigurablePIDGenerator.class) String pid; + String name; + + String description; + @Version Long version; @@ -51,12 +52,10 @@ public abstract class GenericIDORISEntity extends VisitableElement implements Se @LastModifiedDate Instant lastModifiedAt; + Set expectedUseCases; + @Relationship(value = "contributors", direction = Relationship.Direction.OUTGOING) Set contributors; - @Relationship(value = "license", direction = Relationship.Direction.OUTGOING) - License license; - - @Relationship(value = "standards", direction = Relationship.Direction.OUTGOING) - Set standards; -} + Set references; +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/VisitableElement.java b/src/main/java/edu/kit/datamanager/idoris/domain/VisitableElement.java index 873a967..e898802 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/VisitableElement.java +++ b/src/main/java/edu/kit/datamanager/idoris/domain/VisitableElement.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. @@ -17,12 +17,17 @@ package edu.kit.datamanager.idoris.domain; import com.fasterxml.jackson.annotation.JsonIgnore; -import edu.kit.datamanager.idoris.visitors.Visitor; +import edu.kit.datamanager.idoris.rules.logic.RuleOutput; +import edu.kit.datamanager.idoris.rules.logic.Visitor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.data.annotation.Id; import org.springframework.data.neo4j.core.schema.CompositeProperty; +import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.support.UUIDStringGenerator; import java.time.Instant; import java.util.HashMap; @@ -30,14 +35,19 @@ @Getter @Setter -public abstract class VisitableElement { +@EqualsAndHashCode +public abstract class VisitableElement implements edu.kit.datamanager.idoris.rules.logic.VisitableElement { private static final Logger LOG = LoggerFactory.getLogger(VisitableElement.class); @CompositeProperty @JsonIgnore private Map visitedBy = new HashMap<>(); - public T execute(Visitor visitor, Object... args) { + @Id + @GeneratedValue(UUIDStringGenerator.class) + private String internalId; + + public > T execute(Visitor visitor, Object... args) { // if (isVisitedBy(visitor)) { // LOG.info("Class {} already visited by visitor {}. ABORTING!", this.getClass().getName(), visitor.getClass().getName()); // return null; @@ -48,7 +58,7 @@ public T execute(Visitor visitor, Object... args) { // } } - protected abstract T accept(Visitor visitor, Object... args); + protected abstract > T accept(Visitor visitor, Object... args); public Instant wasVisistedBy(Visitor visitor) { return visitedBy.get(visitor.getClass().getName()); @@ -61,4 +71,8 @@ public void clearVisitedByAll() { public void clearVisitedBy(Visitor visitor) { visitedBy.remove(visitor.getClass().getName()); } + + public String getId() { + return internalId; + } } diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/BasicDataType.java b/src/main/java/edu/kit/datamanager/idoris/domain/entities/AtomicDataType.java similarity index 60% rename from src/main/java/edu/kit/datamanager/idoris/domain/entities/BasicDataType.java rename to src/main/java/edu/kit/datamanager/idoris/domain/entities/AtomicDataType.java index 9bd880f..9a23a6a 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/BasicDataType.java +++ b/src/main/java/edu/kit/datamanager/idoris/domain/entities/AtomicDataType.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,55 +16,47 @@ package edu.kit.datamanager.idoris.domain.entities; -import edu.kit.datamanager.idoris.domain.enums.Category; import edu.kit.datamanager.idoris.domain.enums.PrimitiveDataTypes; -import edu.kit.datamanager.idoris.visitors.Visitor; +import edu.kit.datamanager.idoris.rules.logic.RuleOutput; +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.Node; -import org.springframework.data.neo4j.core.schema.Property; import org.springframework.data.neo4j.core.schema.Relationship; import java.util.Set; -@Node("BasicDataType") +@Node("AtomicDataType") @Getter @Setter @AllArgsConstructor @NoArgsConstructor -public final class BasicDataType extends DataType { +public final class AtomicDataType extends DataType { @Relationship(value = "inheritsFrom", direction = Relationship.Direction.OUTGOING) - private BasicDataType inheritsFrom; + private AtomicDataType inheritsFrom; private PrimitiveDataTypes primitiveDataType; - private Category category = Category.Format; - private String unitName; - private String unitSymbol; - private String definedBy; - private String standard_uncertainty; + private String regularExpression; - private String restrictions; - private String regex; - // TODO: Make this an enum - private String regexFlavour = "ecma-262-RegExp"; - - @Property("enum") - private Set valueEnum; + private Set permittedValues; + private Set forbiddenValues; + private Integer minimum; + private Integer maximum; @Override - protected T accept(Visitor visitor, Object... args) { + protected > T accept(Visitor visitor, Object... args) { return visitor.visit(this, args); } @Override public boolean inheritsFrom(DataType dataType) { - if (dataType instanceof BasicDataType basicDataType) { - if (basicDataType.equals(this)) { + if (dataType instanceof AtomicDataType atomicDataType) { + if (atomicDataType.equals(this)) { return true; } - return inheritsFrom != null && inheritsFrom.inheritsFrom(basicDataType); + return inheritsFrom != null && inheritsFrom.inheritsFrom(atomicDataType); } return false; } diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/Attribute.java b/src/main/java/edu/kit/datamanager/idoris/domain/entities/Attribute.java index f577900..1f23fe2 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/Attribute.java +++ b/src/main/java/edu/kit/datamanager/idoris/domain/entities/Attribute.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,35 +16,27 @@ package edu.kit.datamanager.idoris.domain.entities; -import edu.kit.datamanager.idoris.domain.VisitableElement; -import edu.kit.datamanager.idoris.domain.enums.Obligation; -import edu.kit.datamanager.idoris.visitors.Visitor; +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; 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; -import org.springframework.data.neo4j.core.support.UUIDStringGenerator; -@Node +@Node("Attribute") @AllArgsConstructor @RequiredArgsConstructor @Getter @Setter -public class Attribute extends VisitableElement { - @Id - @GeneratedValue(UUIDStringGenerator.class) - private String pid; - private String name; - private String description; - - private boolean repeatable = false; - private Obligation obligation = Obligation.Mandatory; - private String value; +public class Attribute extends GenericIDORISEntity { + private String defaultValue; + private String constantValue; + private Integer lowerBoundCardinality = 0; + private Integer upperBoundCardinality; @Relationship(value = "dataType", direction = Relationship.Direction.OUTGOING) @NotNull @@ -54,7 +46,7 @@ public class Attribute extends VisitableElement { private Attribute override; @Override - protected T accept(Visitor visitor, Object... args) { + protected > T accept(Visitor visitor, Object... args) { return visitor.visit(this, args); } } diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/AttributeMapping.java b/src/main/java/edu/kit/datamanager/idoris/domain/entities/AttributeMapping.java index 7071496..75ba923 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/AttributeMapping.java +++ b/src/main/java/edu/kit/datamanager/idoris/domain/entities/AttributeMapping.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. @@ -17,13 +17,12 @@ package edu.kit.datamanager.idoris.domain.entities; import edu.kit.datamanager.idoris.domain.VisitableElement; -import edu.kit.datamanager.idoris.visitors.Visitor; +import edu.kit.datamanager.idoris.rules.logic.RuleOutput; +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; @@ -35,9 +34,6 @@ @AllArgsConstructor @NoArgsConstructor public class AttributeMapping extends VisitableElement implements Serializable { - @Id - @GeneratedValue - private String id; private String name; @Relationship(value = "input", direction = Relationship.Direction.INCOMING) @@ -51,7 +47,7 @@ public class AttributeMapping extends VisitableElement implements Serializable { private Attribute output; @Override - protected T accept(Visitor visitor, Object... args) { + protected > T accept(Visitor visitor, Object... args) { return visitor.visit(this, args); } } diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/DataType.java b/src/main/java/edu/kit/datamanager/idoris/domain/entities/DataType.java index 9ec474c..10c0616 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/DataType.java +++ b/src/main/java/edu/kit/datamanager/idoris/domain/entities/DataType.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. @@ -24,9 +24,6 @@ import lombok.RequiredArgsConstructor; import lombok.Setter; import org.springframework.data.neo4j.core.schema.Node; -import org.springframework.data.neo4j.core.schema.Property; - -import java.util.List; @Node("DataType") @Getter @@ -35,22 +32,18 @@ @RequiredArgsConstructor @JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION, property = "type") @JsonSubTypes({ - @JsonSubTypes.Type(value = BasicDataType.class, name = "BasicDataType"), + @JsonSubTypes.Type(value = AtomicDataType.class, name = "AtomicDataType"), @JsonSubTypes.Type(value = TypeProfile.class, name = "TypeProfile"), }) -public abstract sealed class DataType extends GenericIDORISEntity permits BasicDataType, TypeProfile { +public abstract sealed class DataType extends GenericIDORISEntity permits AtomicDataType, TypeProfile { private TYPES type; - private String name; - private String description; - private List expectedUses; - @Property("default") private String defaultValue; public abstract boolean inheritsFrom(DataType dataType); public enum TYPES { - BasicDataType, + AtomicDataType, TypeProfile } } diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/License.java b/src/main/java/edu/kit/datamanager/idoris/domain/entities/License.java deleted file mode 100644 index 1437259..0000000 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/License.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2024 Karlsruhe Institute of Technology - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package edu.kit.datamanager.idoris.domain.entities; - -import lombok.*; -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 java.io.Serializable; - -@Node("License") -@Getter -@Setter -@EqualsAndHashCode -@AllArgsConstructor -@RequiredArgsConstructor -public class License implements Serializable { - @Id - @GeneratedValue - private String pid; - private String name; - private String url; - - public License(String url) { - this.name = url; - this.url = url; - } -} diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/ORCiDUser.java b/src/main/java/edu/kit/datamanager/idoris/domain/entities/ORCiDUser.java index 7fdb939..44e58dc 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/ORCiDUser.java +++ b/src/main/java/edu/kit/datamanager/idoris/domain/entities/ORCiDUser.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. @@ -20,15 +20,15 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; import org.springframework.data.neo4j.core.schema.Node; +import java.net.URL; + @Getter -@Setter @AllArgsConstructor @NoArgsConstructor @Node("ORCiDUser") public final class ORCiDUser extends User { @JsonProperty("orcid") - private String orcid; + private URL orcid; } diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/Operation.java b/src/main/java/edu/kit/datamanager/idoris/domain/entities/Operation.java index 26c9820..6afc8ec 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/Operation.java +++ b/src/main/java/edu/kit/datamanager/idoris/domain/entities/Operation.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. @@ -17,17 +17,14 @@ package edu.kit.datamanager.idoris.domain.entities; import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; -import edu.kit.datamanager.idoris.visitors.Visitor; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; +import edu.kit.datamanager.idoris.rules.logic.RuleOutput; +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.Node; import org.springframework.data.neo4j.core.schema.Relationship; -import org.springframework.validation.annotation.Validated; import java.util.List; import java.util.Set; @@ -37,14 +34,8 @@ @Setter @AllArgsConstructor @RequiredArgsConstructor -@Validated public class Operation extends GenericIDORISEntity { - @NotBlank(message = "For better human readability and understanding, please provide a name for the operation.") - private String name; - private String description; - @Relationship(value = "executableOn", direction = Relationship.Direction.OUTGOING) - @NotNull(message = "Please specify the data type on which the operation can be executed.") private Attribute executableOn; @Relationship(value = "returns", direction = Relationship.Direction.INCOMING) private Set returns; @@ -52,13 +43,10 @@ public class Operation extends GenericIDORISEntity { private Set environment; @Relationship(value = "execution", direction = Relationship.Direction.OUTGOING) - @NotNull(message = "Please specify the steps of the operation.") - @Valid -// @Min(value = 1, message = "Please specify at least one step for the operation.") private List execution; @Override - protected T accept(Visitor visitor, Object... args) { + protected > T accept(Visitor visitor, Object... args) { return visitor.visit(this, args); } } diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/OperationStep.java b/src/main/java/edu/kit/datamanager/idoris/domain/entities/OperationStep.java index 63dc544..3b141a4 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/OperationStep.java +++ b/src/main/java/edu/kit/datamanager/idoris/domain/entities/OperationStep.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. @@ -17,19 +17,15 @@ package edu.kit.datamanager.idoris.domain.entities; import edu.kit.datamanager.idoris.domain.VisitableElement; -import edu.kit.datamanager.idoris.visitors.Visitor; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.PositiveOrZero; +import edu.kit.datamanager.idoris.domain.enums.ExecutionMode; +import edu.kit.datamanager.idoris.rules.logic.RuleOutput; +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; -import org.springframework.validation.annotation.Validated; import java.io.Serializable; import java.util.List; @@ -39,59 +35,30 @@ @RequiredArgsConstructor @Getter @Setter -@Validated public class OperationStep extends VisitableElement implements Serializable { - @Id - @GeneratedValue - private String id; + private Integer index; - @NotNull(message = "Execution order index must not be specified otherwise the order of steps is random.") - @PositiveOrZero - private Integer executionOrderIndex; - - @NotBlank(message = "For better human readability, please provide a title for the operation step.") private String name; - @NotNull(message = "An execution mode must be specified. Default is synchronous execution.") private ExecutionMode mode = ExecutionMode.sync; - @Relationship(value = "steps", direction = Relationship.Direction.OUTGOING) - private List steps; + @Relationship(value = "subSteps", direction = Relationship.Direction.OUTGOING) + private List subSteps; - @Relationship(value = "operation", direction = Relationship.Direction.OUTGOING) - private Operation operation; + @Relationship(value = "executeOperation", direction = Relationship.Direction.OUTGOING) + private Operation executeOperation; - @Relationship(value = "operationTypeProfile", direction = Relationship.Direction.OUTGOING) - private OperationTypeProfile operationTypeProfile; + @Relationship(value = "useTechnology", direction = Relationship.Direction.OUTGOING) + private TechnologyInterface useTechnology; - @Relationship(value = "attributes", direction = Relationship.Direction.INCOMING) - private List attributes; + @Relationship(value = "inputMappings", direction = Relationship.Direction.INCOMING) + private List inputMappings; - @Relationship(value = "output", direction = Relationship.Direction.OUTGOING) - private List output; + @Relationship(value = "outputMappings", direction = Relationship.Direction.OUTGOING) + private List outputMappings; @Override - protected T accept(Visitor visitor, Object... args) { + protected > T accept(Visitor visitor, Object... args) { return visitor.visit(this, args); } - - @Override - public String toString() { - return "OperationStep{" + - "id=" + id + - ", executionOrderIndex=" + executionOrderIndex + - ", name='" + name + '\'' + - ", mode=" + mode + - ", steps=" + steps + - ", operation=" + operation + - ", operationTypeProfile=" + operationTypeProfile + - ", attributes=" + attributes + - ", output=" + output + - "} " + super.toString(); - } - - public enum ExecutionMode { - sync, - async - } } diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/enums/Category.java b/src/main/java/edu/kit/datamanager/idoris/domain/entities/Reference.java similarity index 57% rename from src/main/java/edu/kit/datamanager/idoris/domain/enums/Category.java rename to src/main/java/edu/kit/datamanager/idoris/domain/entities/Reference.java index 5037431..b16cef5 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/enums/Category.java +++ b/src/main/java/edu/kit/datamanager/idoris/domain/entities/Reference.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,19 +14,8 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.domain.enums; +package edu.kit.datamanager.idoris.domain.entities; -import lombok.AllArgsConstructor; -import lombok.Getter; -@AllArgsConstructor -@Getter -public enum Category { - MeasurementUnit("Measurement Unit"), - Format("Format"), - Enumeration("Enumeration"), - CharacterSet("Character Set"), - Encoding("Encoding"), - Other("Other"); - private final String name; +public record Reference(String relationType, String targetPID) { } diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/Standard.java b/src/main/java/edu/kit/datamanager/idoris/domain/entities/Standard.java deleted file mode 100644 index 42588ac..0000000 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/Standard.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2024 Karlsruhe Institute of Technology - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package edu.kit.datamanager.idoris.domain.entities; - -import edu.kit.datamanager.idoris.domain.enums.StandardIssuer; -import lombok.*; -import org.springframework.data.neo4j.core.schema.GeneratedValue; -import org.springframework.data.neo4j.core.schema.Id; -import org.springframework.data.neo4j.core.schema.Node; - -@Node("Standard") -@Getter -@Setter -@ToString -@AllArgsConstructor -@RequiredArgsConstructor -public class Standard { - @Id - @GeneratedValue - private String pid; - - private String name; - private StandardIssuer issuer; - private String details; -} - diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/OperationTypeProfile.java b/src/main/java/edu/kit/datamanager/idoris/domain/entities/TechnologyInterface.java similarity index 66% rename from src/main/java/edu/kit/datamanager/idoris/domain/entities/OperationTypeProfile.java rename to src/main/java/edu/kit/datamanager/idoris/domain/entities/TechnologyInterface.java index f8c8f1c..e311354 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/OperationTypeProfile.java +++ b/src/main/java/edu/kit/datamanager/idoris/domain/entities/TechnologyInterface.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. @@ -17,27 +17,23 @@ package edu.kit.datamanager.idoris.domain.entities; import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; -import edu.kit.datamanager.idoris.visitors.Visitor; +import edu.kit.datamanager.idoris.rules.logic.RuleOutput; +import edu.kit.datamanager.idoris.rules.logic.Visitor; import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; import lombok.Setter; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Relationship; import java.util.Set; -@Node("OperationTypeProfile") +@Node("TechnologyInterface") @Getter @Setter +@RequiredArgsConstructor @AllArgsConstructor -@NoArgsConstructor -public class OperationTypeProfile extends GenericIDORISEntity { - private String name; - private String description; - - @Relationship(value = "inheritsFrom", direction = Relationship.Direction.OUTGOING) - private Set inheritsFrom; +public class TechnologyInterface extends GenericIDORISEntity { @Relationship(value = "attributes", direction = Relationship.Direction.INCOMING) private Set attributes; @@ -45,12 +41,10 @@ public class OperationTypeProfile extends GenericIDORISEntity { @Relationship(value = "outputs", direction = Relationship.Direction.OUTGOING) private Set outputs; - // @Relationship(value = "adapters", direction = Relationship.Direction.OUTGOING) - private Set adapters; - + private Set adapters = Set.of(); @Override - protected T accept(Visitor visitor, Object... args) { + protected > T accept(Visitor visitor, Object... args) { return visitor.visit(this, args); } } diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/entities/TypeProfile.java b/src/main/java/edu/kit/datamanager/idoris/domain/entities/TypeProfile.java index 22751d5..f2870f2 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/TypeProfile.java +++ b/src/main/java/edu/kit/datamanager/idoris/domain/entities/TypeProfile.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,8 +16,9 @@ package edu.kit.datamanager.idoris.domain.entities; -import edu.kit.datamanager.idoris.domain.enums.SubSchemaRelation; -import edu.kit.datamanager.idoris.visitors.Visitor; +import edu.kit.datamanager.idoris.domain.enums.CombinationOptions; +import edu.kit.datamanager.idoris.rules.logic.RuleOutput; +import edu.kit.datamanager.idoris.rules.logic.Visitor; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -40,12 +41,13 @@ public final class TypeProfile extends DataType { @Relationship(value = "attributes", direction = Relationship.Direction.OUTGOING) private Set attributes; - private boolean isEmbeddable = true; + private boolean permitEmbedding = true; private boolean isAbstract = false; - private SubSchemaRelation subSchemaRelation = SubSchemaRelation.allowAdditionalProperties; + private boolean allowAdditionalAttributes = true; + private CombinationOptions validationPolicy = CombinationOptions.ALL; @Override - protected T accept(Visitor visitor, Object... args) { + protected > 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/src/main/java/edu/kit/datamanager/idoris/domain/entities/User.java index e7579db..a3c69ea 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/entities/User.java +++ b/src/main/java/edu/kit/datamanager/idoris/domain/entities/User.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. @@ -23,9 +23,10 @@ import lombok.RequiredArgsConstructor; import lombok.Setter; import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; 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.support.UUIDStringGenerator; import java.io.Serializable; import java.time.Instant; @@ -44,8 +45,8 @@ public abstract sealed class User implements Serializable permits ORCiDUser, Tex @CreatedDate Instant createdAt; @Id - @GeneratedValue - private String id; + @GeneratedValue(UUIDStringGenerator.class) + private String internalId; private String type; } diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/enums/Obligation.java b/src/main/java/edu/kit/datamanager/idoris/domain/enums/CombinationOptions.java similarity index 78% rename from src/main/java/edu/kit/datamanager/idoris/domain/enums/Obligation.java rename to src/main/java/edu/kit/datamanager/idoris/domain/enums/CombinationOptions.java index 33122cf..c79ffdd 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/enums/Obligation.java +++ b/src/main/java/edu/kit/datamanager/idoris/domain/enums/CombinationOptions.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. @@ -21,8 +21,10 @@ @Getter @AllArgsConstructor -public enum Obligation { - Mandatory("Mandatory"), - Optional("Optional"); - private final String name; +public enum CombinationOptions { + NONE("none"), + ONE("one"), + ANY("any"), + ALL("all"); + private final String jsonName; } diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/enums/StandardIssuer.java b/src/main/java/edu/kit/datamanager/idoris/domain/enums/ExecutionMode.java similarity index 67% rename from src/main/java/edu/kit/datamanager/idoris/domain/enums/StandardIssuer.java rename to src/main/java/edu/kit/datamanager/idoris/domain/enums/ExecutionMode.java index 6c793be..cc91fdd 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/enums/StandardIssuer.java +++ b/src/main/java/edu/kit/datamanager/idoris/domain/enums/ExecutionMode.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. @@ -16,17 +16,7 @@ package edu.kit.datamanager.idoris.domain.enums; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@AllArgsConstructor -@Getter -public enum StandardIssuer { - ISO("ISO"), - W3C("W3C"), - ITU("ITU"), - RFC("RFC"), - DTR("DTR"), - Other("other"); - private final String name; +public enum ExecutionMode { + sync, + async } diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/enums/NatureOfApplicability.java b/src/main/java/edu/kit/datamanager/idoris/domain/enums/NatureOfApplicability.java deleted file mode 100644 index 47d6efb..0000000 --- a/src/main/java/edu/kit/datamanager/idoris/domain/enums/NatureOfApplicability.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2024 Karlsruhe Institute of Technology - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package edu.kit.datamanager.idoris.domain.enums; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -import java.io.Serializable; - -@Getter -@AllArgsConstructor -public enum NatureOfApplicability implements Serializable, Comparable { - Extends("extends"), - constrains("constrains"), - specifies("specifies"), - depends("depends"), - previousVersion("is_previous_version_of"), - newVersion("is_new_version_of"), - semanticallyIdentical("is_semantically_identical"), - semanticallySimilar("is_semantically_similar"); - private final String name; -} diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/enums/PrimitiveDataTypes.java b/src/main/java/edu/kit/datamanager/idoris/domain/enums/PrimitiveDataTypes.java index 24abc7d..e3e2bcf 100644 --- a/src/main/java/edu/kit/datamanager/idoris/domain/enums/PrimitiveDataTypes.java +++ b/src/main/java/edu/kit/datamanager/idoris/domain/enums/PrimitiveDataTypes.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. @@ -19,13 +19,37 @@ import lombok.AllArgsConstructor; import lombok.Getter; -@AllArgsConstructor @Getter +@AllArgsConstructor public enum PrimitiveDataTypes { string("string", String.class), + integer("integer", Integer.class), number("number", Number.class), - bool("boolean", Boolean.class), - undefined("undefined", Void.class); + bool("boolean", Boolean.class); + private final String jsonName; private final Class javaClass; + + public static PrimitiveDataTypes fromJsonName(String jsonName) { + for (PrimitiveDataTypes type : values()) { + if (type.getJsonName().equalsIgnoreCase(jsonName)) { + return type; + } + } + throw new IllegalArgumentException("Unknown JSON name: " + jsonName); + } + + public boolean isValueValid(Object value) { + if (value == null) { + return false; + } + return switch (this) { + case string -> value instanceof String || !String.valueOf(value).isBlank(); + case integer -> value instanceof Integer || (value instanceof String string && string.matches("-?\\d+")); + case number -> + value instanceof Number || (value instanceof String string && string.matches("-?\\d+(\\.\\d+)?")); + case bool -> + value instanceof Boolean || (value instanceof String string && ("true".equalsIgnoreCase(string) || "false".equalsIgnoreCase(string))); + }; + } } diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/enums/SubSchemaRelation.java b/src/main/java/edu/kit/datamanager/idoris/domain/enums/SubSchemaRelation.java deleted file mode 100644 index b055557..0000000 --- a/src/main/java/edu/kit/datamanager/idoris/domain/enums/SubSchemaRelation.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2024 Karlsruhe Institute of Technology - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package edu.kit.datamanager.idoris.domain.enums; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -@AllArgsConstructor -@Getter -public enum SubSchemaRelation { - denyAdditionalProperties("denyAdditionalProperties", "Implementing FDOs or inheriting TypeProfiles MUST NOT have additional properties to those defined in this TypeProfile."), - allowAdditionalProperties("allowAdditionalProperties", "Implementing FDOs or inheriting TypeProfiles MAY have additional properties to those defined in this TypeProfile."), - requireAllProperties("requireAllProperties", "Implementing FDOs or inheriting TypeProfiles MUST specify all properties defined in this TypeProfile."), - requireAnyOfProperties("requireAnyOfProperties", "Implementing FDOs or inheriting TypeProfiles MUST specify at least one of the properties defined in this TypeProfile."), - requireOneOfProperties("requireOneOfProperties", "Implementing FDOs or inheriting TypeProfiles MUST specify exactly one of the properties defined in this TypeProfile."), - requireNoneOfProperties("requireNoneOfProperties", "Implementing FDOs or inheriting TypeProfiles MUST NOT specify any of the properties defined in this TypeProfile."); - private final String name; - private final String description; -} diff --git a/src/main/java/edu/kit/datamanager/idoris/domain/relationships/StandardApplicability.java b/src/main/java/edu/kit/datamanager/idoris/domain/relationships/StandardApplicability.java deleted file mode 100644 index c92e6ec..0000000 --- a/src/main/java/edu/kit/datamanager/idoris/domain/relationships/StandardApplicability.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2024 Karlsruhe Institute of Technology - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package edu.kit.datamanager.idoris.domain.relationships; - -import edu.kit.datamanager.idoris.domain.entities.Standard; -import edu.kit.datamanager.idoris.domain.enums.NatureOfApplicability; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.Setter; -import org.springframework.data.neo4j.core.schema.RelationshipId; -import org.springframework.data.neo4j.core.schema.RelationshipProperties; -import org.springframework.data.neo4j.core.schema.TargetNode; - -@RelationshipProperties -@Getter -@Setter -@AllArgsConstructor -@RequiredArgsConstructor -public class StandardApplicability { - @RelationshipId - private String id; - - private NatureOfApplicability natureOfApplicability; - private String details; - - @TargetNode - private Standard standard; -} diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/ConfigurablePIDGenerator.java b/src/main/java/edu/kit/datamanager/idoris/pids/ConfigurablePIDGenerator.java new file mode 100644 index 0000000..11ce46e --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/pids/ConfigurablePIDGenerator.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package edu.kit.datamanager.idoris.pids; + +import edu.kit.datamanager.idoris.configuration.ApplicationProperties; +import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.data.neo4j.core.schema.IdGenerator; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +/** + * A configurable PID generator that delegates to either TypedPIDMakerIDGenerator + * or LocalUUIDPIDGenerator based on the 'idoris.pid-generation' application property. + */ +@Component +public class ConfigurablePIDGenerator implements IdGenerator { + + private static final Logger log = LoggerFactory.getLogger(ConfigurablePIDGenerator.class); + + private final ApplicationProperties applicationProperties; + private final ObjectProvider typedPidMakerProvider; + + public ConfigurablePIDGenerator(ApplicationProperties applicationProperties, + ObjectProvider typedPidMakerProvider) { + this.applicationProperties = applicationProperties; + this.typedPidMakerProvider = typedPidMakerProvider; + } + + /** + * Generates a PID based on the configured strategy. + * + * @param primaryLabel The primary label of the entity. + * @param entity The entity for which the PID is generated. + * @return A generated PID as a String. + * @throws IllegalArgumentException if the primary label is null or empty, or if the entity is not an instance of GenericIDORISEntity. + * @throws IllegalStateException if the configured PID generation strategy is not available. + */ + @Override + public String generateId(String primaryLabel, Object entity) { + ApplicationProperties.PIDGeneration strategy = applicationProperties.getPidGeneration(); + log.debug("PID generation strategy determined as: {}", strategy); + + if (strategy == null) { + log.error("PID generation strategy not configured"); + throw new IllegalArgumentException("PID generation strategy is not set in application properties."); + } + + // Validate inputs + if (primaryLabel.isEmpty()) { + log.error("Primary label is null or empty"); + throw new IllegalArgumentException("Primary label must not be null or empty."); + } + if (!(entity instanceof GenericIDORISEntity)) { + log.error("Entity is null or not an instance of GenericIDORISEntity"); + throw new IllegalArgumentException("Entity must be a non-null instance of GenericIDORISEntity."); + } + + switch (strategy) { + case TYPED_PID_MAKER -> { + TypedPIDMakerIDGenerator typedGenerator = typedPidMakerProvider.getIfAvailable(); + + if (typedGenerator != null) { + log.debug("Using TypedPIDMakerIDGenerator for entity labeled '{}'", primaryLabel); + return typedGenerator.generateId(primaryLabel, entity); + } else { + log.error("PID generation strategy is TYPED_PID_MAKER, but TypedPIDMakerIDGenerator bean is not available."); + throw new IllegalStateException("TypedPIDMakerIDGenerator bean is not available. Check your configuration."); + } + } + case LOCAL -> { + log.error("PID generation strategy is LOCAL, generating UUID."); + String pid = UUID.randomUUID().toString(); + log.debug("Generated UUID PID: {}", pid); + return pid; + } + default -> { + log.warn("Unsupported PID generation strategy: {}.", strategy); + throw new IllegalStateException("Unsupported PID generation strategy: " + strategy + ". Please check your configuration."); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/TypedPIDMakerIDGenerator.java b/src/main/java/edu/kit/datamanager/idoris/pids/TypedPIDMakerIDGenerator.java new file mode 100644 index 0000000..f8f3f87 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/pids/TypedPIDMakerIDGenerator.java @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package edu.kit.datamanager.idoris.pids; + +import com.google.common.base.Ascii; +import edu.kit.datamanager.idoris.configuration.ApplicationProperties; +import edu.kit.datamanager.idoris.configuration.TypedPIDMakerConfig; +import edu.kit.datamanager.idoris.domain.GenericIDORISEntity; +import edu.kit.datamanager.idoris.domain.entities.ORCiDUser; +import edu.kit.datamanager.idoris.pids.client.TypedPIDMakerClient; +import edu.kit.datamanager.idoris.pids.client.model.PIDRecord; +import edu.kit.datamanager.idoris.pids.client.model.PIDRecordEntry; +import jakarta.annotation.Nonnull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.data.neo4j.core.schema.IdGenerator; +import org.springframework.stereotype.Component; + +import java.net.URL; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +/** + * ID generator that uses the Typed PID Maker service to generate PIDs. + * It also creates PID records with metadata from the GenericIDORISEntity. + */ +@Component +@Slf4j +@ConditionalOnBean(TypedPIDMakerConfig.class) +public final class TypedPIDMakerIDGenerator implements IdGenerator { + private final TypedPIDMakerClient client; + private final TypedPIDMakerConfig config; + + /** + * The base URL for the Typed PID Maker service. + * This is derived from the application properties. + * Without a trailing slash to ensure proper URL construction. + */ + private final String baseUrl; + + /** + * Constructor. + * + * @param client The TypedPIDMakerClient + * @param config The TypedPIDMakerConfig + */ + @Autowired + public TypedPIDMakerIDGenerator(ApplicationProperties applicationProperties, TypedPIDMakerClient client, TypedPIDMakerConfig config) { + this.client = client; + this.config = config; + + String tempBaseUrl = applicationProperties.getBaseUrl(); + // Validate the base URL from application properties + if (tempBaseUrl == null || tempBaseUrl.trim().isEmpty()) { + log.error("Base URL for Typed PID Maker service is not configured or is empty."); + throw new IllegalArgumentException("Base URL for Typed PID Maker service must be configured."); + } + // Ensure the base URL does not end with a slash + if (tempBaseUrl.endsWith("/")) { + log.warn("Base URL for Typed PID Maker service should not end with a slash. Removing trailing slash."); + tempBaseUrl = tempBaseUrl.substring(0, tempBaseUrl.length() - 1); + } + this.baseUrl = tempBaseUrl; + log.info("Initialized TypedPIDMakerIDGenerator with base URL: {}", this.baseUrl); + } + + @Override + @Nonnull + public String generateId(String primaryLabel, Object entity) { + if (!(entity instanceof GenericIDORISEntity idorisEntity)) { + log.warn("Entity is not a GenericIDORISEntity, falling back to UUID generation"); + return java.util.UUID.randomUUID().toString(); + } + + // If the entity already has a PID, check if we need to update the record + if (idorisEntity.getPid() != null && !idorisEntity.getPid().isEmpty()) { + if (config.isUpdatePIDRecords() && config.isMeaningfulPIDRecords()) { + try { + PIDRecord existingRecord = client.getPIDRecord(idorisEntity.getPid()); + PIDRecord updatedRecord = createPIDRecord(idorisEntity); + client.updatePIDRecord(existingRecord.pid(), updatedRecord); + } catch (Exception e) { + log.error("Failed to update PID record for entity with PID {}: {}", idorisEntity.getPid(), e.getMessage()); + } + } + return idorisEntity.getPid(); + } + + // Create a new PID record + try { + log.debug("Creating PID record for entity with PID {}", idorisEntity.getPid()); + PIDRecord record = createPIDRecord(idorisEntity); + PIDRecord createdRecord = client.createPIDRecord(record); + log.info("Created new PID record with PID: {}", createdRecord.pid()); + + // Update the entity with the new PID + idorisEntity.setPid(createdRecord.pid()); + PIDRecord updatedRecord = createPIDRecord(idorisEntity); + client.updatePIDRecord(createdRecord.pid(), updatedRecord); + log.info("Updated entity with new digitalObjectLocation: {}", updatedRecord); + return createdRecord.pid(); + } catch (Exception e) { + log.error("Failed to create PID record: {}", e.getMessage()); + log.warn("Falling back to UUID generation"); + return java.util.UUID.randomUUID().toString(); + } + } + + /** + * Creates a PID record with metadata from the GenericIDORISEntity. + * Note: This method only adds metadata if the Helmholtz Kernel Information Profile allows it. + * + * @param entity The GenericIDORISEntity + * @return The PID record + */ + private PIDRecord createPIDRecord(GenericIDORISEntity entity) { + List recordEntries = new ArrayList<>(); + + // Only add metadata if configured to do so + if (config.isMeaningfulPIDRecords()) { + // Helmholtz Kernel Information Profile + recordEntries.add(new PIDRecordEntry("21.T11148/076759916209e5d62bd5", "21.T11148/b9b76f887845e32d29f7")); + +// // Add basic metadata +// if (entity.getName() != null) { +// recordEntries.add(new PIDRecordEntry("name", entity.getName())); +// } +// +// if (entity.getDescription() != null) { +// recordEntries.add(new PIDRecordEntry("description", entity.getDescription())); +// } + + // Add timestamps + Instant createdAt = entity.getCreatedAt(); + if (createdAt != null) { + recordEntries.add(new PIDRecordEntry("21.T11148/aafd5fb4c7222e2d950a", createdAt.toString())); + } + + Instant lastModifiedAt = entity.getLastModifiedAt(); + if (lastModifiedAt != null) { + recordEntries.add(new PIDRecordEntry("21.T11148/397d831aa3a9d18eb52c", lastModifiedAt.toString())); + } + + // Add version information + Long version = entity.getVersion(); + if (version != null) { + recordEntries.add(new PIDRecordEntry("21.T11148/c692273deb2772da307f", version.toString())); + } + +// // Add expected use cases +// if (entity.getExpectedUseCases() != null && !entity.getExpectedUseCases().isEmpty()) { +// recordEntries.add(new PIDRecordEntry("expectedUseCases", String.join(", ", entity.getExpectedUseCases()))); +// } + + // Add contributors + if (entity.getContributors() != null && !entity.getContributors().isEmpty()) { + entity.getContributors().forEach(contributor -> { + if (contributor instanceof ORCiDUser orcidUser) { + URL orcidURL = orcidUser.getOrcid(); + if (orcidURL != null && !orcidURL.toString().isEmpty()) { + recordEntries.add(new PIDRecordEntry("21.T11148/1a73af9e7ae00182733b", orcidURL.toExternalForm())); + log.debug("Added ORCiD URL: {}", orcidURL); + } else { + log.warn("This ORCiDUser is invalid, skipping contributor entry: {}", orcidUser); + } + } else { + log.warn("This contributor does not have a URL, skipping: {}", contributor); + } + }); + } + + // Add references + if (entity.getReferences() != null && !entity.getReferences().isEmpty()) { + entity.getReferences().forEach(reference -> { + String relationPID = reference.relationType(); + String targetPID = reference.targetPID(); + if (relationPID != null && !relationPID.isEmpty() && targetPID != null && !targetPID.isEmpty()) { + recordEntries.add(new PIDRecordEntry(relationPID, targetPID)); + log.debug("Added reference: {} -> {}", relationPID, targetPID); + } else { + log.warn("Invalid reference found, skipping: {}", reference); + } + }); + } + + + String doLocation; + if (entity.getPid() != null && !entity.getPid().isEmpty()) { + doLocation = String.format("%s/pid/%s", baseUrl, entity.getPid()); + } else { + log.warn("Entity PID is null or empty, creating temporary DO location with internal id."); + String classname = Ascii.toLowerCase(entity.getClass().getSimpleName()); + doLocation = String.format("%s/api/%s/%s", baseUrl, classname, entity.getInternalId()); + } + log.debug("Using DO location: {}", doLocation); + recordEntries.add(new PIDRecordEntry("digitalObjectLocation", doLocation)); + } + + // Create the PID record + PIDRecord pidRecord = new PIDRecord("", recordEntries); + log.info("Created PIDRecord: {}", pidRecord); + return pidRecord; + } + +} diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClient.java b/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClient.java new file mode 100644 index 0000000..58ae367 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClient.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package edu.kit.datamanager.idoris.pids.client; + +import edu.kit.datamanager.idoris.pids.client.model.PIDRecord; +import org.springframework.web.service.annotation.GetExchange; +import org.springframework.web.service.annotation.HttpExchange; +import org.springframework.web.service.annotation.PostExchange; +import org.springframework.web.service.annotation.PutExchange; + +/** + * Client for the Typed PID Maker service. + * This interface defines the operations for interacting with the service. + */ +@HttpExchange("/api/v1/pit/pid") +public interface TypedPIDMakerClient { + + /** + * Creates a new PID record using the SimplePidRecord format. + * + * @param record The PID record to create + * @return The created PID record + */ + @PostExchange(value = "/", accept = "application/vnd.datamanager.pid.simple+json", contentType = "application/vnd.datamanager.pid.simple+json") + PIDRecord createPIDRecord(PIDRecord record); + + + /** + * Gets a PID record by its PID using the SimplePidRecord format. + * + * @param pid The PID of the record to get + * @return The PID record + */ + @GetExchange(value = "/{pid}", accept = "application/vnd.datamanager.pid.simple+json") + PIDRecord getPIDRecord(String pid); + + /** + * Updates an existing PID record using the SimplePidRecord format. + * + * @param pid The PID of the record to update + * @param record The updated PID record + * @return The updated PID record + */ + @PutExchange(value = "/{pid}", accept = "application/vnd.datamanager.pid.simple+json", contentType = "application/vnd.datamanager.pid.simple+json") + PIDRecord updatePIDRecord(String pid, PIDRecord record); +} diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClientConfig.java b/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClientConfig.java new file mode 100644 index 0000000..05b9214 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/pids/client/TypedPIDMakerClientConfig.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package edu.kit.datamanager.idoris.pids.client; + +import edu.kit.datamanager.idoris.configuration.TypedPIDMakerConfig; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.support.RestClientAdapter; +import org.springframework.web.service.invoker.HttpServiceProxyFactory; + +/** + * Configuration class for the TypedPIDMakerClient. + * This class registers the TypedPIDMakerClient as a bean in the Spring application context. + */ +@Configuration +@ConditionalOnBean(TypedPIDMakerConfig.class) +public class TypedPIDMakerClientConfig { + + /** + * Creates a TypedPIDMakerClient bean using Spring HTTP Interface. + * + * @param config The TypedPIDMakerConfig + * @return The TypedPIDMakerClient + */ + @Bean + public TypedPIDMakerClient typedPIDMakerClient(TypedPIDMakerConfig config) { + // Create a client HTTP request factory with the configured timeout + org.springframework.http.client.ClientHttpRequestFactory requestFactory = + new org.springframework.http.client.SimpleClientHttpRequestFactory(); + + RestClient restClient = RestClient.builder() + .baseUrl(config.getBaseUrl()) + .requestFactory(requestFactory) + .defaultHeaders(headers -> headers.setAccept(java.util.List.of(org.springframework.http.MediaType.parseMediaType("application/vnd.datamanager.pid.simple+json")))) + .defaultStatusHandler(org.springframework.http.HttpStatusCode::isError, (request, response) -> { + throw new org.springframework.web.client.RestClientException("Error response: " + response.getStatusCode()); + }) + .build(); + + HttpServiceProxyFactory factory = HttpServiceProxyFactory + .builderFor(RestClientAdapter.create(restClient)) + .build(); + + return factory.createClient(TypedPIDMakerClient.class); + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/dao/ILicenseDao.java b/src/main/java/edu/kit/datamanager/idoris/pids/client/model/PIDRecord.java similarity index 55% rename from src/main/java/edu/kit/datamanager/idoris/dao/ILicenseDao.java rename to src/main/java/edu/kit/datamanager/idoris/pids/client/model/PIDRecord.java index 267c12f..3080d56 100644 --- a/src/main/java/edu/kit/datamanager/idoris/dao/ILicenseDao.java +++ b/src/main/java/edu/kit/datamanager/idoris/pids/client/model/PIDRecord.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,14 +14,16 @@ * limitations under the License. */ -package edu.kit.datamanager.idoris.dao; +package edu.kit.datamanager.idoris.pids.client.model; -import edu.kit.datamanager.idoris.domain.entities.License; -import org.springframework.data.rest.core.annotation.RepositoryRestResource; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -@RepositoryRestResource(collectionResourceRel = "licenses", path = "licenses") -public interface ILicenseDao extends IAbstractRepo { - License findByName(String name); +import java.util.List; - License findByUrl(String url); +/** + * Represents a Simple PID Record in the Typed PID Maker service. + * This follows the SimplePidRecord structure from the Typed PID Maker API. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record PIDRecord(String pid, List record) { } diff --git a/src/main/java/edu/kit/datamanager/idoris/pids/client/model/PIDRecordEntry.java b/src/main/java/edu/kit/datamanager/idoris/pids/client/model/PIDRecordEntry.java new file mode 100644 index 0000000..70bd6f1 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/pids/client/model/PIDRecordEntry.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package edu.kit.datamanager.idoris.pids.client.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Represents a key-value pair in a SimplePidRecord. + * This follows the PIDRecordEntry structure from the Typed PID Maker API. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record PIDRecordEntry(String key, String value) { +} diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleService.java b/src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleService.java new file mode 100644 index 0000000..d44f4d5 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/rules/logic/RuleService.java @@ -0,0 +1,482 @@ +/* + * 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.rules.logic; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Array; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * Central service that manages rule discovery and execution based on precomputed dependency graphs. + *

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

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

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

    + * Usage example: + *

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

    + * Extension points: The rule engine can be extended by implementing the {@link IRule} + * interface and annotating the implementation with {@link Rule}. The annotation processor will + * automatically incorporate the new rule into the precomputed graph. + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class RuleService { + + /** + * Provides access to all Spring-managed beans for rule discovery. + *

    + * 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; + + /** + * Thread-safe registry mapping fully qualified class names to rule instances. + *

    + * This registry serves as a cache of rule implementations that have been loaded from the + * Spring context. It uses the fully qualified class name as the key since that's what + * the precomputed graph uses to identify rules. The {@link ConcurrentHashMap} implementation + * ensures thread safety during concurrent rule execution. + */ + private final Map> ruleRegistry = new ConcurrentHashMap<>(); + + /** + * The precomputed rule dependency graph loaded from generated code. + *

    + * This graph contains the topologically sorted rule class names for each combination of + * rule task and target element type. It's generated at compile-time by the annotation + * processor, eliminating the need for runtime dependency analysis. The graph is the + * authoritative source for rule execution order. + */ + private PrecomputedRuleGraph precomputedGraph; + + /** + * Initializes the rule engine by loading the precomputed dependency graph and + * discovering required rule implementations. + *

    + * This method follows a two-step initialization process: + *

      + *
    1. First, it loads the precomputed rule dependency graph from the generated implementation
    2. + *
    3. Then, it selectively loads only the rule implementations that are referenced in the graph
    4. + *
    + *

    + * This approach ensures that only rules that are actually needed are instantiated, reducing + * memory usage and startup time. It also allows the rule engine to provide detailed diagnostic + * information about missing rules at startup rather than failing at runtime. + *

    + * This method is automatically called by Spring after dependency injection is complete. + * + * @throws IllegalStateException if the rule engine initialization fails, either because + * the precomputed graph couldn't be loaded or because critical + * rule implementations are missing + */ + @PostConstruct + void initialize() { + try { + // Load precomputed graph first to know which rules we need + loadPrecomputedGraph(); + + // Only discover rules that are actually referenced in the precomputed graph + discoverRequiredRules(); + + log.info("Rule engine initialized with {} rules", ruleRegistry.size()); + } catch (Exception e) { + log.error("Failed to initialize rule engine", e); + throw new IllegalStateException("Rule engine initialization failed", e); + } + } + + /** + * Loads the precomputed rule dependency graph from generated code. + *

    + * This method uses reflection to instantiate the implementation class of {@link PrecomputedRuleGraph} + * that was generated by the annotation processor at compile time. The generated class contains + * the complete dependency information for all rules, including their topological sorting. + *

    + * The precomputed graph is the source of truth for rule execution order and is used to: + *

      + *
    • Determine which rule implementations need to be loaded from Spring
    • + *
    • Determine the correct execution order of rules for each task and element type
    • + *
    • Optimize rule execution by enabling parallel processing where possible
    • + *
    + * + * @throws IllegalStateException if the precomputed graph implementation cannot be loaded, + * which could happen if the annotation processor didn't run + * or if the generated class is not on the classpath + */ + private void loadPrecomputedGraph() { + log.info("Loading precomputed rule dependency graph..."); + + try { + // Load the generated implementation class + Class implClass = Class.forName("edu.kit.datamanager.idoris.rules.logic.PrecomputedRuleGraphImpl"); + precomputedGraph = (PrecomputedRuleGraph) implClass.getConstructor().newInstance(); + + log.info("Precomputed rule graph loaded successfully"); + } catch (Exception e) { + log.error("Failed to load precomputed rule graph", e); + throw new IllegalStateException("Precomputed rule graph not available", e); + } + } + + /** + * Discovers and registers only the rule beans that are referenced in the precomputed graph. + *

    + * This method implements a key optimization in the rule engine: it only loads rule + * implementations that are actually needed based on the precomputed graph. This approach + * significantly reduces memory usage and startup time, especially in large systems with + * many rule implementations where only a subset might be used for specific tasks. + *

    + * The method works in three steps: + *

      + *
    1. Extract all unique rule class names from the precomputed graph
    2. + *
    3. Query the Spring context for all {@link IRule} implementations
    4. + *
    5. Register only those rule implementations that are referenced in the graph
    6. + *
    + *

    + * Additionally, this method provides diagnostic information about missing rules, which + * can help identify configuration issues early during application startup rather than + * failing at runtime. + */ + private void discoverRequiredRules() { + log.info("Discovering required rule implementations..."); + + // Extract all unique rule class names from the precomputed graph and track their discovery status + // This creates a map where keys are class names and values are booleans indicating if they've been found + Map requiredRuleClasses = precomputedGraph.getSortedRulesByTaskAndType() + .values() // Get all target type maps + .stream() // Stream the maps + .flatMap(targetTypeMap -> targetTypeMap.values().stream()) // Flatten to all rule lists + .flatMap(List::stream) // Flatten to individual rule class names + .collect(Collectors.toMap( + className -> className, // Key is the class name + className -> false, // Initial value false (not yet found) + (existing, replacement) -> existing // Keep existing entry on duplicate key + )); + + 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); + } + }); + + // Log any missing rules + long missingRules = requiredRuleClasses.values().stream() + .mapToLong(found -> found ? 0 : 1) + .sum(); + + if (missingRules > 0) { + log.warn("{} rules referenced in precomputed graph were not found in Spring context", missingRules); + + if (log.isDebugEnabled()) { + requiredRuleClasses.entrySet().stream() + .filter(entry -> !entry.getValue()) + .forEach(entry -> log.debug("Missing rule: {}", entry.getKey())); + } + } + + log.info("Discovered {} required rule implementations", ruleRegistry.size()); + } + + /** + * Executes all applicable rules for the given task and element. + *

    + * 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. + *

    + * 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. Merge the results from all rules into a single result object
    6. + *
    + *

    + * 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 + * @param resultFactory factory for creating result objects (typically a constructor reference) + * @param element type extending {@link VisitableElement} + * @param result type extending {@link RuleOutput} + * @return the merged result of all executed rules, or an empty result if no rules apply + * @throws RuntimeException if a critical error occurs during rule execution that prevents + * completion of the operation + */ + public > R executeRules( + RuleTask task, + T element, + Supplier resultFactory + ) { + String elementType = element.getClass().getName(); + log.debug("Executing rules for task={}, elementType={}", task, elementType); + + // Get sorted rule class names directly from precomputed graph + List ruleClassNames = precomputedGraph.getSortedRuleClasses(task, elementType); + + if (ruleClassNames.isEmpty()) { + log.debug("No rules found for task={}, elementType={}", task, elementType); + return resultFactory.get(); + } + + log.debug("Found {} rules for task={}, elementType={}", ruleClassNames.size(), task, elementType); + + // Execute rules in parallel and merge results + return executeRulesInParallel(ruleClassNames, element, resultFactory); + } + + /** + * Executes rules in parallel while respecting their precomputed ordering. + *

    + * This method is responsible for the actual parallel 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. + *

    + * 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
    • + *
    + *

    + * If no rule instances are available for execution, an empty result is returned. This ensures + * that the method always returns a valid result object even if no rules are executed. + * + * @param ruleClassNames the list of rule class names in execution order + * @param element the domain element to process + * @param resultFactory factory for creating result objects + * @param element type extending VisitableElement + * @param result type extending RuleOutput + * @return the merged result of all executed rules + * @throws RuntimeException if rule execution fails critically + */ + private > R executeRulesInParallel( + 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(); + } + + // 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); + } + + // Merge results + return mergeResults(resultFutures, resultFactory); + } + + /** + * Executes a single rule asynchronously and returns its result future. + *

    + * 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: + *

      + *
    • Creating a fresh result object for the rule
    • + *
    • Safely casting the rule to the correct parameterized type
    • + *
    • Invoking the rule's processing logic
    • + *
    • Capturing and handling any exceptions that occur during execution
    • + *
    + *

    + * The type casting in this method is safe because the precomputed graph ensures that + * rules are only invoked with compatible element and result types. The {@code @SuppressWarnings} + * annotation is used to silence the compiler warning about this cast. + * + * @param rule the rule implementation to execute + * @param element the domain element to process + * @param resultFactory factory for creating result objects + * @param element type extending VisitableElement + * @param result type extending RuleOutput + * @return a CompletableFuture containing the rule's execution result + */ + @SuppressWarnings("unchecked") + private > CompletableFuture executeRule( + IRule rule, + T element, + Supplier resultFactory + ) { + return CompletableFuture.supplyAsync(() -> { + String ruleName = rule.getClass().getSimpleName(); + log.debug("Executing rule: {}", ruleName); + + R result = resultFactory.get(); + + try { + // Perform a type-safe cast to the specific generic parameter types needed for this rule execution + // This cast is guaranteed to be safe because the precomputed graph ensures type compatibility + IRule typedRule = (IRule) rule; + + // Execute the rule's processing logic with the input element and result container + typedRule.process(element, result); + log.debug("Rule {} execution completed successfully", ruleName); + return result; + } catch (Exception e) { + log.error("Rule {} execution failed: {}", ruleName, e.getMessage(), e); + throw new RuntimeException("Rule execution failed: " + e.getMessage(), e); + } + }); + } + + /** + * Merges results from multiple rule executions into a single consolidated result. + *

    + * This method aggregates the results from all rule executions into a single result object. + * It handles several important aspects of result processing: + *

      + *
    • Failure resilience: Gracefully handles failures in individual rule executions
    • + *
    • Type safety: Creates properly typed arrays for the merge operation
    • + *
    • Empty result handling: Returns a valid empty result when no results are available
    • + *
    + *

    + * The merge operation itself is delegated to the {@link RuleOutput#merge} method of the result type, + * which implements domain-specific logic for combining multiple results. This allows for + * flexible result aggregation strategies depending on the specific rule task. + * + * @param resultFutures futures containing the results of rule executions + * @param resultFactory factory for creating the initial result object + * @param result type extending RuleOutput + * @return a single merged result containing the combined output of all rules + */ + private > R mergeResults( + List> resultFutures, + Supplier resultFactory + ) { + R finalResult = resultFactory.get(); + + if (resultFutures.isEmpty()) { + return finalResult; + } + + log.debug("Merging {} rule results", resultFutures.size()); + + // Extract results from all futures, gracefully handling any that failed during execution + // This creates a list containing only the successfully completed rule results + List successfulResults = resultFutures.stream() + .map(future -> { + try { + // Get the result from the completed future + return future.join(); + } catch (Exception e) { + // If the future completed exceptionally, log a warning and return null + // This prevents failures in one rule from affecting the entire merge process + log.warn("Skipping failed rule result during merge: {}", e.getMessage()); + return null; + } + }) + .filter(Objects::nonNull) // Remove any nulls from failed rule executions + .toList(); + + if (successfulResults.isEmpty()) { + return finalResult; + } + + // Create a properly typed array for the merge operation using reflection + // This is necessary because Java's type erasure prevents direct instantiation of generic arrays + // We use the concrete class of the first result to determine the array component type + @SuppressWarnings("unchecked") + R[] resultsArray = successfulResults.toArray((R[]) Array.newInstance( + successfulResults.getFirst().getClass(), successfulResults.size())); + + return finalResult.merge(resultsArray); + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/logic/Visitor.java b/src/main/java/edu/kit/datamanager/idoris/rules/logic/Visitor.java new file mode 100644 index 0000000..37b5da0 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/rules/logic/Visitor.java @@ -0,0 +1,223 @@ +/* + * 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.rules.logic; + +import edu.kit.datamanager.idoris.domain.VisitableElement; +import edu.kit.datamanager.idoris.domain.entities.*; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + + +/** + * Abstract base class for visitors that process various types of visitable elements. + * This class implements the Visitor pattern to navigate and process elements in the domain model. + * + *

    The Visitor pattern allows for adding operations to a class hierarchy without modifying the classes themselves. + * Each subclass can implement specific behavior for different element types.

    + * + *

    This implementation includes:

    + *
      + *
    • Cycle detection to prevent infinite recursion
    • + *
    • Result caching to avoid redundant processing
    • + *
    • A factory-based approach for creating output instances
    • + *
    + * + * @param the type of rule output produced by this visitor, must extend RuleOutput + */ +@Slf4j +public abstract class Visitor> { + /** + * Set to track visited element IDs to detect cycles in the visitation graph + */ + private final Set visited = new HashSet<>(); + + /** + * Cache for storing already processed results by element ID + */ + private final Map cache = new HashMap<>(); + + /** + * Factory for creating new output instances + */ + private final Supplier outputFactory; + + /** + * Creates a new visitor with the specified output factory. + * + * @param outputFactory a supplier function that creates new instances of the output type T + */ + protected Visitor(Supplier outputFactory) { + this.outputFactory = outputFactory; + } + + /** + * Visits an Attribute element and processes it. + * Default implementation treats this as a not allowed element type. + * + * @param attribute the attribute to visit + * @param args additional arguments that may be used during visitation + * @return processing result of type T + */ + public T visit(Attribute attribute, Object... args) { + return notAllowed(attribute); + } + + /** + * Visits an AttributeMapping element and processes it. + * Default implementation treats this as a not allowed element type. + * + * @param attributeMapping the attribute mapping to visit + * @param args additional arguments that may be used during visitation + * @return processing result of type T + */ + public T visit(AttributeMapping attributeMapping, Object... args) { + return notAllowed(attributeMapping); + } + + /** + * Visits an AtomicDataType element and processes it. + * Default implementation treats this as a not allowed element type. + * + * @param atomicDataType the atomic data type to visit + * @param args additional arguments that may be used during visitation + * @return processing result of type T + */ + public T visit(AtomicDataType atomicDataType, Object... args) { + return notAllowed(atomicDataType); + } + + /** + * Visits a TypeProfile element and processes it. + * Default implementation treats this as a not allowed element type. + * + * @param typeProfile the type profile to visit + * @param args additional arguments that may be used during visitation + * @return processing result of type T + */ + public T visit(TypeProfile typeProfile, Object... args) { + return notAllowed(typeProfile); + } + + /** + * Visits an Operation element and processes it. + * Default implementation treats this as a not allowed element type. + * + * @param operation the operation to visit + * @param args additional arguments that may be used during visitation + * @return processing result of type T + */ + public T visit(Operation operation, Object... args) { + return notAllowed(operation); + } + + /** + * Visits an OperationStep element and processes it. + * Default implementation treats this as a not allowed element type. + * + * @param operationStep the operation step to visit + * @param args additional arguments that may be used during visitation + * @return processing result of type T + */ + public T visit(OperationStep operationStep, Object... args) { + return notAllowed(operationStep); + } + + /** + * Visits a TechnologyInterface element and processes it. + * Default implementation treats this as a not allowed element type. + * + * @param technologyInterface the technology interface to visit + * @param args additional arguments that may be used during visitation + * @return processing result of type T + */ + public T visit(TechnologyInterface technologyInterface, Object... args) { + return notAllowed(technologyInterface); + } + + /** + * Checks if an element with the given ID has already been processed (cached). + * This method also handles cycle detection during traversal. + * + *

    The method performs the following steps:

    + *
      + *
    1. Check if the element is in the cache, and return the cached result if found
    2. + *
    3. Check if the element has been visited before (cycle detection)
    4. + *
    5. If a cycle is detected, handle it with {@link #handleCircle(String)}
    6. + *
    7. If not visited before, mark it as visited
    8. + *
    + * + * @param id the unique identifier of the element being visited + * @return the cached result if available, the result of cycle handling if a cycle is detected, or null otherwise + */ + protected final T checkCache(String id) { + if (cache.containsKey(id)) { + log.debug("Cache hit for {}", id); + return cache.get(id); + } + log.debug("Cache miss for {}", id); + if (visited.contains(id)) { + log.debug("Cycle detected for {}", id); + if (id == null) return null; + return handleCircle(id); + } else { + visited.add(id); + } + return null; + } + + /** + * Handles the detection of cycles in the visitation graph. + * Creates a new output instance with an error message about the cycle. + * + * @param id the ID of the element where a cycle was detected + * @return a new output instance with an error message about the cycle + */ + protected T handleCircle(String id) { + return outputFactory.get().addMessage("Cycle detected", OutputMessage.MessageSeverity.ERROR, id); + } + + /** + * Handles an element of a type that is not supported by this visitor. + * Logs a warning and returns an empty output instance. + * + * @param element the element that is not allowed to be processed by this visitor + * @return an empty output instance + */ + protected T notAllowed(@NotNull VisitableElement element) { + log.warn("Element of type {} not allowed in {}. Ignoring...", element.getClass().getSimpleName(), this.getClass().getSimpleName()); + return outputFactory.get(); + } + + /** + * Saves the processing result for an element to the cache. + * This prevents redundant processing if the same element is visited again. + * + * @param id the unique identifier of the element + * @param result the processing result to cache + * @return the same result that was cached (for method chaining) + */ + protected final T save(String id, T result) { + cache.put(id, result); + return result; + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/validation/InheritanceValidator.java b/src/main/java/edu/kit/datamanager/idoris/rules/validation/InheritanceValidator.java new file mode 100644 index 0000000..eaa6004 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/rules/validation/InheritanceValidator.java @@ -0,0 +1,147 @@ +/* + * 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.rules.validation; + +import edu.kit.datamanager.idoris.domain.entities.AtomicDataType; +import edu.kit.datamanager.idoris.domain.entities.Attribute; +import edu.kit.datamanager.idoris.domain.entities.TypeProfile; +import edu.kit.datamanager.idoris.rules.logic.Rule; +import edu.kit.datamanager.idoris.rules.logic.RuleTask; +import lombok.extern.slf4j.Slf4j; + +import static edu.kit.datamanager.idoris.rules.logic.OutputMessage.MessageSeverity.ERROR; + + +/** + * Rule-based validator that checks inheritance constraints for entities. + * This validator ensures that inheritance relationships are valid and that + * inherited properties maintain consistency with parent entities. + */ +@Slf4j +@Rule( + appliesTo = { + AtomicDataType.class, + Attribute.class, + TypeProfile.class + }, + name = "InheritanceValidationRule", + description = "Validates that entities properly follow inheritance rules and constraints", + tasks = {RuleTask.VALIDATE} +) +public class InheritanceValidator extends ValidationVisitor { + + /** + * 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 + */ + @Override + public ValidationResult visit(Attribute attribute, Object... args) { + ValidationResult result = new ValidationResult(); + + if (attribute.getOverride() != null && attribute.getOverride().getDataType() != null) { + Attribute override = attribute.getOverride(); + + if (!attribute.getDataType().inheritsFrom(override.getDataType())) + result.addMessage("The data type of an attribute MUST be inherited from the data type of the attribute that was overwritten.", + attribute, 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, 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, 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, ERROR); + } + + return result; + } + + /** + * Validates inheritance relationships for AtomicDataType entities + * + * @param atomicDataType The atomic data type to validate + * @param args Additional arguments (not used in this implementation) + * @return ValidationResult containing any validation errors + */ + @Override + public ValidationResult visit(AtomicDataType atomicDataType, Object... args) { + ValidationResult result = new ValidationResult(); + + AtomicDataType parent = atomicDataType.getInheritsFrom(); + if (parent != null) { + if (!atomicDataType.getPrimitiveDataType().equals(parent.getPrimitiveDataType())) + result.addMessage("Primitive data type does not match parent", atomicDataType, ERROR); + + // Compare permitted values with parent + if (parent.getPermittedValues() != null && parent.getPermittedValues().size() > 0) { + if (atomicDataType.getPermittedValues() == null || atomicDataType.getPermittedValues().isEmpty()) + result.addMessage("Permitted values are not defined for atomic data type, but should contain at least those defined by the parent", + atomicDataType, ERROR); + else if (!atomicDataType.getPermittedValues().containsAll(parent.getPermittedValues())) + result.addMessage("Permitted values do not match parent", atomicDataType, ERROR); + } + + // Compare forbidden values with parent + if (parent.getForbiddenValues() != null && parent.getForbiddenValues().size() > 0) { + if (atomicDataType.getForbiddenValues() == null || atomicDataType.getForbiddenValues().isEmpty()) + result.addMessage("Forbidden values are not defined for atomic data type, but should contain at least those defined by the parent", + atomicDataType, ERROR); + else if (!atomicDataType.getForbiddenValues().containsAll(parent.getForbiddenValues())) + result.addMessage("Forbidden values do not match parent", atomicDataType, ERROR); + } + + // Detect conflicts between permitted and forbidden values in atomic data type and parent + if (atomicDataType.getPermittedValues() != null && atomicDataType.getForbiddenValues() != null && + !atomicDataType.getPermittedValues().isEmpty() && !atomicDataType.getForbiddenValues().isEmpty() && + atomicDataType.getPermittedValues().stream().anyMatch(atomicDataType.getForbiddenValues()::contains)) { + result.addMessage("Atomic data type has conflicting permitted and forbidden values", atomicDataType, ERROR); + } + } + + return result; + } + + /** + * Validates inheritance relationships for TypeProfile entities + * + * @param typeProfile The type profile to validate + * @param args Additional arguments (not used in this implementation) + * @return ValidationResult containing any validation errors + */ + @Override + public ValidationResult visit(TypeProfile typeProfile, Object... args) { + ValidationResult result = new ValidationResult(); + + if (typeProfile.getInheritsFrom() != null && typeProfile.getInheritsFrom().size() > 0) { + for (TypeProfile parent : typeProfile.getInheritsFrom()) { + if (parent.isAbstract() && !typeProfile.isAbstract()) { + result.addMessage("TypeProfile " + typeProfile.getPid() + " is not abstract, but inherits from the TypeProfile " + + parent.getPid() + " that is abstract.", typeProfile, ERROR); + } + } + } + + return result; + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/validation/SyntaxValidator.java b/src/main/java/edu/kit/datamanager/idoris/rules/validation/SyntaxValidator.java new file mode 100644 index 0000000..fc26390 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/rules/validation/SyntaxValidator.java @@ -0,0 +1,346 @@ +/* + * 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.rules.validation; + +import edu.kit.datamanager.idoris.domain.entities.*; +import edu.kit.datamanager.idoris.domain.enums.CombinationOptions; +import edu.kit.datamanager.idoris.domain.enums.ExecutionMode; +import edu.kit.datamanager.idoris.domain.enums.PrimitiveDataTypes; +import edu.kit.datamanager.idoris.rules.logic.Rule; +import edu.kit.datamanager.idoris.rules.logic.RuleTask; +import lombok.extern.slf4j.Slf4j; + +import java.util.Arrays; + +import static edu.kit.datamanager.idoris.rules.logic.OutputMessage.MessageSeverity.*; + +/** + * Rule-based validator that checks syntax constraints for entities. + * This validator ensures that entities follow the required syntax rules + * for better usability and correctness. + */ +@Slf4j +@Rule( + appliesTo = { + AtomicDataType.class, + Attribute.class, + AttributeMapping.class, + Operation.class, + OperationStep.class, + TechnologyInterface.class, + TypeProfile.class + }, + name = "SyntaxValidationRule", + description = "Validates that entities follow required syntax rules and constraints", + tasks = {RuleTask.VALIDATE} +) +public class SyntaxValidator 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 + */ + @Override + public ValidationResult visit(Attribute attribute, Object... args) { + ValidationResult result = new ValidationResult(); + + if (attribute.getName() == null || attribute.getName().isEmpty()) { + result.addMessage("For better human readability and understanding, you MUST provide a name for the attribute.", + attribute, ERROR); + } + + if (attribute.getDescription() == null || attribute.getDescription().isEmpty()) { + result.addMessage("For better human readability and understanding, you SHOULD provide a description for the attribute.", + attribute, WARNING); + } + + if (attribute.getDataType() == null) { + result.addMessage("You MUST provide a data type for the attribute.", attribute, ERROR); + } + + if (attribute.getLowerBoundCardinality() == null) { + result.addMessage("You MUST provide a lower bound cardinality for the attribute.", attribute, ERROR); + } else if (attribute.getLowerBoundCardinality() < 0) { + result.addMessage("The lower bound cardinality of an attribute MUST be a positive number or zero.", + attribute, 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, 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, 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, WARNING); + } + + return result; + } + + /** + * Validates syntax constraints for AttributeMapping entities + * + * @param attributeMapping The attribute mapping to validate + * @param args Additional arguments (not used in this implementation) + * @return ValidationResult containing any validation errors + */ + @Override + public ValidationResult visit(AttributeMapping attributeMapping, Object... args) { + ValidationResult result = new ValidationResult(); + + if (attributeMapping.getName() == null || attributeMapping.getName().isEmpty()) { + result.addMessage("For better human readability and understanding, you SHOULD provide a name for the attribute mapping.", + attributeMapping, WARNING); + } + + if (attributeMapping.getIndex() != null && attributeMapping.getIndex() < 0) { + result.addMessage("The index is out of range. It has to be a positive number or zero.", + attributeMapping, ERROR); + } + + if (attributeMapping.getOutput() == null) { + result.addMessage("Output MUST be specified.", attributeMapping, ERROR); + } + + if (attributeMapping.getInput() == null && attributeMapping.getValue() == null) { + result.addMessage("Input and value MUST NOT be unspecified at the same time.", + attributeMapping, ERROR); + } + + // Check that the output cardinalities are compatible with the input cardinalities. + if (attributeMapping.getInput() != null && attributeMapping.getOutput() != null) { + // If the input has an upper cardinality of more than one, the output has at most one or null, and no index is specified, return an error. + if (attributeMapping.getInput().getLowerBoundCardinality() > 0 && + attributeMapping.getInput().getUpperBoundCardinality() != null && + attributeMapping.getInput().getUpperBoundCardinality() > 1 && + (attributeMapping.getOutput().getLowerBoundCardinality() < 1 || + (attributeMapping.getOutput().getUpperBoundCardinality() != null && + attributeMapping.getOutput().getUpperBoundCardinality() < 1)) && + attributeMapping.getIndex() == null) { + result.addMessage("The output cardinality is not compatible with the input cardinality. If the input has an upper cardinality of more than one, the output must have at least a lower cardinality of one and an upper cardinality of one or null.", + attributeMapping, ERROR); + } + + // If the input has an upper cardinality smaller than the lower cardinality of the output, return an error. + if (attributeMapping.getInput().getUpperBoundCardinality() != null && + attributeMapping.getOutput().getLowerBoundCardinality() > attributeMapping.getInput().getUpperBoundCardinality()) { + result.addMessage("The output cardinality is not compatible with the input cardinality. The lower bound cardinality of the output must be less than or equal to the upper bound cardinality of the input.", + attributeMapping, ERROR); + } + } + + return result; + } + + /** + * Validates syntax constraints for AtomicDataType entities + * + * @param atomicDataType The atomic data type to validate + * @param args Additional arguments (not used in this implementation) + * @return ValidationResult containing any validation errors + */ + @Override + public ValidationResult visit(AtomicDataType atomicDataType, Object... args) { + ValidationResult result = new ValidationResult(); + + validateDataType(atomicDataType, result); + + if (atomicDataType.getPrimitiveDataType() == null) { + result.addMessage("You MUST provide a primitive data type for the basic data type. Please select from: " + + Arrays.toString(PrimitiveDataTypes.values()), atomicDataType, ERROR); + } + + // Ensure that all permitted and forbidden values are of the same primitive data type + if (atomicDataType.getPermittedValues() != null && atomicDataType.getPrimitiveDataType() != null) { + for (String value : atomicDataType.getPermittedValues()) { + if (!atomicDataType.getPrimitiveDataType().isValueValid(value)) { + result.addMessage("The permitted value '" + value + "' is not valid for the primitive data type " + + atomicDataType.getPrimitiveDataType() + ".", atomicDataType, ERROR); + } + } + } + + // Ensure that no conflicts of permitted and forbidden values exist + if (atomicDataType.getPermittedValues() != null && atomicDataType.getForbiddenValues() != null) { + for (String value : atomicDataType.getPermittedValues()) { + if (atomicDataType.getForbiddenValues().contains(value)) { + result.addMessage("The permitted values and forbidden values of the atomic data type must not overlap. The value '" + + value + "' is both permitted and forbidden.", atomicDataType, ERROR); + } + } + } + + return result; + } + + /** + * Validates syntax constraints for TypeProfile entities + * + * @param typeProfile The type profile to validate + * @param args Additional arguments (not used in this implementation) + * @return ValidationResult containing any validation errors + */ + @Override + public ValidationResult visit(TypeProfile typeProfile, Object... args) { + ValidationResult result = new ValidationResult(); + + validateDataType(typeProfile, result); + + if (typeProfile.getValidationPolicy() == null) { + result.addMessage("You MUST provide a validation policy for the type profile. Please select from: " + + Arrays.toString(CombinationOptions.values()), typeProfile, ERROR); + } + + return result; + } + + /** + * Validates syntax constraints for Operation entities + * + * @param operation The operation to validate + * @param args Additional arguments (not used in this implementation) + * @return ValidationResult containing any validation errors + */ + @Override + public ValidationResult visit(Operation operation, Object... args) { + ValidationResult result = new ValidationResult(); + + if (operation.getName() == null || operation.getName().isEmpty()) { + result.addMessage("For better human readability and understanding, you MUST provide a name for the operation.", + operation, ERROR); + } + + if (operation.getDescription() == null || operation.getDescription().isEmpty()) { + result.addMessage("For better human readability and understanding, you SHOULD provide a description for the operation.", + operation, WARNING); + } + + if (operation.getExecutableOn() == null) { + result.addMessage("You MUST specify an attribute on which the operation can be executed.", + operation, ERROR); + } + + if ((operation.getReturns() == null || operation.getReturns().isEmpty())) { + result.addMessage("There are no return values provided for this Operation.", operation, INFO); + } + + if (operation.getExecution() == null || operation.getExecution().isEmpty()) { + result.addMessage("You MUST specify at least one execution step for a valid operation.", + operation, ERROR); + } + + return result; + } + + /** + * Validates syntax constraints for OperationStep entities + * + * @param operationStep The operation step to validate + * @param args Additional arguments (not used in this implementation) + * @return ValidationResult containing any validation errors + */ + @Override + public ValidationResult visit(OperationStep operationStep, Object... args) { + ValidationResult result = new ValidationResult(); + + if (operationStep.getName() == null || operationStep.getName().isEmpty()) { + result.addMessage("For better human readability, you SHOULD provide a name for the operation step.", + operationStep, WARNING); + } + + if (operationStep.getIndex() == null) { + result.addMessage("Execution order index MUST be specified. If multiple Operation Steps for an Operation have the same index, the execution may happen in random order or be parallelized.", + operationStep, ERROR); + } + + if (operationStep.getMode() == null) { + result.addMessage("An execution mode must be specified. Default is synchronous execution. Select from: " + + Arrays.toString(ExecutionMode.values()), operationStep, ERROR); + } + + if ((operationStep.getExecuteOperation() == null && operationStep.getUseTechnology() == null) || + (operationStep.getExecuteOperation() != null && operationStep.getUseTechnology() != null)) { + result.addMessage("You MUST specify either an operation or an operation type profile for the operation step. You can only specify exactly one!", + operationStep, ERROR); + } + + return result; + } + + /** + * Validates syntax constraints for TechnologyInterface entities + * + * @param technologyInterface The technology interface to validate + * @param args Additional arguments (not used in this implementation) + * @return ValidationResult containing any validation errors + */ + @Override + public ValidationResult visit(TechnologyInterface technologyInterface, Object... args) { + ValidationResult result = new ValidationResult(); + + if (technologyInterface.getName() == null || technologyInterface.getName().isEmpty()) { + result.addMessage("For better human readability and understanding, you MUST provide a name for the operation type profile.", + technologyInterface, ERROR); + } + + if (technologyInterface.getDescription() == null || technologyInterface.getDescription().isEmpty()) { + result.addMessage("For better human readability and understanding, you SHOULD provide a description for the operation type profile.", + technologyInterface, WARNING); + } + + if (technologyInterface.getAdapters() == null || technologyInterface.getAdapters().isEmpty()) { + result.addMessage("You SHOULD specify at least one adapter for the technology interface.", + technologyInterface, WARNING); + } + + return result; + } + + /** + * Helper method to validate common data type properties + * + * @param dataType The data type to validate + * @param result The validation result to add messages to + */ + private void validateDataType(DataType dataType, ValidationResult result) { + if (dataType.getType() == null) { + result.addMessage("You MUST provide a type for the data type. Please select from: " + + Arrays.toString(DataType.TYPES.values()), dataType, ERROR); + } + + if (dataType.getName() == null || dataType.getName().isEmpty()) { + result.addMessage("For better human readability and understanding, you MUST provide a name for the data type.", + dataType, ERROR); + } + + if (dataType.getDescription() == null || dataType.getDescription().isEmpty()) { + result.addMessage("For better human readability and understanding, you SHOULD provide a description for the data type.", + dataType, WARNING); + } + + if (dataType.getExpectedUseCases() == null || dataType.getExpectedUseCases().isEmpty()) { + result.addMessage("For better human readability and understanding, you SHOULD provide a list of expected uses for the data type.", + dataType, WARNING); + } + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationPolicyValidator.java b/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationPolicyValidator.java new file mode 100644 index 0000000..25d5935 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationPolicyValidator.java @@ -0,0 +1,150 @@ +/* + * 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.rules.validation; + +import edu.kit.datamanager.idoris.domain.entities.Attribute; +import edu.kit.datamanager.idoris.domain.entities.TypeProfile; +import edu.kit.datamanager.idoris.domain.enums.CombinationOptions; +import edu.kit.datamanager.idoris.rules.logic.Rule; +import edu.kit.datamanager.idoris.rules.logic.RuleTask; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static edu.kit.datamanager.idoris.rules.logic.OutputMessage.MessageSeverity.ERROR; + +/** + * Rule-based validator that checks validation policy constraints for entities. + * This validator ensures that validation policies between parent and child entities + * are followed correctly. + */ +@Slf4j +@Rule( + appliesTo = { + TypeProfile.class + }, + name = "ValidationPolicyRule", + description = "Validates that entities follow the validation policies defined by their parent entities", + tasks = {RuleTask.VALIDATE} +) +public class ValidationPolicyValidator extends ValidationVisitor { + + /** + * Validates validation policy constraints for TypeProfile entities + * + * @param typeProfile The type profile to validate + * @param args Additional arguments (not used in this implementation) + * @return ValidationResult containing any validation errors + */ + @Override + public ValidationResult visit(TypeProfile typeProfile, Object... args) { + ValidationResult result = new ValidationResult(); + + if (typeProfile.getInheritsFrom() == null || typeProfile.getInheritsFrom().isEmpty()) { + log.debug("TypeProfile {} has no parent TypeProfiles. Skipping validation.", typeProfile.getPid()); + return result; + } + + for (TypeProfile parent : typeProfile.getInheritsFrom()) { + if (parent == null || parent.getValidationPolicy() == null) { + log.debug("Parent TypeProfile is null or has no SubSchemaRelation defined. Skipping validation."); + continue; + } + log.debug("Validating TypeProfile {} against parent TypeProfile {}", typeProfile.getPid(), parent.getPid()); + + if (!parent.isAllowAdditionalAttributes() && !typeProfile.getAttributes().isEmpty()) + result.addMessage("TypeProfile " + typeProfile.getPid() + " defines additional properties, but inherits from the TypeProfile " + + parent.getPid() + " that denies additional properties.", + getTypeProfileAndParentElementaryInformation(typeProfile, parent, + Map.of("countOfAttributes", typeProfile.getAttributes().size())), ERROR); + + switch (parent.getValidationPolicy()) { + case ALL -> { + for (Attribute attribute : parent.getAttributes()) { + List undefinedAttributes = typeProfile.getAttributes().stream() + .filter(a -> a.getDataType().equals(attribute.getDataType())) + .toList(); + + if (!undefinedAttributes.isEmpty()) { + result.addMessage("TypeProfile " + typeProfile.getPid() + " does not define all properties defined in the TypeProfile " + + parent.getPid() + " that requires all properties.", + getTypeProfileAndParentElementaryInformation(typeProfile, parent, + Map.of("numberOfUndefinedAttributes", undefinedAttributes.size(), + "undefinedAttributes", undefinedAttributes)), ERROR); + } + } + } + case ANY -> { + if (typeProfile.getAttributes().stream().noneMatch(a -> parent.getAttributes().stream() + .anyMatch(pa -> pa.getDataType().equals(a.getDataType())))) { + result.addMessage("TypeProfile " + typeProfile.getPid() + " does not define any property defined in the TypeProfile " + + parent.getPid() + " that requires at least one property.", + getTypeProfileAndParentElementaryInformation(typeProfile, parent, null), ERROR); + } + } + case ONE -> { + if (typeProfile.getAttributes().stream().filter(a -> parent.getAttributes().stream() + .anyMatch(pa -> pa.getDataType().equals(a.getDataType()))).count() != 1) { + result.addMessage("TypeProfile " + typeProfile.getPid() + " does not define exactly one property defined in the TypeProfile " + + parent.getPid() + " that requires exactly one property.", + getTypeProfileAndParentElementaryInformation(typeProfile, parent, null), ERROR); + } + } + case NONE -> { + List illegallyDefinedAttributes = typeProfile.getAttributes().stream() + .filter(a -> parent.getAttributes().stream().anyMatch(pa -> pa.getDataType().equals(a.getDataType()))) + .toList(); + + if (!illegallyDefinedAttributes.isEmpty()) { + result.addMessage("TypeProfile " + typeProfile.getPid() + " defines a property defined in the TypeProfile " + + parent.getPid() + " that requires no property.", + getTypeProfileAndParentElementaryInformation(typeProfile, parent, + Map.of("numberOfIllegallyDefinedAttributes", illegallyDefinedAttributes.size(), + "illegallyDefinedAttributes", illegallyDefinedAttributes)), ERROR); + } + } + default -> throw new IllegalStateException("Unknown ValidationPolicy " + parent.getValidationPolicy()); + } + } + log.debug("Validation of TypeProfile {} against parent TypeProfiles completed. result={}", typeProfile.getPid(), result); + return result; + } + + /** + * Helper method to create information about type profiles for error messages + * + * @param typeProfile The type profile being validated + * @param parent The parent type profile + * @param otherInformation Additional information to include + * @return Map containing elementary information about the type profiles + */ + private Object getTypeProfileAndParentElementaryInformation(TypeProfile typeProfile, TypeProfile parent, Map otherInformation) { + Map result = new HashMap<>(); + result.put("this", new ElementaryInformation(typeProfile.getPid(), typeProfile.getName(), typeProfile.getValidationPolicy())); + result.put("parent", new ElementaryInformation(parent.getPid(), parent.getName(), parent.getValidationPolicy())); + result.put("otherInformation", otherInformation); + return result; + } + + /** + * Record for storing elementary information about a type profile + */ + private record ElementaryInformation(String pid, String name, CombinationOptions validationPolicy) { + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationResult.java b/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationResult.java new file mode 100644 index 0000000..d74a37c --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationResult.java @@ -0,0 +1,213 @@ +/* + * 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.rules.validation; + +import edu.kit.datamanager.idoris.rules.logic.OutputMessage; +import edu.kit.datamanager.idoris.rules.logic.RuleOutput; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static edu.kit.datamanager.idoris.rules.logic.OutputMessage.MessageSeverity.*; + +/** + * A composite result container for validation operations. + * + *

    This class implements the {@link RuleOutput} interface to be compatible with the rule processing + * framework. It stores validation messages of different severity levels (errors, warnings, info) + * and can be structured hierarchically with child results to reflect complex validation processes.

    + * + *

    Features:

    + *
      + *
    • Hierarchical structure with parent-child relationships
    • + *
    • Message categorization by severity (ERROR, WARNING, INFO)
    • + *
    • Aggregation of message counts across the hierarchy
    • + *
    • Fluent interface for building validation results
    • + *
    + * + *

    Example usage:

    + *
    + * ValidationResult result = new ValidationResult()
    + *     .addMessage("Invalid format", element, OutputMessage.MessageSeverity.ERROR)
    + *     .addChild(nestedValidation);
    + *
    + * if (!result.isValid()) {
    + *     // Handle validation failure
    + * }
    + * 
    + */ +@Slf4j +@Getter +@Setter +public class ValidationResult implements RuleOutput { + /** + * List of validation messages directly associated with this result + */ + private List messages = new ArrayList<>(); + + /** + * List of child validation results that form a hierarchical structure + */ + private List children = new ArrayList<>(); + + /** + * Adds a validation message to this result. + * + * @param message the text content of the message + * @param element the element that the message relates to + * @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)); + return this; + } + + /** + * Adds a child validation result to this result. + * Empty child results are not added to avoid cluttering the hierarchy. + * + * @param child the child validation result to add + * @return this result instance for method chaining + */ + public ValidationResult addChild(ValidationResult child) { + if (child != null && !child.isEmpty()) children.add(child); + return this; + } + + /** + * Checks if this validation result represents a valid state. + * A result is considered valid if it has no error messages in itself or any of its children. + * + * @return true if no errors exist, false otherwise + */ + public boolean isValid() { + return getErrorCount() == 0; + } + + /** + * Counts the total number of error messages in this result and all its children. + * + * @return the total number of error messages + */ + public int getErrorCount() { + int ownErrors = (int) messages.stream().filter(m -> m.severity() == ERROR).count(); + int childErrors = children.stream().mapToInt(ValidationResult::getErrorCount).sum(); + return ownErrors + childErrors; + } + + /** + * Counts the total number of warning messages in this result and all its children. + * + * @return the total number of warning messages + */ + public int getWarningCount() { + int ownWarnings = (int) messages.stream().filter(m -> m.severity() == WARNING).count(); + int childWarnings = children.stream().mapToInt(ValidationResult::getWarningCount).sum(); + return ownWarnings + childWarnings; + } + + /** + * Counts the total number of informational messages in this result and all its children. + * + * @return the total number of informational messages + */ + public int getInfoCount() { + int ownInfo = (int) messages.stream().filter(m -> m.severity() == INFO).count(); + int childInfo = children.stream().mapToInt(ValidationResult::getInfoCount).sum(); + return ownInfo + childInfo; + } + + @Override + public String toString() { + return "ValidationResult{" + + "messages=" + messages + + ", children=" + children + + ", errorCount=" + getErrorCount() + + ", warningCount=" + getWarningCount() + + '}'; + } + + public boolean isEmpty() { + boolean noErrors = getErrorCount() == 0; + boolean noWarnings = getWarningCount() == 0; + boolean noInfo = getInfoCount() == 0; + return noErrors && noWarnings && noInfo; + } + + public List getOutputMessages(OutputMessage.MessageSeverity severity) { + List result = new ArrayList<>(); + messages.stream().filter(m -> m.severity() == severity).forEach(result::add); + children.stream().map(child -> child.getOutputMessages(severity)).forEach(result::addAll); + return result; + } + + public Map> getOutputMessages() { + return Map.of( + OutputMessage.MessageSeverity.ERROR, getOutputMessages(OutputMessage.MessageSeverity.ERROR), + OutputMessage.MessageSeverity.WARNING, getOutputMessages(OutputMessage.MessageSeverity.WARNING), + OutputMessage.MessageSeverity.INFO, getOutputMessages(OutputMessage.MessageSeverity.INFO) + ); + } + + @Override + public ValidationResult merge(ValidationResult... others) { + if (others == null) return this; + + try { + // Process each ValidationResult individually + for (ValidationResult other : others) { + if (other == null) continue; + + if (!other.isEmpty()) { + this.addChild(other); + } + } + } catch (Exception e) { + // Catch any exception to prevent the application from crashing + log.error("Error in ValidationResult.merge: {}", e.getMessage(), e); + } + + return this; + } + + @Override + public ValidationResult empty() { + return new ValidationResult(); + } + + @Override + 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)); + return this; + } + + private OutputMessage.MessageSeverity convertSeverity(OutputMessage.MessageSeverity severity) { + return switch (severity) { + case INFO -> OutputMessage.MessageSeverity.INFO; + case WARNING -> OutputMessage.MessageSeverity.WARNING; + case ERROR -> OutputMessage.MessageSeverity.ERROR; + }; + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationVisitor.java b/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationVisitor.java new file mode 100644 index 0000000..13ac031 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/rules/validation/ValidationVisitor.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.rules.validation; + +/** + * A specialized visitor for validating domain entities against defined rules. + * + *

    This visitor traverses the domain model and delegates validation to the rule service. + * For each visited element, it executes all applicable validation rules and accumulates + * the results into a hierarchical validation report.

    + * + *

    The visitor uses caching to avoid redundant validation of the same elements and + * can detect and handle cycles in the domain model graph.

    + * + *

    This implementation is a Spring component that can be injected into services that + * need validation capabilities.

    + */ + +import edu.kit.datamanager.idoris.domain.VisitableElement; +import edu.kit.datamanager.idoris.rules.logic.IRule; +import edu.kit.datamanager.idoris.rules.logic.Visitor; + +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 + public void process(VisitableElement input, ValidationResult output) { + ValidationResult result = input.execute(this); + output.merge(result); + } +} diff --git a/src/main/java/edu/kit/datamanager/idoris/validators/ValidationMessage.java b/src/main/java/edu/kit/datamanager/idoris/validators/ValidationMessage.java deleted file mode 100644 index c9455a5..0000000 --- a/src/main/java/edu/kit/datamanager/idoris/validators/ValidationMessage.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2024 Karlsruhe Institute of Technology - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package edu.kit.datamanager.idoris.validators; - -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Getter; - -import javax.annotation.Nullable; - -public record ValidationMessage(@NotNull String message, @Nullable Object element, @NotNull MessageSeverity severity) { - @AllArgsConstructor - @Getter - public enum MessageSeverity { - INFO("INFO"), - WARNING("WARNING"), - ERROR("ERROR"); - private final String value; - - public boolean isHigherOrEqualTo(MessageSeverity other) { - return this.ordinal() >= other.ordinal(); - } - } -} diff --git a/src/main/java/edu/kit/datamanager/idoris/validators/ValidationResult.java b/src/main/java/edu/kit/datamanager/idoris/validators/ValidationResult.java deleted file mode 100644 index 9f19ef1..0000000 --- a/src/main/java/edu/kit/datamanager/idoris/validators/ValidationResult.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2024 Karlsruhe Institute of Technology - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package edu.kit.datamanager.idoris.validators; - -import lombok.Getter; -import lombok.Setter; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -@Getter -@Setter -public class ValidationResult { - private List messages = new ArrayList<>(); - private List children = new ArrayList<>(); - - public ValidationResult addMessage(String message, Object element, ValidationMessage.MessageSeverity type) { - messages.add(new ValidationMessage(message, element, type)); - return this; - } - - public ValidationResult addChild(ValidationResult child) { - if (child != null && !child.isEmpty()) children.add(child); - return this; - } - - public boolean isValid() { - return getErrorCount() > 1; - } - - public int getErrorCount() { - int ownErrors = (int) messages.stream().filter(m -> m.severity() == ValidationMessage.MessageSeverity.ERROR).count(); - int childErrors = children.stream().mapToInt(ValidationResult::getErrorCount).sum(); - return ownErrors + childErrors; - } - - public int getWarningCount() { - int ownWarnings = (int) messages.stream().filter(m -> m.severity() == ValidationMessage.MessageSeverity.WARNING).count(); - int childWarnings = children.stream().mapToInt(ValidationResult::getWarningCount).sum(); - return ownWarnings + childWarnings; - } - - public int getInfoCount() { - int ownInfo = (int) messages.stream().filter(m -> m.severity() == ValidationMessage.MessageSeverity.INFO).count(); - int childInfo = children.stream().mapToInt(ValidationResult::getInfoCount).sum(); - return ownInfo + childInfo; - } - - @Override - public String toString() { - return "ValidationResult{" + - "messages=" + messages + - ", children=" + children + - ", errorCount=" + getErrorCount() + - ", warningCount=" + getWarningCount() + - '}'; - } - - public boolean isEmpty() { - boolean noErrors = getErrorCount() == 0; - boolean noWarnings = getWarningCount() == 0; - boolean noInfo = getInfoCount() == 0; - return noErrors && noWarnings && noInfo; - } - - public List getValidationMessages(ValidationMessage.MessageSeverity severity) { - List result = new ArrayList<>(); - messages.stream().filter(m -> m.severity() == severity).forEach(result::add); - children.stream().map(child -> child.getValidationMessages(severity)).forEach(result::addAll); - return result; - } - - public Map> getValidationMessages() { - return Map.of( - ValidationMessage.MessageSeverity.ERROR, getValidationMessages(ValidationMessage.MessageSeverity.ERROR), - ValidationMessage.MessageSeverity.WARNING, getValidationMessages(ValidationMessage.MessageSeverity.WARNING), - ValidationMessage.MessageSeverity.INFO, getValidationMessages(ValidationMessage.MessageSeverity.INFO) - ); - } -} diff --git a/src/main/java/edu/kit/datamanager/idoris/validators/VisitableElementValidator.java b/src/main/java/edu/kit/datamanager/idoris/validators/VisitableElementValidator.java deleted file mode 100644 index 1339a7e..0000000 --- a/src/main/java/edu/kit/datamanager/idoris/validators/VisitableElementValidator.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) 2024 Karlsruhe Institute of Technology - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package edu.kit.datamanager.idoris.validators; - -import edu.kit.datamanager.idoris.configuration.ApplicationProperties; -import edu.kit.datamanager.idoris.domain.VisitableElement; -import edu.kit.datamanager.idoris.visitors.InheritanceValidator; -import edu.kit.datamanager.idoris.visitors.SubSchemaRelationValidator; -import edu.kit.datamanager.idoris.visitors.SyntaxValidator; -import edu.kit.datamanager.idoris.visitors.Visitor; -import lombok.AllArgsConstructor; -import lombok.extern.java.Log; -import org.springframework.validation.Errors; -import org.springframework.validation.Validator; - -import java.util.Set; - -@Log -@AllArgsConstructor -public class VisitableElementValidator implements Validator { - private ApplicationProperties applicationProperties; - - @Override - public boolean supports(Class clazz) { - return VisitableElement.class.isAssignableFrom(clazz); - } - - @Override - public void validate(Object target, Errors errors) { - VisitableElement visitableElement = (VisitableElement) target; - - Set> validators = Set.of( - new SubSchemaRelationValidator(), - new SyntaxValidator(), - new InheritanceValidator() - ); - - validators.forEach(validator -> { - log.info("Executing " + validator.getClass().getName() + " on " + visitableElement.getClass().getName() + "..."); - ValidationResult validationResult = visitableElement.execute(validator); - var validationMessages = validationResult.getValidationMessages() - .entrySet() - .stream() - .filter(e -> e.getKey().isHigherOrEqualTo(applicationProperties.getValidationLevel())) - .filter(e -> !e.getValue().isEmpty()) - .toArray(); - - if (validationMessages.length == 0) return; - - switch (applicationProperties.getValidationPolicy()) { - case STRICT -> { - log.warning("STRICT Validation failed: " + validationResult); - errors.rejectValue(null, validator.getClass().getSimpleName(), validationMessages, "Validation with " + validator.getClass().getSimpleName() + " failed."); - } - case LAX -> { - if (!validationResult.isValid()) { - log.warning("LAX Validation failed: " + validationResult); - errors.rejectValue(null, validator.getClass().getSimpleName(), validationMessages, "Validation with " + validator.getClass().getSimpleName() + " failed."); - } - } - } - }); - } -} diff --git a/src/main/java/edu/kit/datamanager/idoris/visitors/InheritanceValidator.java b/src/main/java/edu/kit/datamanager/idoris/visitors/InheritanceValidator.java deleted file mode 100644 index 8824a66..0000000 --- a/src/main/java/edu/kit/datamanager/idoris/visitors/InheritanceValidator.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright (c) 2024 Karlsruhe Institute of Technology - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package edu.kit.datamanager.idoris.visitors; - -import edu.kit.datamanager.idoris.domain.entities.*; -import edu.kit.datamanager.idoris.domain.enums.Obligation; -import edu.kit.datamanager.idoris.validators.ValidationResult; - -import static edu.kit.datamanager.idoris.validators.ValidationMessage.MessageSeverity.ERROR; - -public class InheritanceValidator extends Visitor { - @Override - public ValidationResult visit(Attribute attribute, Object... args) { - ValidationResult result; - if ((result = checkCache(attribute.getPid())) != null) return result; - else result = new ValidationResult(); - - if (attribute.getDataType() != null) result.addChild(attribute.getDataType().execute(this, args)); - if (attribute.getOverride() != null) result.addChild(attribute.getOverride().execute(this, args)); - - if (attribute.getOverride() != null && attribute.getOverride().getDataType() != null) { - Attribute override = attribute.getOverride(); - - if (!attribute.getDataType().inheritsFrom(override.getDataType())) - result.addMessage("The data type of an attribute MUST be inherited from the data type of the attribute that was overwritten.", attribute, ERROR); - - if (attribute.getObligation() == Obligation.Optional && override.getObligation() == Obligation.Mandatory) - result.addMessage("The obligation of an attribute MUST be more or equally restrictive than the obligation of the attribute that was overwritten. Overriding a mandatory attribute as an optional attribute is NOT possible.", attribute, ERROR); - } - - return save(attribute.getPid(), result); - } - - @Override - public ValidationResult visit(AttributeMapping attributeMapping, Object... args) { - ValidationResult result; - if ((result = checkCache(attributeMapping.getId())) != null) return result; - else result = new ValidationResult(); - - if (attributeMapping.getInput() != null) result.addChild(attributeMapping.getInput().execute(this, args)); - if (attributeMapping.getOutput() != null) result.addChild(attributeMapping.getOutput().execute(this, args)); - - return save(attributeMapping.getId(), result); - } - - @Override - public ValidationResult visit(BasicDataType basicDataType, Object... args) { - ValidationResult result; - if ((result = checkCache(basicDataType.getPid())) != null) return result; - else result = new ValidationResult(); - - BasicDataType parent = basicDataType.getInheritsFrom(); - if (parent != null) { - result.addChild(parent.execute(this, args)); - if (!basicDataType.getPrimitiveDataType().equals(parent.getPrimitiveDataType())) - result.addMessage("Primitive data type does not match parent", basicDataType, ERROR); - if (!basicDataType.getCategory().equals(parent.getCategory())) - result.addMessage("Category does not match parent", basicDataType, ERROR); - - if ((parent.getValueEnum() != null && basicDataType.getValueEnum() != null) && !parent.getValueEnum().isEmpty() && !parent.getValueEnum().containsAll(basicDataType.getValueEnum())) - result.addMessage("Value enum does not match parent", basicDataType, ERROR); - } - - return save(basicDataType.getPid(), result); - } - - @Override - public ValidationResult visit(TypeProfile typeProfile, Object... args) { - ValidationResult result; - if ((result = checkCache(typeProfile.getPid())) != null) return result; - else result = new ValidationResult(); - - if (typeProfile.getInheritsFrom() != null) { - for (TypeProfile parent : typeProfile.getInheritsFrom()) { - result.addChild(parent.execute(this)); - } - } - - if (typeProfile.getAttributes() != null) { - for (Attribute attribute : typeProfile.getAttributes()) { - result.addChild(attribute.execute(this)); - } - } - - return save(typeProfile.getPid(), result); - } - - @Override - public ValidationResult visit(Operation operation, Object... args) { - ValidationResult result; - if ((result = checkCache(operation.getPid())) != null) return result; - else result = new ValidationResult(); - - if (operation.getExecutableOn() != null) result.addChild(operation.getExecutableOn().execute(this, args)); - - if (operation.getReturns() != null) { - for (Attribute attribute : operation.getReturns()) { - result.addChild(attribute.execute(this, args)); - } - } - - if (operation.getEnvironment() != null) { - for (Attribute attribute : operation.getEnvironment()) { - result.addChild(attribute.execute(this, args)); - } - } - - if (operation.getExecution() != null) { - for (OperationStep step : operation.getExecution()) { - result.addChild(step.execute(this, args)); - } - } - - return save(operation.getPid(), result); - } - - @Override - public ValidationResult visit(OperationStep operationStep, Object... args) { - ValidationResult result; - if ((result = checkCache(operationStep.getId())) != null) return result; - else result = new ValidationResult(); - - if (operationStep.getOperation() != null) result.addChild(operationStep.getOperation().execute(this, args)); - if (operationStep.getOperationTypeProfile() != null) - result.addChild(operationStep.getOperationTypeProfile().execute(this, args)); - - if (operationStep.getAttributes() != null) { - for (AttributeMapping attributeMapping : operationStep.getAttributes()) { - result.addChild(attributeMapping.execute(this, args)); - } - } - - if (operationStep.getOutput() != null) { - for (AttributeMapping attributeMapping : operationStep.getOutput()) { - result.addChild(attributeMapping.execute(this, args)); - } - } - - if (operationStep.getSteps() != null) { - for (OperationStep child : operationStep.getSteps()) { - result.addChild(child.execute(this, args)); - } - } - - return save(operationStep.getId(), result); - } - - @Override - public ValidationResult visit(OperationTypeProfile operationTypeProfile, Object... args) { - ValidationResult result; - if ((result = checkCache(operationTypeProfile.getPid())) != null) return result; - else result = new ValidationResult(); - - if (operationTypeProfile.getAttributes() != null) { - for (Attribute attribute : operationTypeProfile.getAttributes()) { - result.addChild(attribute.execute(this, args)); - } - } - - if (operationTypeProfile.getInheritsFrom() != null) { - for (OperationTypeProfile parent : operationTypeProfile.getInheritsFrom()) { - result.addChild(parent.execute(this, args)); - } - } - - if (operationTypeProfile.getOutputs() != null) { - for (Attribute attribute : operationTypeProfile.getOutputs()) { - result.addChild(attribute.execute(this, args)); - } - } - - return save(operationTypeProfile.getPid(), result); - } -} diff --git a/src/main/java/edu/kit/datamanager/idoris/visitors/SubSchemaRelationValidator.java b/src/main/java/edu/kit/datamanager/idoris/visitors/SubSchemaRelationValidator.java deleted file mode 100644 index 1635459..0000000 --- a/src/main/java/edu/kit/datamanager/idoris/visitors/SubSchemaRelationValidator.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright (c) 2024 Karlsruhe Institute of Technology - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package edu.kit.datamanager.idoris.visitors; - -import edu.kit.datamanager.idoris.domain.entities.*; -import edu.kit.datamanager.idoris.domain.enums.SubSchemaRelation; -import edu.kit.datamanager.idoris.validators.ValidationResult; -import lombok.extern.java.Log; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static edu.kit.datamanager.idoris.validators.ValidationMessage.MessageSeverity.ERROR; -import static edu.kit.datamanager.idoris.validators.ValidationMessage.MessageSeverity.WARNING; - -@Log -public class SubSchemaRelationValidator extends Visitor { - - @Override - public ValidationResult visit(Attribute attribute, Object... args) { - ValidationResult result; - if ((result = checkCache(attribute.getPid())) != null) return result; - log.info("Redirecting from Attribute " + attribute.getPid() + " to DataType"); - if (attribute.getDataType() != null) result = attribute.getDataType().execute(this, args); - return save(attribute.getPid(), result); - } - - @Override - public ValidationResult visit(AttributeMapping attributeMapping, Object... args) { - ValidationResult result = new ValidationResult(); - if (attributeMapping.getInput() == null && attributeMapping.getOutput() == null) return result; - - Attribute input = attributeMapping.getInput(); - Attribute output = attributeMapping.getOutput(); - if (input != null) result.addChild(input.execute(this, args)); - if (output != null) result.addChild(output.execute(this, args)); - return save(attributeMapping.getId(), result); - } - - @Override - public ValidationResult visit(BasicDataType basicDataType, Object... args) { - return notAllowed(basicDataType); - } - - @Override - public ValidationResult visit(TypeProfile typeProfile, Object... args) { - ValidationResult result; - if ((result = checkCache(typeProfile.getPid())) != null) return result; - else result = new ValidationResult(); - - if (typeProfile.getInheritsFrom() == null || typeProfile.getInheritsFrom().isEmpty()) { - log.info("TypeProfile " + typeProfile.getPid() + " has no parent TypeProfiles. Skipping validation."); - return save(typeProfile.getPid(), result); - } - for (TypeProfile parent : typeProfile.getInheritsFrom()) { - if (parent == null || parent.getSubSchemaRelation() == null) { - log.info("Parent TypeProfile is null or has no SubSchemaRelation defined. Skipping validation."); - continue; - } - log.info("Validating TypeProfile " + typeProfile.getPid() + " against parent TypeProfile " + parent.getPid()); - result.addChild(parent.execute(this, args)); - switch (parent.getSubSchemaRelation()) { - case denyAdditionalProperties: - if (!typeProfile.getAttributes().isEmpty()) { - result.addMessage("TypeProfile " + typeProfile.getPid() + " defines additional properties, but inherits from the TypeProfile " + parent.getPid() + " that denies additional properties.", getTypeProfileAndParentElementaryInformation(typeProfile, parent, Map.of("countOfAttributes", typeProfile.getAttributes().size())), ERROR); - } - break; - case allowAdditionalProperties: - log.info("TypeProfile " + typeProfile.getPid() + " meets all requirements."); - break; - case requireAllProperties: - for (Attribute attribute : parent.getAttributes()) { - List undefinedAttributes = typeProfile.getAttributes().stream().filter(a -> a.getDataType().equals(attribute.getDataType())).toList(); - if (!undefinedAttributes.isEmpty()) { - result.addMessage("TypeProfile " + typeProfile.getPid() + " does not define all properties defined in the TypeProfile " + parent.getPid() + " that requires all properties.", getTypeProfileAndParentElementaryInformation(typeProfile, parent, Map.of("numberOfUndefinedAttributes", undefinedAttributes.size(), "undefinedAttributes", undefinedAttributes)), ERROR); - } - } - break; - case requireAnyOfProperties: - if (typeProfile.getAttributes().stream().noneMatch(a -> parent.getAttributes().stream().anyMatch(pa -> pa.getDataType().equals(a.getDataType())))) { - result.addMessage("TypeProfile " + typeProfile.getPid() + " does not define any property defined in the TypeProfile " + parent.getPid() + " that requires at least one property.", getTypeProfileAndParentElementaryInformation(typeProfile, parent, null), ERROR); - } - break; - case requireOneOfProperties: - if (typeProfile.getAttributes().stream().filter(a -> parent.getAttributes().stream().anyMatch(pa -> pa.getDataType().equals(a.getDataType()))).count() != 1) { - result.addMessage("TypeProfile " + typeProfile.getPid() + " does not define exactly one property defined in the TypeProfile " + parent.getPid() + " that requires exactly one property.", getTypeProfileAndParentElementaryInformation(typeProfile, parent, null), ERROR); - } - break; - case requireNoneOfProperties: - List illegallyDefinedAttributes = typeProfile.getAttributes().stream().filter(a -> parent.getAttributes().stream().anyMatch(pa -> pa.getDataType().equals(a.getDataType()))).toList(); - if (!illegallyDefinedAttributes.isEmpty()) { - result.addMessage("TypeProfile " + typeProfile.getPid() + " defines a property defined in the TypeProfile " + parent.getPid() + " that requires no property.", getTypeProfileAndParentElementaryInformation(typeProfile, parent, Map.of("numberOfIllegallyDefinedAttributes", illegallyDefinedAttributes.size(), "illegallyDefinedAttributes", illegallyDefinedAttributes)), ERROR); - } - break; - default: - throw new IllegalStateException("Unknown SubSchemaRelation " + parent.getSubSchemaRelation()); - } - } - log.info("Validation of TypeProfile " + typeProfile.getPid() + " against parent TypeProfiles completed. result=" + result); - return save(typeProfile.getPid(), result); - } - - @Override - public ValidationResult visit(Operation operation, Object... args) { - ValidationResult result; - if ((result = checkCache(operation.getPid())) != null) return result; - else result = new ValidationResult(); - - if (operation.getExecution() != null) - for (OperationStep step : operation.getExecution()) - result.addChild(step.execute(this, args)); - - if (operation.getExecutableOn() != null) - result.addChild(operation.getExecutableOn().execute(this, args)); - - if (operation.getReturns() != null) - for (Attribute returns : operation.getReturns()) - result.addChild(returns.execute(this, args)); - - if (operation.getEnvironment() != null) - for (Attribute env : operation.getEnvironment()) - result.addChild(env.execute(this, args)); - - return save(operation.getPid(), result); - } - - @Override - public ValidationResult visit(OperationStep operationStep, Object... args) { - ValidationResult result; - if ((result = checkCache(operationStep.getId())) != null) return result; - else result = new ValidationResult(); - - if (operationStep.getAttributes() != null) - for (AttributeMapping input : operationStep.getAttributes()) - result.addChild(input.execute(this, args)); - - if (operationStep.getOutput() != null) - for (AttributeMapping output : operationStep.getOutput()) - result.addChild(output.execute(this, args)); - - if (operationStep.getSteps() != null) - for (OperationStep step : operationStep.getSteps()) - result.addChild(step.execute(this, args)); - - if (operationStep.getOperationTypeProfile() != null) - result.addChild(operationStep.getOperationTypeProfile().execute(this, args)); - - if (operationStep.getOperation() != null) - result.addChild(operationStep.getOperation().execute(this, args)); - - return save(operationStep.getId(), result); - } - - @Override - public ValidationResult visit(OperationTypeProfile operationTypeProfile, Object... args) { - ValidationResult result; - if ((result = checkCache(operationTypeProfile.getPid())) != null) return result; - else result = new ValidationResult(); - - if (operationTypeProfile.getInheritsFrom() != null) - for (OperationTypeProfile parent : operationTypeProfile.getInheritsFrom()) - result.addChild(parent.execute(this, args)); - - if (operationTypeProfile.getAttributes() != null) - for (Attribute attribute : operationTypeProfile.getAttributes()) - result.addChild(attribute.execute(this, args)); - - if (operationTypeProfile.getOutputs() != null) - for (Attribute outputs : operationTypeProfile.getOutputs()) - result.addChild(outputs.execute(this, args)); - - return save(operationTypeProfile.getPid(), result); - } - - private Object getTypeProfileAndParentElementaryInformation(TypeProfile typeProfile, TypeProfile parent, Map otherInformation) { - Map result = new HashMap<>(); - result.put("this", new elementaryInformation(typeProfile.getPid(), typeProfile.getName(), typeProfile.getSubSchemaRelation())); - result.put("parent", new elementaryInformation(parent.getPid(), parent.getName(), parent.getSubSchemaRelation())); - result.put("otherInformation", otherInformation); - return result; - } - - @Override - protected ValidationResult handleCircle(String id) { - return new ValidationResult().addMessage("Cycle detected", id, WARNING); - } - - private record elementaryInformation(String pid, String name, SubSchemaRelation subSchemaRelation) { - } -} diff --git a/src/main/java/edu/kit/datamanager/idoris/visitors/SyntaxValidator.java b/src/main/java/edu/kit/datamanager/idoris/visitors/SyntaxValidator.java deleted file mode 100644 index d17ae50..0000000 --- a/src/main/java/edu/kit/datamanager/idoris/visitors/SyntaxValidator.java +++ /dev/null @@ -1,292 +0,0 @@ -/* - * Copyright (c) 2024 Karlsruhe Institute of Technology - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package edu.kit.datamanager.idoris.visitors; - -import edu.kit.datamanager.idoris.domain.entities.*; -import edu.kit.datamanager.idoris.domain.enums.Category; -import edu.kit.datamanager.idoris.domain.enums.PrimitiveDataTypes; -import edu.kit.datamanager.idoris.domain.enums.SubSchemaRelation; -import edu.kit.datamanager.idoris.validators.ValidationResult; -import lombok.extern.java.Log; - -import java.util.Arrays; - -import static edu.kit.datamanager.idoris.validators.ValidationMessage.MessageSeverity.*; - -@Log -public class SyntaxValidator extends Visitor { - @Override - public ValidationResult visit(Attribute attribute, Object... args) { - ValidationResult result; - if ((result = checkCache(attribute.getPid())) != null) return result; - else result = new ValidationResult(); - - if (attribute.getName() == null || attribute.getName().isEmpty()) { - result.addMessage("For better human readability and understanding, you MUST provide a name for the attribute.", attribute, ERROR); - } - - if (attribute.getDescription() == null || attribute.getDescription().isEmpty()) { - result.addMessage("For better human readability and understanding, you SHOULD provide a description for the attribute.", attribute, WARNING); - } - - if (attribute.getDataType() == null) { - result.addMessage("You MUST provide a data type for the attribute.", attribute, ERROR); - } else { - result.addChild(attribute.getDataType().execute(this, args)); - } - - if (attribute.getObligation() == null) { - result.addMessage("You MUST provide an obligation for the attribute. The default value is 'Mandatory'.", attribute, ERROR); - } - - if (attribute.getOverride() != null) { - result.addChild(attribute.getOverride().execute(this, args)); - } - - return save(attribute.getPid(), result); - } - - @Override - public ValidationResult visit(AttributeMapping attributeMapping, Object... args) { - ValidationResult result; - if ((result = checkCache(attributeMapping.getId())) != null) return result; - else result = new ValidationResult(); - - if (attributeMapping.getName() == null || attributeMapping.getName().isEmpty()) { - result.addMessage("For better human readability and understanding, you SHOULD provide a name for the attribute mapping.", attributeMapping, WARNING); - } - - if (attributeMapping.getIndex() != null && attributeMapping.getIndex() < 0) { - result.addMessage("The index is out of range. It has to be a positive number or zero.", attributeMapping, ERROR); - } - - if (attributeMapping.getOutput() == null) { - result.addMessage("Output MUST be specified.", attributeMapping, ERROR); - } - - if (attributeMapping.getInput() == null && attributeMapping.getValue() == null) { - result.addMessage("Input and value MUST NOT be unspecified at the same time.", attributeMapping, ERROR); - } - - if (attributeMapping.getInput() != null && attributeMapping.getInput().isRepeatable() && attributeMapping.getOutput() != null && !attributeMapping.getOutput().isRepeatable() && attributeMapping.getIndex() == null) { - result.addMessage("The input is repeatable, the output is not repeatable and no index is specified.", attributeMapping, ERROR); - } - - return save(attributeMapping.getId(), result); - } - - @Override - public ValidationResult visit(BasicDataType basicDataType, Object... args) { - ValidationResult result; - if ((result = checkCache(basicDataType.getPid())) != null) return result; - else result = new ValidationResult(); - - visitDataType(basicDataType, result); - - if (basicDataType.getInheritsFrom() != null) - result.addChild(basicDataType.getInheritsFrom().execute(this, args)); - - if (basicDataType.getPrimitiveDataType() == null) { - result.addMessage("You MUST provide a primitive data type for the basic data type. Please select from: " + Arrays.toString(PrimitiveDataTypes.values()), basicDataType, ERROR); - } - - if (basicDataType.getCategory() == null) { - result.addMessage("You MUST provide a category for the basic data type. Please select from: " + Arrays.toString(Category.values()), basicDataType, ERROR); - } else switch (basicDataType.getCategory()) { - case MeasurementUnit -> { - if (basicDataType.getUnitName() == null || basicDataType.getUnitName().isEmpty()) { - result.addMessage("A measurement unit MUST have a unit name", basicDataType, ERROR); - } - if (basicDataType.getUnitSymbol() == null || basicDataType.getUnitSymbol().isEmpty()) { - result.addMessage("A measurement unit MUST have a unit symbol", basicDataType, ERROR); - } - if (basicDataType.getDefinedBy() == null || basicDataType.getDefinedBy().isEmpty()) { - result.addMessage("A measurement unit SHOULD specify the defined by field", basicDataType, WARNING); - } - if (basicDataType.getStandard_uncertainty() == null || basicDataType.getStandard_uncertainty().isEmpty()) { - result.addMessage("A measurement unit SHOULD specify the standard uncertainty", basicDataType, WARNING); - } - } - case Format -> { - if (basicDataType.getRegex() == null || basicDataType.getRegex().isEmpty()) { - result.addMessage("A format data type MUST have a regex.", basicDataType, ERROR); - } - if (basicDataType.getRegexFlavour() == null || basicDataType.getRegexFlavour().isEmpty()) { - result.addMessage("A format data type MUST specify the regex flavour. The encouraged default value is 'ecma-262-RegExp'.'", basicDataType, ERROR); - } - } - case Enumeration -> { - if (basicDataType.getValueEnum() == null || basicDataType.getValueEnum().isEmpty()) { - result.addMessage("An enumerated BasicDataType MUST have provide a set of acceptable values", basicDataType, ERROR); - } - } - default -> { - } - } - - return save(basicDataType.getPid(), result); - } - - @Override - public ValidationResult visit(TypeProfile typeProfile, Object... args) { - ValidationResult result; - if ((result = checkCache(typeProfile.getPid())) != null) return result; - else result = new ValidationResult(); - - visitDataType(typeProfile, result); - - if (typeProfile.getInheritsFrom() != null) - typeProfile.getInheritsFrom().stream().map(typeProfile1 -> typeProfile1.execute(this, args)).forEach(result::addChild); - - if (typeProfile.getAttributes() != null) { - typeProfile.getAttributes().stream().map(attribute -> attribute.execute(this, args)).forEach(result::addChild); - } - - if (typeProfile.getSubSchemaRelation() == null) { - result.addMessage("You MUST provide a sub schema relation for the type profile. Please select from: " + Arrays.toString(SubSchemaRelation.values()), typeProfile, ERROR); - } - - return save(typeProfile.getPid(), result); - } - - @Override - public ValidationResult visit(Operation operation, Object... args) { - ValidationResult result; - if ((result = checkCache(operation.getPid())) != null) return result; - else result = new ValidationResult(); - - if (operation.getName() == null || operation.getName().isEmpty()) { - result.addMessage("For better human readability and understanding, you MUST provide a name for the operation.", operation, ERROR); - } - - if (operation.getDescription() == null || operation.getDescription().isEmpty()) { - result.addMessage("For better human readability and understanding, you SHOULD provide a description for the operation.", operation, WARNING); - } - - if (operation.getExecutableOn() == null) { - result.addMessage("You MUST specify an attribute on which the operation can be executed.", operation, ERROR); - } - - if ((operation.getReturns() == null || operation.getReturns().isEmpty())) { - result.addMessage("There are no return values provided for this Operation.", operation, INFO); - } - - if (operation.getExecution() == null || operation.getExecution().isEmpty()) { - result.addMessage("You MUST specify at least one execution step for a valid operation.", operation, ERROR); - } - - operation.getExecution().stream().map(operationStep -> operationStep.execute(this, args)).forEach(result::addChild); - - return save(operation.getPid(), result); - } - - @Override - public ValidationResult visit(OperationStep operationStep, Object... args) { - ValidationResult result; - if ((result = checkCache(operationStep.getId())) != null) return result; - else result = new ValidationResult(); - - if (operationStep.getName() == null || operationStep.getName().isEmpty()) { - result.addMessage("For better human readability, you SHOULD provide a name for the operation step.", operationStep, WARNING); - } - - if (operationStep.getExecutionOrderIndex() == null) { - result.addMessage("Execution order index MUST be specified. If multiple Operation Steps for an Operation have the same index, the execution may happen in random order or be parallelized.", operationStep, ERROR); - } - - if (operationStep.getMode() == null) { - result.addMessage("An execution mode must be specified. Default is synchronous execution. Select from: " + Arrays.toString(OperationStep.ExecutionMode.values()), operationStep, ERROR); - } - - if ((operationStep.getOperation() == null && operationStep.getOperationTypeProfile() == null) || (operationStep.getOperation() != null && operationStep.getOperationTypeProfile() != null)) { - result.addMessage("You MUST specify either an operation or an operation type profile for the operation step. You can only specify exactly one!", operationStep, ERROR); - } - - if (operationStep.getOperation() != null) { - result.addChild(operationStep.getOperation().execute(this, args)); - } else if (operationStep.getOperationTypeProfile() != null) { - result.addChild(operationStep.getOperationTypeProfile().execute(this, args)); - } - - if (operationStep.getAttributes() != null) { - operationStep.getAttributes().stream().map(attributeMapping -> attributeMapping.execute(this, args)).forEach(result::addChild); - } - - if (operationStep.getOutput() != null) { - operationStep.getOutput().stream().map(attributeMapping -> attributeMapping.execute(this, args)).forEach(result::addChild); - } - - return save(operationStep.getId(), result); - } - - @Override - public ValidationResult visit(OperationTypeProfile operationTypeProfile, Object... args) { - ValidationResult result; - if ((result = checkCache(operationTypeProfile.getPid())) != null) return result; - else result = new ValidationResult(); - - if (operationTypeProfile.getName() == null || operationTypeProfile.getName().isEmpty()) { - result.addMessage("For better human readability and understanding, you MUST provide a name for the operation type profile.", operationTypeProfile, ERROR); - } - - if (operationTypeProfile.getDescription() == null || operationTypeProfile.getDescription().isEmpty()) { - result.addMessage("For better human readability and understanding, you SHOULD provide a description for the operation type profile.", operationTypeProfile, WARNING); - } - - if (operationTypeProfile.getInheritsFrom() != null) { - operationTypeProfile.getInheritsFrom().stream().map(operationTypeProfile1 -> operationTypeProfile1.execute(this, args)).forEach(result::addChild); - } - - if (operationTypeProfile.getAttributes() != null) { - operationTypeProfile.getAttributes().stream().map(attribute -> attribute.execute(this, args)).forEach(result::addChild); - } - - if (operationTypeProfile.getOutputs() != null) { - operationTypeProfile.getOutputs().stream().map(attribute -> attribute.execute(this, args)).forEach(result::addChild); - } - -// TODO -// if (operationTypeProfile.getAdapters() != null) { -// operationTypeProfile.getAdapters().stream().map(fdo -> fdo.execute(this, args)).forEach(result::addChild); -// } - - return save(operationTypeProfile.getPid(), result); - } - - private void visitDataType(DataType dataType, ValidationResult result) { - if (dataType.getType() == null) { - result.addMessage("You MUST provide a type for the data type. Please select from: " + Arrays.toString(DataType.TYPES.values()), dataType, ERROR); - } - - if (dataType.getName() == null || dataType.getName().isEmpty()) { - result.addMessage("For better human readability and understanding, you MUST provide a name for the data type.", dataType, ERROR); - } - - if (dataType.getDescription() == null || dataType.getDescription().isEmpty()) { - result.addMessage("For better human readability and understanding, you SHOULD provide a description for the data type.", dataType, WARNING); - } - - if (dataType.getExpectedUses() == null || dataType.getExpectedUses().isEmpty()) { - result.addMessage("For better human readability and understanding, you SHOULD provide a list of expected uses for the data type.", dataType, WARNING); - } - } - - @Override - protected ValidationResult handleCircle(String id) { - return new ValidationResult().addMessage("Cycle detected", id, INFO); - } -} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/visitors/Visitor.java b/src/main/java/edu/kit/datamanager/idoris/visitors/Visitor.java deleted file mode 100644 index 3661db4..0000000 --- a/src/main/java/edu/kit/datamanager/idoris/visitors/Visitor.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (c) 2024 Karlsruhe Institute of Technology - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package edu.kit.datamanager.idoris.visitors; - -import edu.kit.datamanager.idoris.domain.VisitableElement; -import edu.kit.datamanager.idoris.domain.entities.*; -import edu.kit.datamanager.idoris.validators.ValidationResult; -import jakarta.validation.constraints.NotNull; -import lombok.extern.java.Log; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import static edu.kit.datamanager.idoris.validators.ValidationMessage.MessageSeverity.ERROR; - -@Log -public abstract class Visitor { - private final Set visited = new HashSet<>(); - private final Map cache = new HashMap<>(); - - public abstract T visit(Attribute attribute, Object... args); - - public abstract T visit(AttributeMapping attributeMapping, Object... args); - - public abstract T visit(BasicDataType basicDataType, Object... args); - - public abstract T visit(TypeProfile typeProfile, Object... args); - - public abstract T visit(Operation operation, Object... args); - - public abstract T visit(OperationStep operationStep, Object... args); - - public abstract T visit(OperationTypeProfile operationTypeProfile, Object... args); - - protected final ValidationResult checkCache(String id) { - if (cache.containsKey(id)) { - log.info("Cache hit for " + id); - return cache.get(id); - } - log.info("Cache miss for " + id); - if (visited.contains(id)) { - log.info("Cycle detected for " + id); - if (id == null) return null; - return handleCircle(id); - } else { - visited.add(id); - } - return null; - } - - protected ValidationResult handleCircle(String id) { - return new ValidationResult().addMessage("Cycle detected", id, ERROR); - } - - protected ValidationResult notAllowed(@NotNull VisitableElement element) { - log.warning("Element of type " + element.getClass().getSimpleName() + " not allowed in " + this.getClass().getSimpleName() + ". Ignoring..."); - return new ValidationResult(); - } - - protected final ValidationResult save(String id, ValidationResult result) { - cache.put(id, result); - return result; - } -} diff --git a/src/main/java/edu/kit/datamanager/idoris/web/v1/OperationController.java b/src/main/java/edu/kit/datamanager/idoris/web/v1/OperationController.java index c8c7be6..db89b00 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/v1/OperationController.java +++ b/src/main/java/edu/kit/datamanager/idoris/web/v1/OperationController.java @@ -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. @@ -19,8 +19,8 @@ import edu.kit.datamanager.idoris.dao.IDataTypeDao; import edu.kit.datamanager.idoris.dao.IOperationDao; import edu.kit.datamanager.idoris.domain.entities.Operation; -import edu.kit.datamanager.idoris.validators.ValidationResult; -import edu.kit.datamanager.idoris.visitors.SubSchemaRelationValidator; +import edu.kit.datamanager.idoris.rules.validation.ValidationPolicyValidator; +import edu.kit.datamanager.idoris.rules.validation.ValidationResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.rest.webmvc.RepositoryRestController; import org.springframework.http.ResponseEntity; @@ -38,7 +38,7 @@ public class OperationController { @GetMapping("v1/operations/{pid}/validate") public ResponseEntity validate(@PathVariable("pid") String pid) { Operation operation = operationDao.findById(pid).orElseThrow(); - SubSchemaRelationValidator validator = new SubSchemaRelationValidator(); + ValidationPolicyValidator validator = new ValidationPolicyValidator(); ValidationResult result = operation.execute(validator); if (result.isValid()) { return ResponseEntity.ok(result); diff --git a/src/main/java/edu/kit/datamanager/idoris/web/v1/PidRedirectController.java b/src/main/java/edu/kit/datamanager/idoris/web/v1/PidRedirectController.java new file mode 100644 index 0000000..3c894c1 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/idoris/web/v1/PidRedirectController.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2025 Karlsruhe Institute of Technology + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package edu.kit.datamanager.idoris.web.v1; + +import com.google.common.base.Ascii; +import edu.kit.datamanager.idoris.dao.*; +import lombok.extern.java.Log; +import org.neo4j.driver.Value; +import org.springframework.data.neo4j.core.Neo4jClient; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.net.URI; +import java.util.*; + +@Controller +@RequestMapping("/") +@Log +public class PidRedirectController { + + private static final String FIND_ENTITY_BY_PID_QUERY = "MATCH (n {pid: $pid}) RETURN n.pid AS pid, labels(n) AS nodeLabels LIMIT 1"; + + private final Neo4jClient neo4jClient; + private final Map> labelToDaoMap; + + public PidRedirectController(Neo4jClient neo4jClient, + IAtomicDataTypeDao atomicDataTypeDao, + IAttributeDao attributeDao, + IOperationDao operationDao, + ITechnologyInterfaceDao technologyInterfaceDao, + ITypeProfileDao typeProfileDao) { + this.neo4jClient = neo4jClient; + + Map> mapBuilder = new HashMap<>(); + mapBuilder.put("AtomicDataType", atomicDataTypeDao); + mapBuilder.put("Attribute", attributeDao); + mapBuilder.put("Operation", operationDao); + mapBuilder.put("TechnologyInterface", technologyInterfaceDao); + mapBuilder.put("TypeProfile", typeProfileDao); + this.labelToDaoMap = Collections.unmodifiableMap(mapBuilder); + } + + /** + * Redirects to the appropriate entity based on the PID value. + * The method fetches the entity data from Neo4j, extracts the PID and labels, + * and determines the redirect path based on the labels. + * + * @param pidValue The PID value to redirect to. + * @return A ResponseEntity with a redirect status or not found if no entity is found. + */ + @GetMapping("/{pidValue}") + public ResponseEntity redirectToEntity(@PathVariable("pidValue") String pidValue) { + Optional> entityDataOptional = fetchNeo4jData(pidValue); + + if (entityDataOptional.isEmpty()) { + log.warning("No entity data found in Neo4j for PID: " + pidValue); + return ResponseEntity.notFound().build(); + } + + Map entityData = entityDataOptional.get(); + String entityPid = entityData.containsKey("pid") ? (String) entityData.get("pid") : null; + + if (entityPid == null || entityPid.isEmpty()) { + log.warning("Extracted PID is null or empty for input: " + pidValue); + return ResponseEntity.notFound().build(); + } + + List nodeLabels = extractAndFilterLabels(entityData); + + if (nodeLabels.isEmpty()) { + log.warning("No suitable labels found for PID: " + pidValue + " after filtering."); + return ResponseEntity.notFound().build(); + } + + Optional redirectPathBaseOptional = determineRedirectPathBase(entityPid, nodeLabels); + + if (redirectPathBaseOptional.isPresent()) { + String redirectUrl = String.format("/%s/%s", redirectPathBaseOptional.get(), entityPid); + return ResponseEntity.status(HttpStatus.FOUND) + .location(URI.create(redirectUrl)) + .build(); + } else { + log.warning("No endpoint could be determined for PID: " + pidValue + " with labels: " + nodeLabels); + return ResponseEntity.notFound().build(); + } + } + + /** + * Fetches entity data from Neo4j based on the provided PID value. + * + * @param pidValue The PID value to search for. + * @return An Optional containing the entity data if found, otherwise empty. + */ + private Optional> fetchNeo4jData(String pidValue) { + return neo4jClient.query(FIND_ENTITY_BY_PID_QUERY) + .bind(pidValue).to("pid") + .fetch().one(); + } + + /** + * Extracts and filters labels from the entity data. + * Filters out null, empty, "GenericIDORISEntity", and labels starting with "_". + * + * @param entityData The entity data map containing labels. + * @return A list of filtered labels. + */ + private List extractAndFilterLabels(Map entityData) { + Object rawLabels = entityData.get("nodeLabels"); + if (!(rawLabels instanceof Value nodeLabelsValue)) { + log.fine("nodeLabels field is missing or not of type Value for entity data: " + entityData); + return Collections.emptyList(); + } + + return nodeLabelsValue.asList(Value::asString) + .stream() + .filter(label -> label != null && !label.isEmpty()) + .map(String::trim) + .filter(label -> !label.equals("GenericIDORISEntity") && !label.startsWith("_")) + .toList(); + } + + /** + * Determines the redirect path base based on the entity PID and its labels. + * It looks up the DAO for each label, fetches the entity by PID, and constructs the path base. + * + * @param entityPid The PID of the entity. + * @param nodeLabels The list of labels associated with the entity. + * @return An Optional containing the redirect path base if found, otherwise empty. + */ + private Optional determineRedirectPathBase(String entityPid, List nodeLabels) { + return nodeLabels.stream() + .map(labelToDaoMap::get) // Get DAO for label + .filter(Objects::nonNull) // Filter out labels not in map (or DAOs that are null) + .map(dao -> { + try { + // Assuming IGenericRepo has findByPID method. + // The exact return type of findByPID (e.g., entity or Optional) + // is handled by the filter(Objects::nonNull) that follows. + return dao.findByPid(entityPid); + } catch (Exception e) { + log.warning("Error calling findByPID on DAO for PID " + entityPid + ": " + e.getMessage()); + return null; // Treat as not found if there's an error + } + }) + .filter(Objects::nonNull) // Filter out if entity not found by this DAO or if an error occurred + .map(entity -> Ascii.toLowerCase(entity.getClass().getSimpleName()) + "s") // Determine path base + .findFirst(); // Take the first one that matches + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/idoris/web/v1/TypeProfileController.java b/src/main/java/edu/kit/datamanager/idoris/web/v1/TypeProfileController.java index 9da299a..5d2173b 100644 --- a/src/main/java/edu/kit/datamanager/idoris/web/v1/TypeProfileController.java +++ b/src/main/java/edu/kit/datamanager/idoris/web/v1/TypeProfileController.java @@ -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. @@ -21,8 +21,8 @@ import edu.kit.datamanager.idoris.dao.ITypeProfileDao; import edu.kit.datamanager.idoris.domain.entities.Attribute; import edu.kit.datamanager.idoris.domain.entities.TypeProfile; -import edu.kit.datamanager.idoris.validators.ValidationResult; -import edu.kit.datamanager.idoris.visitors.SubSchemaRelationValidator; +import edu.kit.datamanager.idoris.rules.validation.ValidationPolicyValidator; +import edu.kit.datamanager.idoris.rules.validation.ValidationResult; import jakarta.validation.constraints.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.rest.webmvc.RepositoryRestController; @@ -51,7 +51,7 @@ public class TypeProfileController { @GetMapping("typeProfiles/{pid}/validate") public ResponseEntity validate(@PathVariable("pid") String pid) { TypeProfile typeProfile = typeProfileDao.findById(pid).orElseThrow(); - SubSchemaRelationValidator validator = new SubSchemaRelationValidator(); + ValidationPolicyValidator validator = new ValidationPolicyValidator(); ValidationResult result = typeProfile.execute(validator); if (result.isValid()) { return ResponseEntity.ok(result); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d3f61b1..e3ccd0f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2024 Karlsruhe Institute of Technology +# Copyright (c) 2024-2025 Karlsruhe Institute of Technology # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,7 +14,9 @@ # limitations under the License. # spring.application.name=idoris -logging.level.root=DEBUG +logging.level.root=INFO +logging.level.org.springframework=INFO +logging.level.edu.kit.datamanager=DEBUG spring.neo4j.uri=bolt://localhost:7687 spring.neo4j.authentication.username=neo4j spring.neo4j.authentication.password=superSecret @@ -22,3 +24,8 @@ spring.data.rest.basePath=/api server.port=8095 idoris.validation-level=info idoris.validation-policy=strict +idoris.pid-generation=TYPED_PID_MAKER +idoris.typed-pid-maker.base-url=http://localhost:8090 +idoris.typed-pid-maker.timeout=5000 +idoris.typed-pid-maker.meaningful-pid-records=true +idoris.typed-pid-maker.update-pid-records=true diff --git a/src/main/resources/test-config/application.properties b/src/main/resources/test-config/application.properties index d3f61b1..e3ccd0f 100644 --- a/src/main/resources/test-config/application.properties +++ b/src/main/resources/test-config/application.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2024 Karlsruhe Institute of Technology +# Copyright (c) 2024-2025 Karlsruhe Institute of Technology # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,7 +14,9 @@ # limitations under the License. # spring.application.name=idoris -logging.level.root=DEBUG +logging.level.root=INFO +logging.level.org.springframework=INFO +logging.level.edu.kit.datamanager=DEBUG spring.neo4j.uri=bolt://localhost:7687 spring.neo4j.authentication.username=neo4j spring.neo4j.authentication.password=superSecret @@ -22,3 +24,8 @@ spring.data.rest.basePath=/api server.port=8095 idoris.validation-level=info idoris.validation-policy=strict +idoris.pid-generation=TYPED_PID_MAKER +idoris.typed-pid-maker.base-url=http://localhost:8090 +idoris.typed-pid-maker.timeout=5000 +idoris.typed-pid-maker.meaningful-pid-records=true +idoris.typed-pid-maker.update-pid-records=true diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index d3f61b1..e3ccd0f 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2024 Karlsruhe Institute of Technology +# Copyright (c) 2024-2025 Karlsruhe Institute of Technology # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,7 +14,9 @@ # limitations under the License. # spring.application.name=idoris -logging.level.root=DEBUG +logging.level.root=INFO +logging.level.org.springframework=INFO +logging.level.edu.kit.datamanager=DEBUG spring.neo4j.uri=bolt://localhost:7687 spring.neo4j.authentication.username=neo4j spring.neo4j.authentication.password=superSecret @@ -22,3 +24,8 @@ spring.data.rest.basePath=/api server.port=8095 idoris.validation-level=info idoris.validation-policy=strict +idoris.pid-generation=TYPED_PID_MAKER +idoris.typed-pid-maker.base-url=http://localhost:8090 +idoris.typed-pid-maker.timeout=5000 +idoris.typed-pid-maker.meaningful-pid-records=true +idoris.typed-pid-maker.update-pid-records=true diff --git a/src/test/resources/test-config/application.properties b/src/test/resources/test-config/application.properties index d3f61b1..e3ccd0f 100644 --- a/src/test/resources/test-config/application.properties +++ b/src/test/resources/test-config/application.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2024 Karlsruhe Institute of Technology +# Copyright (c) 2024-2025 Karlsruhe Institute of Technology # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,7 +14,9 @@ # limitations under the License. # spring.application.name=idoris -logging.level.root=DEBUG +logging.level.root=INFO +logging.level.org.springframework=INFO +logging.level.edu.kit.datamanager=DEBUG spring.neo4j.uri=bolt://localhost:7687 spring.neo4j.authentication.username=neo4j spring.neo4j.authentication.password=superSecret @@ -22,3 +24,8 @@ spring.data.rest.basePath=/api server.port=8095 idoris.validation-level=info idoris.validation-policy=strict +idoris.pid-generation=TYPED_PID_MAKER +idoris.typed-pid-maker.base-url=http://localhost:8090 +idoris.typed-pid-maker.timeout=5000 +idoris.typed-pid-maker.meaningful-pid-records=true +idoris.typed-pid-maker.update-pid-records=true