From d69b1f2ec12ba885a4dc623f4fe8f6110e940190 Mon Sep 17 00:00:00 2001 From: Jhon Rueda Date: Fri, 27 Mar 2026 09:29:31 +0100 Subject: [PATCH 1/3] Validating asset creation with OntologyHub --- .../extensions/ontology-validator/README.md | 0 .../ontology-validator/build.gradle.kts | 17 +++ .../validator/OntologyJsonValidator.java | 137 ++++++++++++++++++ .../validator/OntologyValidatorExtension.java | 48 ++++++ .../validator/OntologyWarningHolder.java | 9 ++ ...rg.eclipse.edc.spi.system.ServiceExtension | 1 + .../launchers/connector/build.gradle.kts | 3 + .../inesdata-connector/settings.gradle.kts | 1 + 8 files changed, 216 insertions(+) create mode 100644 adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/README.md create mode 100644 adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/build.gradle.kts create mode 100644 adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyJsonValidator.java create mode 100644 adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyValidatorExtension.java create mode 100644 adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyWarningHolder.java create mode 100644 adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension diff --git a/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/README.md b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/README.md new file mode 100644 index 00000000..e69de29b diff --git a/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/build.gradle.kts b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/build.gradle.kts new file mode 100644 index 00000000..f3ddde72 --- /dev/null +++ b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + `java-library` + id("com.gmv.inesdata.edc-application") +} + +dependencies { + api(libs.edc.api.core) + api(project(":spi:vocabulary-spi")) + api(project(":extensions:vocabulary-shared-api")) + api(project(":extensions:federated-catalog-cache-api")) + + implementation(libs.edc.control.plane.spi) + implementation(libs.edc.lib.validator) + implementation(libs.edc.transaction.spi) +} + + diff --git a/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyJsonValidator.java b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyJsonValidator.java new file mode 100644 index 00000000..5806cf0e --- /dev/null +++ b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyJsonValidator.java @@ -0,0 +1,137 @@ +package org.upm.inesdata.validator; + +import jakarta.json.JsonObject; +import jakarta.json.JsonString; +import jakarta.json.JsonValue; +import org.eclipse.edc.validator.spi.ValidationResult; +import org.eclipse.edc.validator.spi.Validator; +import org.eclipse.edc.spi.monitor.Monitor; + +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +public class OntologyJsonValidator implements Validator { + + private final Monitor monitor; + private final String allowedDomain; + + public OntologyJsonValidator(String allowedDomain, Monitor monitor) { + this.allowedDomain = allowedDomain; + this.monitor = monitor; + } + + @Override + public ValidationResult validate(JsonObject input) { + + monitor.info("[OntologyValidator] Start validation."); + + Map flat = new HashMap<>(); + flattenJson("", input, flat); + + String foundKey = null; + String url = null; + + for (var e : flat.entrySet()) { + if (e.getKey().endsWith("urlOntologyHub")) { + foundKey = e.getKey(); + url = e.getValue(); + break; + } + } + + if (foundKey == null) { + monitor.info("[OntologyValidator] No urlOntologyHub found."); + return ValidationResult.success(); + } + + String cleanPath = normalizePath(foundKey); + + if (url == null || url.isBlank()) { + monitor.info("[OntologyValidator] urlOntologyHub empty → success."); + return ValidationResult.success(); + } + + // Validación de dominio — WARNING, no error + if (allowedDomain != null && !allowedDomain.isBlank()) { + if (!url.startsWith(allowedDomain)) { + monitor.warning("[OntologyValidator] WARNING: Domain not allowed: " + url); + return ValidationResult.success(); // ⚠️ NO bloquear + } + } + + // HEAD request para validar existencia + try { + URL target = new URL(url); + HttpURLConnection http = (HttpURLConnection) target.openConnection(); + http.setRequestMethod("HEAD"); + http.setConnectTimeout(3000); + http.setReadTimeout(3000); + http.connect(); + + int code = http.getResponseCode(); + monitor.info("[OntologyValidator] HEAD=" + code); + + if (code == 404 || code == 400) { + monitor.warning("[OntologyValidator] WARNING: URL unreachable: HTTP " + code); + return ValidationResult.success(); // ⚠️ NO bloquear + } + + } catch (Exception e) { + monitor.warning("[OntologyValidator] WARNING: Error reaching URL: " + e.getMessage()); + return ValidationResult.success(); // ⚠️ NO bloquear + } + + return ValidationResult.success(); + } + + // Normaliza rutas JSON‑LD largas + private String normalizePath(String path) { + if (path == null) return "urlOntologyHub"; + String[] parts = path.split("\\."); + StringBuilder sb = new StringBuilder(); + for (String p : parts) { + if (p.startsWith("http")) { + int i = p.lastIndexOf("/"); + if (i != -1 && i < p.length()-1) p = p.substring(i+1); + } + if (sb.length() > 0) sb.append("."); + sb.append(p); + } + return sb.toString(); + } + + // Flatten JSON‑LD + private void flattenJson(String prefix, JsonValue value, Map flat) { + switch (value.getValueType()) { + + case OBJECT: + JsonObject obj = value.asJsonObject(); + + if (obj.containsKey("@value")) { + flat.put(prefix, obj.getString("@value")); + return; + } + + for (String key : obj.keySet()) { + String next = prefix.isEmpty() ? key : prefix + "." + key; + flattenJson(next, obj.get(key), flat); + } + break; + + case ARRAY: + for (JsonValue item : value.asJsonArray()) { + flattenJson(prefix, item, flat); + } + break; + + case STRING: + flat.put(prefix, ((JsonString) value).getString()); + break; + + default: + flat.put(prefix, value.toString()); + } + } +} diff --git a/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyValidatorExtension.java b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyValidatorExtension.java new file mode 100644 index 00000000..87348a90 --- /dev/null +++ b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyValidatorExtension.java @@ -0,0 +1,48 @@ +package org.upm.inesdata.validator; + +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Provides; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry; + +import static org.eclipse.edc.connector.controlplane.asset.spi.domain.Asset.EDC_ASSET_TYPE; + +@Provides(OntologyValidatorExtension.class) +@Extension(OntologyValidatorExtension.NAME) +public class OntologyValidatorExtension implements ServiceExtension { + + public static final String NAME = "Ontology Validator Extension"; + + @Inject + private JsonObjectValidatorRegistry validatorRegistry; // <---- INYECCIÓN REAL + + private Monitor monitor; + + @Override + public String name() { + return NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + monitor = context.getMonitor(); + monitor.info("🔥 OntologyValidatorExtension initialized!!"); + } + + @Override + public void prepare() { + + String allowedDomain = "http://ontology-hub-demo.dev.ds.dataspaceunit.upm"; + + monitor.info("🧪 Registrando OntologyJsonValidator para dominio permitido: " + allowedDomain); + + // SI ESTA LÍNEA SE EJECUTA VERÁS EL LOG ARRIBA + validatorRegistry.register( + EDC_ASSET_TYPE, + new OntologyJsonValidator(allowedDomain, monitor) + ); + } +} \ No newline at end of file diff --git a/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyWarningHolder.java b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyWarningHolder.java new file mode 100644 index 00000000..5c04a2fc --- /dev/null +++ b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyWarningHolder.java @@ -0,0 +1,9 @@ +package org.upm.inesdata.validator; + +public class OntologyWarningHolder { + private static final ThreadLocal warning = new ThreadLocal<>(); + + public static void set(String msg) { warning.set(msg); } + public static String get() { return warning.get(); } + public static void clear() { warning.remove(); } +} \ No newline at end of file diff --git a/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 00000000..4fe6e04d --- /dev/null +++ b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1 @@ +org.upm.inesdata.validator.OntologyValidatorExtension diff --git a/adapters/inesdata/sources/inesdata-connector/launchers/connector/build.gradle.kts b/adapters/inesdata/sources/inesdata-connector/launchers/connector/build.gradle.kts index c7173354..aaee0e16 100644 --- a/adapters/inesdata/sources/inesdata-connector/launchers/connector/build.gradle.kts +++ b/adapters/inesdata/sources/inesdata-connector/launchers/connector/build.gradle.kts @@ -129,6 +129,9 @@ dependencies { // Observability implementation(libs.edc.api.observability) + // Ontology Validator + implementation(project(":extensions:ontology-validator")) + // Vulnerabilities implementation(libs.google.protobuf) implementation(libs.jetty.http) diff --git a/adapters/inesdata/sources/inesdata-connector/settings.gradle.kts b/adapters/inesdata/sources/inesdata-connector/settings.gradle.kts index a3255e8f..cbbb2891 100644 --- a/adapters/inesdata/sources/inesdata-connector/settings.gradle.kts +++ b/adapters/inesdata/sources/inesdata-connector/settings.gradle.kts @@ -39,6 +39,7 @@ include(":extensions:vocabulary-shared-api") include(":extensions:vocabulary-shared-retrieval") include(":extensions:inesdata-transfer-process-api") include(":extensions:asset-validator") +include(":extensions:ontology-validator") // Connector include(":launchers:connector") From 2659c39a2e89214226a7cd8e987f47fcbbfcfce8 Mon Sep 17 00:00:00 2001 From: Jhon Rueda Date: Thu, 9 Apr 2026 10:36:20 +0200 Subject: [PATCH 2/3] Avanzando en la extension de validacion --- adapters/inesdata/sources/Ontology-Hub | 1 + .../asset-create/asset-create.component.html | 12 ++++ .../asset-create/asset-create.component.ts | 66 +++++++++++++++++-- .../uploader-file.component.html | 2 +- .../uploader-file/uploader-file.component.ts | 1 + .../src/app/shared/services/asset.service.ts | 21 +++--- .../semantic-file-validator.service.ts | 51 ++++++++++++++ .../validator/OntologyValidatorExtension.java | 2 +- 8 files changed, 138 insertions(+), 18 deletions(-) create mode 160000 adapters/inesdata/sources/Ontology-Hub create mode 100644 adapters/inesdata/sources/inesdata-connector-interface/src/app/shared/services/semantic-file-validator.service.ts diff --git a/adapters/inesdata/sources/Ontology-Hub b/adapters/inesdata/sources/Ontology-Hub new file mode 160000 index 00000000..1969d3d3 --- /dev/null +++ b/adapters/inesdata/sources/Ontology-Hub @@ -0,0 +1 @@ +Subproject commit 1969d3d3c0e31da591212b1805788c335756bb8b diff --git a/adapters/inesdata/sources/inesdata-connector-interface/src/app/pages/assets/asset-create/asset-create.component.html b/adapters/inesdata/sources/inesdata-connector-interface/src/app/pages/assets/asset-create/asset-create.component.html index 12460586..b8c464ec 100644 --- a/adapters/inesdata/sources/inesdata-connector-interface/src/app/pages/assets/asset-create/asset-create.component.html +++ b/adapters/inesdata/sources/inesdata-connector-interface/src/app/pages/assets/asset-create/asset-create.component.html @@ -232,6 +232,18 @@ + +
+ Ontology Information + + URI Ontology + + + + Shacl File + +
+ diff --git a/adapters/inesdata/sources/inesdata-connector-interface/src/app/pages/assets/asset-create/asset-create.component.ts b/adapters/inesdata/sources/inesdata-connector-interface/src/app/pages/assets/asset-create/asset-create.component.ts index fea7d621..ef0003c4 100644 --- a/adapters/inesdata/sources/inesdata-connector-interface/src/app/pages/assets/asset-create/asset-create.component.ts +++ b/adapters/inesdata/sources/inesdata-connector-interface/src/app/pages/assets/asset-create/asset-create.component.ts @@ -20,6 +20,7 @@ import { BehaviorSubject } from 'rxjs'; import { Router } from '@angular/router'; import { MatSlideToggleChange } from '@angular/material/slide-toggle'; import { NgModel } from '@angular/forms'; +import { SemanticFileValidatorService } from 'src/app/shared/services/semantic-file-validator.service'; @Component({ @@ -39,6 +40,9 @@ export class AssetCreateComponent implements OnInit { ] }; + //manage the validation button for semantic files + showTestFileButton = false; + // Dynamic forms from vocabylary variables vocabularies: Vocabulary[]; selectedVocabularies: Vocabulary[] @@ -75,6 +79,7 @@ export class AssetCreateComponent implements OnInit { keywords: string = ''; format: string = ''; byteSize: string = ''; + ontologyUri: string = ''; // Storage information amazonS3DataAddress: AmazonS3DataAddress = { @@ -90,6 +95,10 @@ export class AssetCreateComponent implements OnInit { type: 'InesDataStore' }; + shaclsInesDataStoreAddress: InesDataStoreAddress = { + type: 'InesDataStore' + }; + assetType: any; assetTypes = Object.entries(ASSET_TYPES); defaultForms: JsonFormData[]; @@ -151,7 +160,8 @@ export class AssetCreateComponent implements OnInit { private notificationService: NotificationService, @Inject('STORAGE_TYPES') public storageTypes: StorageType[], private router: Router, - private loadingService: LoadingService) { + private loadingService: LoadingService, + private semanticFileValidator: SemanticFileValidatorService) { } async onSave() { @@ -245,7 +255,8 @@ export class AssetCreateComponent implements OnInit { properties["dcterms:description"] = this.description; properties["dcat:byteSize"] = this.byteSize; properties["dcterms:format"] = this.format; - + properties["ontologyUri"] = this.ontologyUri; + properties["shacl"] = this.shaclsInesDataStoreAddress.shaclContent || null; this.addKeywords(properties); } @@ -349,12 +360,37 @@ export class AssetCreateComponent implements OnInit { setFiles(event: File[]) { if (event?.length > 0) { - this.inesDataStoreAddress.file = event[0] + this.inesDataStoreAddress.file = event[0]; + this.fileRequiresValidation(); } else { delete this.inesDataStoreAddress.file } } + async setShaclsFiles(event: File[], shaclFile:any) { + if (event?.length > 0) { + const file = event[0]; + + const isTxtExtension = file.name.toLowerCase().endsWith('.txt'); + const isTxtMime = file.type === 'text/plain' || file.type === ''; + + if (!isTxtExtension || !isTxtMime) { + this.showError("Error de extensión del fichero", "Solo se permiten archivos de texto plano (.txt)"); + delete this.shaclsInesDataStoreAddress.file; + delete this.shaclsInesDataStoreAddress.shaclContent; + shaclFile.deleteFile(0); + return; + } + + this.shaclsInesDataStoreAddress.file = file; + this.shaclsInesDataStoreAddress.shaclContent = await file.text(); + } else { + delete this.shaclsInesDataStoreAddress.file; + delete this.shaclsInesDataStoreAddress.shaclContent; + + } + } + onSelectionChangeVocabulary() { this.selectedForms = [] @@ -448,6 +484,28 @@ export class AssetCreateComponent implements OnInit { } onToggleChange(propertyName: string, event: MatSlideToggleChange): void { - this.httpDataAddress[propertyName] = event ? 'true' : 'false'; + this.httpDataAddress[propertyName] = event.checked; } + + + async fileRequiresValidation(){ + let isSemanticFile = false; + + if(!this.inesDataStoreAddress.file){ + return + } + + await this.semanticFileValidator.isASemanticFile(this.inesDataStoreAddress.file).then(res => { + isSemanticFile = res + }); + + if(isSemanticFile){ + alert("Es un archivo semantico") + } + + alert(this.inesDataStoreAddress.file.type); + + } + + } diff --git a/adapters/inesdata/sources/inesdata-connector-interface/src/app/shared/components/uploader-file/uploader-file.component.html b/adapters/inesdata/sources/inesdata-connector-interface/src/app/shared/components/uploader-file/uploader-file.component.html index a87dcc53..09962362 100644 --- a/adapters/inesdata/sources/inesdata-connector-interface/src/app/shared/components/uploader-file/uploader-file.component.html +++ b/adapters/inesdata/sources/inesdata-connector-interface/src/app/shared/components/uploader-file/uploader-file.component.html @@ -1,5 +1,5 @@
- + = new EventEmitter() diff --git a/adapters/inesdata/sources/inesdata-connector-interface/src/app/shared/services/asset.service.ts b/adapters/inesdata/sources/inesdata-connector-interface/src/app/shared/services/asset.service.ts index f114aa48..cebdf6b4 100644 --- a/adapters/inesdata/sources/inesdata-connector-interface/src/app/shared/services/asset.service.ts +++ b/adapters/inesdata/sources/inesdata-connector-interface/src/app/shared/services/asset.service.ts @@ -47,9 +47,7 @@ export class AssetService { } } - return from(lastValueFrom(this.http.post( - `${this.BASE_URL}`, body - ))); + return this.http.post(`${this.BASE_URL}`, body); } /** @@ -61,9 +59,9 @@ export class AssetService { throw new Error('Required parameter id was null or undefined when calling getAsset.'); } - return from(lastValueFrom(this.http.get( + return this.http.get( `${this.BASE_URL}${environment.runtime.service.asset.get}${id}` - ))); + ); } /** @@ -75,9 +73,9 @@ export class AssetService { throw new Error('Required parameter id was null or undefined when calling removeAsset.'); } - return from(lastValueFrom(this.http.delete( + return this.http.delete( `${this.BASE_URL}${environment.runtime.service.asset.get}${id}` - ))); + ); } /** @@ -94,9 +92,9 @@ export class AssetService { } } - return from(lastValueFrom(this.http.post>( + return this.http.post>( `${this.BASE_URL}${environment.runtime.service.asset.getAll}`, body - )).then(results => { + ).pipe(map(results => { return expandArray(results, () => new Asset()); })); } @@ -114,9 +112,9 @@ export class AssetService { ...querySpec }; - return from(lastValueFrom(this.http.post( + return this.http.post( `${environment.runtime.managementApiUrl}${environment.runtime.service.asset.count}`, body - ))); + ); } async uploadChunk(assetEntryDto: any, chunk: Blob, fileName: string, chunkIndex: number, totalChunks: number): Promise { @@ -177,4 +175,3 @@ export class AssetService { ); } } - diff --git a/adapters/inesdata/sources/inesdata-connector-interface/src/app/shared/services/semantic-file-validator.service.ts b/adapters/inesdata/sources/inesdata-connector-interface/src/app/shared/services/semantic-file-validator.service.ts new file mode 100644 index 00000000..8eb8cd22 --- /dev/null +++ b/adapters/inesdata/sources/inesdata-connector-interface/src/app/shared/services/semantic-file-validator.service.ts @@ -0,0 +1,51 @@ +import { Injectable } from "@angular/core"; +import { rdfParser } from "rdf-parse"; +import { Readable } from "readable-stream"; + + +@Injectable({ + providedIn: 'root' +}) +export class SemanticFileValidatorService{ + private readonly RDF_MIME_TYPES = [ + 'text/turtle', + 'application/rdf+xml', + 'application/n-triples', + 'application/n-quads', + 'application/ld+json', + 'application/rdf+json', + 'text/n3' + ]; + + constructor(private rdfParser: RdfParser, private readable: Readable) + { + + } + + async isASemanticFile(file: File): Promise { + try { + const text = await file.text(); // ✅ siempre leemos el contenido + const stream = Readable.from([text]); + + const quadStream = rdfParser.parse(stream, { + contentType: file.type || 'application/octet-stream', + path: file.name + }); + + return await new Promise((resolve) => { + quadStream.on('data', () => { + quadStream.destroy(); + resolve(true); // ✅ detectado + }); + + quadStream.on('error', () => resolve(false)); // ✅ no es RDF + quadStream.on('end', () => resolve(false)); // ✅ no se detectó RDF + }); + + } catch (e) { + return false; // ✅ archivo ilegible, no es RDF + } + } + + +} \ No newline at end of file diff --git a/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyValidatorExtension.java b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyValidatorExtension.java index 87348a90..c98ee78d 100644 --- a/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyValidatorExtension.java +++ b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyValidatorExtension.java @@ -35,7 +35,7 @@ public void initialize(ServiceExtensionContext context) { @Override public void prepare() { - String allowedDomain = "http://ontology-hub-demo.dev.ds.dataspaceunit.upm"; + String allowedDomain = "http://demo-ontology-hub.demo.svc.cluster.local"; monitor.info("🧪 Registrando OntologyJsonValidator para dominio permitido: " + allowedDomain); From de655596adf4e38cc1047f7c395d5463fd76c757 Mon Sep 17 00:00:00 2001 From: Jhon Alexander Rueda Sanchez Date: Fri, 17 Apr 2026 09:44:27 +0200 Subject: [PATCH 3/3] The semantic asset validation has been implemented and it is working --- .../package-lock.json | 1 + .../inesdata-connector-interface/package.json | 1 + .../asset-create/asset-create.component.html | 61 +++++- .../asset-create/asset-create.component.ts | 185 +++++++++++++++--- .../ontology-viewer.component.html | 4 +- .../confirmation-dialog.component.ts | 2 + .../src/app/shared/models/ontology.ts | 13 ++ .../src/app/shared/services/asset.service.ts | 26 ++- .../app/shared/services/ontology.service.ts | 34 +++- .../semantic-file-validator.service.ts | 67 +++---- .../src/environments/environment.prod.ts | 2 +- .../src/environments/environment.ts | 2 +- .../ontology-validator/build.gradle.kts | 2 + .../validator/OntologyJsonValidator.java | 137 ------------- .../validator/OntologyValidatorExtension.java | 34 ++-- .../validator/OntologyWarningHolder.java | 9 - .../AssetRdfValidationApiController.java | 56 ++++++ .../validator/dtos/ValidationResponse.java | 16 ++ .../services/RdfValidationService.java | 17 ++ .../validator/services/enums/RdfFormat.java | 51 +++++ .../services/impl/JenaValidationService.java | 132 +++++++++++++ 21 files changed, 608 insertions(+), 244 deletions(-) delete mode 100644 adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyJsonValidator.java delete mode 100644 adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyWarningHolder.java create mode 100644 adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/controller/AssetRdfValidationApiController.java create mode 100644 adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/dtos/ValidationResponse.java create mode 100644 adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/services/RdfValidationService.java create mode 100644 adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/services/enums/RdfFormat.java create mode 100644 adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/services/impl/JenaValidationService.java diff --git a/adapters/inesdata/sources/inesdata-connector-interface/package-lock.json b/adapters/inesdata/sources/inesdata-connector-interface/package-lock.json index f55aad3b..51067f6e 100644 --- a/adapters/inesdata/sources/inesdata-connector-interface/package-lock.json +++ b/adapters/inesdata/sources/inesdata-connector-interface/package-lock.json @@ -42,6 +42,7 @@ "ngx-json-viewer": "~3.2.1", "ngx-schema-form": "^2.11.0", "rxjs": "~7.8.1", + "rdflib": "2.2.6", "z-schema": "^6.0.1", "zone.js": "~0.14.2" }, diff --git a/adapters/inesdata/sources/inesdata-connector-interface/package.json b/adapters/inesdata/sources/inesdata-connector-interface/package.json index 6afb8818..98f56208 100644 --- a/adapters/inesdata/sources/inesdata-connector-interface/package.json +++ b/adapters/inesdata/sources/inesdata-connector-interface/package.json @@ -48,6 +48,7 @@ "ngx-json-viewer": "~3.2.1", "ngx-schema-form": "^2.11.0", "rxjs": "~7.8.1", + "rdflib": "2.2.6", "z-schema": "^6.0.1", "zone.js": "~0.14.2" }, diff --git a/adapters/inesdata/sources/inesdata-connector-interface/src/app/pages/assets/asset-create/asset-create.component.html b/adapters/inesdata/sources/inesdata-connector-interface/src/app/pages/assets/asset-create/asset-create.component.html index b8c464ec..0b8339a3 100644 --- a/adapters/inesdata/sources/inesdata-connector-interface/src/app/pages/assets/asset-create/asset-create.component.html +++ b/adapters/inesdata/sources/inesdata-connector-interface/src/app/pages/assets/asset-create/asset-create.component.html @@ -235,15 +235,66 @@
Ontology Information - + + URI Ontology -
+ +
+ + {{ontology.titles[0]?.value}} + + - Shacl File - +
+ + + Versions + + + + {{version.name}} + + + + + + Shacles + + + + {{shacl}} + + + Upload a new shacle file + + + + +
+ Shacl File* + +
+ +
+
+
+
+ +
+ -
diff --git a/adapters/inesdata/sources/inesdata-connector-interface/src/app/pages/assets/asset-create/asset-create.component.ts b/adapters/inesdata/sources/inesdata-connector-interface/src/app/pages/assets/asset-create/asset-create.component.ts index ef0003c4..4ae971ac 100644 --- a/adapters/inesdata/sources/inesdata-connector-interface/src/app/pages/assets/asset-create/asset-create.component.ts +++ b/adapters/inesdata/sources/inesdata-connector-interface/src/app/pages/assets/asset-create/asset-create.component.ts @@ -2,6 +2,7 @@ import { Component, Inject, OnInit, QueryList, ViewChildren } from '@angular/cor import { HttpDataAddress, DataAddress } from '@think-it-labs/edc-connector-client'; import { JsonDoc } from "../../../shared/models/json-doc"; import { StorageType } from "../../../shared/models/storage-type"; +import { MatDialog } from '@angular/material/dialog'; import { AmazonS3DataAddress } from "../../../shared/models/amazon-s3-data-address"; import { Vocabulary } from "../../../shared/models/vocabulary"; import { NotificationService } from 'src/app/shared/services/notification.service'; @@ -21,6 +22,9 @@ import { Router } from '@angular/router'; import { MatSlideToggleChange } from '@angular/material/slide-toggle'; import { NgModel } from '@angular/forms'; import { SemanticFileValidatorService } from 'src/app/shared/services/semantic-file-validator.service'; +import { Ontology, OntologyVersion } from 'src/app/shared/models/ontology'; +import { OntologyService } from 'src/app/shared/services/ontology.service'; +import { ConfirmationDialogComponent, ConfirmDialogModel } from 'src/app/shared/components/confirmation-dialog/confirmation-dialog.component'; @Component({ @@ -42,6 +46,8 @@ export class AssetCreateComponent implements OnInit { //manage the validation button for semantic files showTestFileButton = false; + showUploadFileButton = false; + // Dynamic forms from vocabylary variables vocabularies: Vocabulary[]; @@ -80,6 +86,14 @@ export class AssetCreateComponent implements OnInit { format: string = ''; byteSize: string = ''; ontologyUri: string = ''; + ontologyVersion: string = ''; + ontologyShacle: any = ''; + + ontologies: Ontology[] = []; + filteredOntologies: Ontology[] = []; + ontologyVersions: OntologyVersion[] = []; + ontologyShacles: any[] = []; + // Storage information amazonS3DataAddress: AmazonS3DataAddress = { @@ -120,6 +134,14 @@ export class AssetCreateComponent implements OnInit { this.selectedForms = [] this.selectedAssetTypeVocabularies = [] + this.ontologyService.getOntologyLists().subscribe({ + next: (res: Ontology[]) => { + this.ontologies = res; + this.filteredOntologies = res; + }, + }); + + this.vocabularyService.requestVocabularies().subscribe({ next: (res: Vocabulary[]) => { this.vocabularies = res; @@ -161,7 +183,9 @@ export class AssetCreateComponent implements OnInit { @Inject('STORAGE_TYPES') public storageTypes: StorageType[], private router: Router, private loadingService: LoadingService, - private semanticFileValidator: SemanticFileValidatorService) { + private ontologyService: OntologyService, + private semanticFileValidator: SemanticFileValidatorService, + private dialog: MatDialog) { } async onSave() { @@ -256,7 +280,8 @@ export class AssetCreateComponent implements OnInit { properties["dcat:byteSize"] = this.byteSize; properties["dcterms:format"] = this.format; properties["ontologyUri"] = this.ontologyUri; - properties["shacl"] = this.shaclsInesDataStoreAddress.shaclContent || null; + properties["ontologyVersion"] = this.ontologyVersion; + properties["ontologyShacle"] = this.ontologyShacle; this.addKeywords(properties); } @@ -301,6 +326,10 @@ export class AssetCreateComponent implements OnInit { if (!this.id || !this.storageTypeId || !this.name || !this.version || !this.description || !this.keywords || !this.shortDescription || !this.assetType) { return false; } else { + if (this.ontologyUri && (!this.ontologyVersion || !this.ontologyShacle || (this.ontologyShacle === 'newShacleFile' && !this.shaclsInesDataStoreAddress.file))) { + return false; + } + if (this.storageTypeId === DATA_ADDRESS_TYPES.httpData && (!this.httpDataAddress.name || !this.httpDataAddress.baseUrl || !this.validateUrl())) { return false; } @@ -368,26 +397,12 @@ export class AssetCreateComponent implements OnInit { } async setShaclsFiles(event: File[], shaclFile:any) { + this.showUploadFileButton = false; if (event?.length > 0) { - const file = event[0]; - - const isTxtExtension = file.name.toLowerCase().endsWith('.txt'); - const isTxtMime = file.type === 'text/plain' || file.type === ''; - - if (!isTxtExtension || !isTxtMime) { - this.showError("Error de extensión del fichero", "Solo se permiten archivos de texto plano (.txt)"); - delete this.shaclsInesDataStoreAddress.file; - delete this.shaclsInesDataStoreAddress.shaclContent; - shaclFile.deleteFile(0); - return; - } - - this.shaclsInesDataStoreAddress.file = file; - this.shaclsInesDataStoreAddress.shaclContent = await file.text(); + this.showUploadFileButton = true; + this.shaclsInesDataStoreAddress.file = event[0]; } else { delete this.shaclsInesDataStoreAddress.file; - delete this.shaclsInesDataStoreAddress.shaclContent; - } } @@ -491,7 +506,7 @@ export class AssetCreateComponent implements OnInit { async fileRequiresValidation(){ let isSemanticFile = false; - if(!this.inesDataStoreAddress.file){ + if(!this.inesDataStoreAddress.file || this.ontologyShacle === 'newShacleFile' || this.ontologyShacle === ''){ return } @@ -499,13 +514,137 @@ export class AssetCreateComponent implements OnInit { isSemanticFile = res }); - if(isSemanticFile){ - alert("Es un archivo semantico") + if(isSemanticFile && this.ontologyUri && this.ontologyVersion && this.ontologyShacle){ + this.showTestFileButton = true; + } + } + + onSearchOntology(event: any) { + const value = (event.target as HTMLInputElement).value.toLowerCase(); + this.filteredOntologies = this.ontologies.filter(ontology => + ontology.titles[0].value.toLowerCase().includes(value) || + ontology.uri.toLowerCase().includes(value) + ); + } + + runOntologyTest() { + const selectedOntology = this.ontologies.find(o => o.uri === this.ontologyUri); + const prefix = selectedOntology?.prefix || ''; + + this.loadingService.showLoading('Validating semantic file...'); + const ontologyUrl = this.ontologyService.buildUrl(prefix, 'ontology', this.ontologyVersion.split('T')[0]); + const shaclUrl = this.ontologyService.buildUrl(prefix, 'shacl', this.ontologyShacle); + const rdfFormat = this.resolveRdfFormat(this.inesDataStoreAddress.file); + + this.assetService.testRdfAsset(ontologyUrl, shaclUrl, this.inesDataStoreAddress.file, rdfFormat).subscribe({ + next: (res) => { + this.notificationService.showInfo('Validation completed successfully'); + this.loadingService.hideLoading(); + }, + error: (err) => { + this.loadingService.hideLoading(); + + // Extraemos los mensajes del array de error si existe, si no el mensaje general + const detail = Array.isArray(err.error) + ? err.error.map((e: any) => typeof e === 'string' ? e : e.message).join('\n') + : err.error?.message || err.message || "Unknown validation error"; + + const dialogData = new ConfirmDialogModel("Validation Failed", detail); + dialogData.confirmText = "Close"; + dialogData.confirmColor = "warn"; + dialogData.showCancel = false; // Esto oculta el botón "Cancel" + + this.dialog.open(ConfirmationDialogComponent, { data: dialogData, width: '600px' }); + } + }); + } + + private resolveRdfFormat(file: File): string { + const mimeType = (file?.type || '').toLowerCase(); + switch (mimeType) { + case 'text/turtle': + return 'turtle'; + case 'application/rdf+xml': + return 'rdfxml'; + case 'application/ld+json': + return 'jsonld'; + case 'application/n-triples': + return 'ntriples'; + case 'text/n3': + return 'n3'; + default: + break; } - alert(this.inesDataStoreAddress.file.type); + const fileName = (file?.name || '').toLowerCase(); + if (fileName.endsWith('.n3')) return 'n3'; + if (fileName.endsWith('.ttl')) return 'turtle'; + if (fileName.endsWith('.rdf') || fileName.endsWith('.xml') || fileName.endsWith('.owl')) return 'rdfxml'; + if (fileName.endsWith('.jsonld')) return 'jsonld'; + if (fileName.endsWith('.nt')) return 'ntriples'; + return 'turtle'; } + fillVersions(uri: string) { + const selectedOntology = this.ontologies.find(o => o.uri === uri); + this.ontologyVersions = selectedOntology?.versions || []; + this.ontologyShacles = selectedOntology?.artifacts?.shapes || []; + + // Selección automática si solo hay una versión + if (this.ontologyVersions.length === 1) { + this.ontologyVersion = this.ontologyVersions[0].issued; + } else { + this.ontologyVersion = ''; + } + + // Selección automática si solo hay un Shacle + if (this.ontologyShacles.length === 1) { + this.ontologyShacle = this.ontologyShacles[0]; + } else { + this.ontologyShacle = ''; + } + } + + + uploadShacleFile() { + if (this.ontologyShacle === 'newShacleFile' && this.shaclsInesDataStoreAddress.file && this.ontologyUri) { + const selectedOntology = this.ontologies.find(o => o.uri === this.ontologyUri); + if (selectedOntology) { + this.loadingService.showLoading('Uploading Shacl file...'); + this.ontologyService.postUploadShacl( + this.shaclsInesDataStoreAddress.file, + selectedOntology.prefix, + selectedOntology.uri + ).subscribe({ + next: () => { + this.ontologyService.getOntologyLists().subscribe({ + next: (res: Ontology[]) => { + this.ontologies = res; + this.filteredOntologies = res; + + const updatedOntology = this.ontologies.find(o => o.uri === this.ontologyUri); + this.ontologyShacles = updatedOntology?.artifacts?.shapes || []; + this.ontologyShacle = ''; + + this.loadingService.hideLoading(); + this.notificationService.showInfo('Shacl file uploaded successfully'); + this.showUploadFileButton = false; + delete this.shaclsInesDataStoreAddress.file; + }, + error: () => { + this.loadingService.hideLoading(); + this.notificationService.showError('Error refreshing ontology list'); + } + }); + }, + error: (err) => { + this.loadingService.hideLoading(); + this.notificationService.showError('Error uploading Shacl file'); + } + }); + } + } + } } diff --git a/adapters/inesdata/sources/inesdata-connector-interface/src/app/pages/ontologies/ontology-viewer/ontology-viewer.component.html b/adapters/inesdata/sources/inesdata-connector-interface/src/app/pages/ontologies/ontology-viewer/ontology-viewer.component.html index dda9dafa..4d56dc82 100644 --- a/adapters/inesdata/sources/inesdata-connector-interface/src/app/pages/ontologies/ontology-viewer/ontology-viewer.component.html +++ b/adapters/inesdata/sources/inesdata-connector-interface/src/app/pages/ontologies/ontology-viewer/ontology-viewer.component.html @@ -4,7 +4,7 @@ >( `${this.BASE_URL}${environment.runtime.service.asset.getAll}`, body - ).pipe(map(results => { - return expandArray(results, () => new Asset()); + ).pipe(switchMap((results: Array) => { + return from(expandArray(results, () => new Asset())); })); } @@ -174,4 +175,23 @@ export class AssetService { this.http.post(`${this.FINALIZE_UPLOAD_URL}`, formData) ); } + + testRdfAsset(ontologyUrl: string, + shaclUrl: string, + rdfFile: File, + format: string): Observable { + + const formData = new FormData(); + formData.append('ontologyUrl', ontologyUrl); + formData.append('shaclUrl', shaclUrl); + formData.append('rdf', rdfFile); + formData.append('format', format); + + return this.http.post( + this.TEST_RDF_ASSET, + formData + ); + } + + } diff --git a/adapters/inesdata/sources/inesdata-connector-interface/src/app/shared/services/ontology.service.ts b/adapters/inesdata/sources/inesdata-connector-interface/src/app/shared/services/ontology.service.ts index 6c467d24..f8758db2 100644 --- a/adapters/inesdata/sources/inesdata-connector-interface/src/app/shared/services/ontology.service.ts +++ b/adapters/inesdata/sources/inesdata-connector-interface/src/app/shared/services/ontology.service.ts @@ -7,7 +7,9 @@ import { Observable } from 'rxjs'; import { environment } from "src/environments/environment"; import { Ontology } from '../models/ontology'; -const BASE_URL = 'http://ontology-hub-demo.dev.ds.dataspaceunit.upm/dataset'; +const BASE_URL = 'http://ontology-hub-demo.dev.ds.dataspaceunit.upm'; +const ADMIN_USER = 'admin@gmail.com'; +const ADMIN_PASSWORD = 'admin1234'; @Injectable({ providedIn: 'root' @@ -23,9 +25,37 @@ export class OntologyService{ } public getOntologyLists(): Observable { - const url = `${this.ontologyBaseUrl}/api/v2/vocabulary/list`; + const url = `${this.ontologyBaseUrl}/dataset/api/v2/vocabulary/list`; return this.http.get(url); } + public postUploadShacl(file: File, prefix: string, vocabUrl: string): Observable { + const formData = new FormData(); + formData.append('file', file); + formData.append('user', ADMIN_USER); + formData.append('password', ADMIN_PASSWORD); + formData.append('prefix', prefix); + formData.append('vocabUrl', vocabUrl); + const url = `${this.ontologyBaseUrl}/dataset/api/v2/vocabulary/artifacts/shapes`; + return this.http.post(url, formData); + } + + /** + * BuildingUrl + * For re construct ontology hub Urls for ontology and shacl files + */ + public buildUrl(prefix: string, type: "ontology" | "shacl", version: string|null):string{ + //Ontologia http://ontology-hub-demo.dev.ds.dataspaceunit.upm/dataset/vocabs/s4auto/versions/2026-04-16.n3 + //Shacl http://ontology-hub-demo.dev.ds.dataspaceunit.upm/dataset/vocabs/s4auto/artifacts/shapes/TestGeneradosSaref4Grid.txt + console.log(prefix, type, version); + let url = `${this.ontologyBaseUrl}/dataset/vocabs/${prefix}`; + if(type === 'ontology'){ + url += `/versions/${version}.n3`; + }else if(type === 'shacl' ){ + url += `/artifacts/shapes/${version}`; + } + console.log(url); + return url; + } } \ No newline at end of file diff --git a/adapters/inesdata/sources/inesdata-connector-interface/src/app/shared/services/semantic-file-validator.service.ts b/adapters/inesdata/sources/inesdata-connector-interface/src/app/shared/services/semantic-file-validator.service.ts index 8eb8cd22..fac9a140 100644 --- a/adapters/inesdata/sources/inesdata-connector-interface/src/app/shared/services/semantic-file-validator.service.ts +++ b/adapters/inesdata/sources/inesdata-connector-interface/src/app/shared/services/semantic-file-validator.service.ts @@ -1,51 +1,42 @@ import { Injectable } from "@angular/core"; -import { rdfParser } from "rdf-parse"; -import { Readable } from "readable-stream"; - +import * as $rdf from "rdflib"; @Injectable({ - providedIn: 'root' + providedIn: "root", }) -export class SemanticFileValidatorService{ - private readonly RDF_MIME_TYPES = [ - 'text/turtle', - 'application/rdf+xml', - 'application/n-triples', - 'application/n-quads', - 'application/ld+json', - 'application/rdf+json', - 'text/n3' - ]; - - constructor(private rdfParser: RdfParser, private readable: Readable) - { - - } +export class SemanticFileValidatorService { - async isASemanticFile(file: File): Promise { - try { - const text = await file.text(); // ✅ siempre leemos el contenido - const stream = Readable.from([text]); + // Formatos soportados por rdflib en navegador + private readonly FORMATS = [ + "text/turtle", // Turtle + "application/rdf+xml", // RDF/XML + "application/ld+json", // JSON-LD + "text/n3" // Notation3 + ]; - const quadStream = rdfParser.parse(stream, { - contentType: file.type || 'application/octet-stream', - path: file.name - }); + private readonly BASE_IRI = "urn:semantic-file:"; // identificador ficticio - return await new Promise((resolve) => { - quadStream.on('data', () => { - quadStream.destroy(); - resolve(true); // ✅ detectado - }); + constructor() {} - quadStream.on('error', () => resolve(false)); // ✅ no es RDF - quadStream.on('end', () => resolve(false)); // ✅ no se detectó RDF - }); + async isASemanticFile(file: File): Promise { + try { + const text = await file.text(); + const store = $rdf.graph(); + for (const format of this.FORMATS) { + try { + // Intentamos parsear en cada formato conocido + $rdf.parse(text, store, this.BASE_IRI, format); + return true; // ✅ El archivo contiene RDF válido } catch (e) { - return false; // ✅ archivo ilegible, no es RDF + // fallo → probar siguiente formato } - } + } - + return false; // ❌ Ningún formato RDF lo pudo interpretar + + } catch { + return false; // ❌ error leyendo archivo → no es semántico + } + } } \ No newline at end of file diff --git a/adapters/inesdata/sources/inesdata-connector-interface/src/environments/environment.prod.ts b/adapters/inesdata/sources/inesdata-connector-interface/src/environments/environment.prod.ts index 72ad1185..0a55c1cc 100644 --- a/adapters/inesdata/sources/inesdata-connector-interface/src/environments/environment.prod.ts +++ b/adapters/inesdata/sources/inesdata-connector-interface/src/environments/environment.prod.ts @@ -10,7 +10,7 @@ export const environment = { managementApiUrl: '', catalogUrl: '', sharedUrl: "", - ontologyUrl: "http://ontology-hub-demo.dev.ds.dataspaceunit.upm/dataset", + ontologyUrl: "http://ontology-hub-demo.dev.ds.dataspaceunit.upm", participantId: "", storageAccount: '', storageExplorerLinkTemplate: '', diff --git a/adapters/inesdata/sources/inesdata-connector-interface/src/environments/environment.ts b/adapters/inesdata/sources/inesdata-connector-interface/src/environments/environment.ts index b37ed667..2547d570 100644 --- a/adapters/inesdata/sources/inesdata-connector-interface/src/environments/environment.ts +++ b/adapters/inesdata/sources/inesdata-connector-interface/src/environments/environment.ts @@ -14,7 +14,7 @@ export const environment = { managementApiUrl: "http://localhost:19193/management", catalogUrl: "http://localhost:19193/management/federatedcatalog", sharedUrl: "http://localhost:19196/shared", - ontologyUrl: "http://ontology-hub-demo.dev.ds.dataspaceunit.upm/dataset", + ontologyUrl: "http://ontology-hub-demo.dev.ds.dataspaceunit.upm", participantId: "connector-c1", storageAccount: "company2assets", storageExplorerLinkTemplate: "storageexplorer://v=1", diff --git a/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/build.gradle.kts b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/build.gradle.kts index f3ddde72..94e38b92 100644 --- a/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/build.gradle.kts +++ b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/build.gradle.kts @@ -9,6 +9,8 @@ dependencies { api(project(":extensions:vocabulary-shared-api")) api(project(":extensions:federated-catalog-cache-api")) + implementation("org.glassfish.jersey.media:jersey-media-multipart:2.41") + implementation("org.apache.jena:apache-jena-libs:5.1.0") implementation(libs.edc.control.plane.spi) implementation(libs.edc.lib.validator) implementation(libs.edc.transaction.spi) diff --git a/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyJsonValidator.java b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyJsonValidator.java deleted file mode 100644 index 5806cf0e..00000000 --- a/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyJsonValidator.java +++ /dev/null @@ -1,137 +0,0 @@ -package org.upm.inesdata.validator; - -import jakarta.json.JsonObject; -import jakarta.json.JsonString; -import jakarta.json.JsonValue; -import org.eclipse.edc.validator.spi.ValidationResult; -import org.eclipse.edc.validator.spi.Validator; -import org.eclipse.edc.spi.monitor.Monitor; - -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.HashMap; -import java.util.Map; - -public class OntologyJsonValidator implements Validator { - - private final Monitor monitor; - private final String allowedDomain; - - public OntologyJsonValidator(String allowedDomain, Monitor monitor) { - this.allowedDomain = allowedDomain; - this.monitor = monitor; - } - - @Override - public ValidationResult validate(JsonObject input) { - - monitor.info("[OntologyValidator] Start validation."); - - Map flat = new HashMap<>(); - flattenJson("", input, flat); - - String foundKey = null; - String url = null; - - for (var e : flat.entrySet()) { - if (e.getKey().endsWith("urlOntologyHub")) { - foundKey = e.getKey(); - url = e.getValue(); - break; - } - } - - if (foundKey == null) { - monitor.info("[OntologyValidator] No urlOntologyHub found."); - return ValidationResult.success(); - } - - String cleanPath = normalizePath(foundKey); - - if (url == null || url.isBlank()) { - monitor.info("[OntologyValidator] urlOntologyHub empty → success."); - return ValidationResult.success(); - } - - // Validación de dominio — WARNING, no error - if (allowedDomain != null && !allowedDomain.isBlank()) { - if (!url.startsWith(allowedDomain)) { - monitor.warning("[OntologyValidator] WARNING: Domain not allowed: " + url); - return ValidationResult.success(); // ⚠️ NO bloquear - } - } - - // HEAD request para validar existencia - try { - URL target = new URL(url); - HttpURLConnection http = (HttpURLConnection) target.openConnection(); - http.setRequestMethod("HEAD"); - http.setConnectTimeout(3000); - http.setReadTimeout(3000); - http.connect(); - - int code = http.getResponseCode(); - monitor.info("[OntologyValidator] HEAD=" + code); - - if (code == 404 || code == 400) { - monitor.warning("[OntologyValidator] WARNING: URL unreachable: HTTP " + code); - return ValidationResult.success(); // ⚠️ NO bloquear - } - - } catch (Exception e) { - monitor.warning("[OntologyValidator] WARNING: Error reaching URL: " + e.getMessage()); - return ValidationResult.success(); // ⚠️ NO bloquear - } - - return ValidationResult.success(); - } - - // Normaliza rutas JSON‑LD largas - private String normalizePath(String path) { - if (path == null) return "urlOntologyHub"; - String[] parts = path.split("\\."); - StringBuilder sb = new StringBuilder(); - for (String p : parts) { - if (p.startsWith("http")) { - int i = p.lastIndexOf("/"); - if (i != -1 && i < p.length()-1) p = p.substring(i+1); - } - if (sb.length() > 0) sb.append("."); - sb.append(p); - } - return sb.toString(); - } - - // Flatten JSON‑LD - private void flattenJson(String prefix, JsonValue value, Map flat) { - switch (value.getValueType()) { - - case OBJECT: - JsonObject obj = value.asJsonObject(); - - if (obj.containsKey("@value")) { - flat.put(prefix, obj.getString("@value")); - return; - } - - for (String key : obj.keySet()) { - String next = prefix.isEmpty() ? key : prefix + "." + key; - flattenJson(next, obj.get(key), flat); - } - break; - - case ARRAY: - for (JsonValue item : value.asJsonArray()) { - flattenJson(prefix, item, flat); - } - break; - - case STRING: - flat.put(prefix, ((JsonString) value).getString()); - break; - - default: - flat.put(prefix, value.toString()); - } - } -} diff --git a/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyValidatorExtension.java b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyValidatorExtension.java index c98ee78d..9e66921b 100644 --- a/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyValidatorExtension.java +++ b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyValidatorExtension.java @@ -1,48 +1,36 @@ package org.upm.inesdata.validator; import org.eclipse.edc.runtime.metamodel.annotation.Extension; -import org.eclipse.edc.runtime.metamodel.annotation.Provides; import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; -import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry; +import org.eclipse.edc.web.spi.WebService; +import org.upm.inesdata.validator.controller.AssetRdfValidationApiController; +import org.upm.inesdata.validator.services.RdfValidationService; +import org.upm.inesdata.validator.services.impl.JenaValidationService; -import static org.eclipse.edc.connector.controlplane.asset.spi.domain.Asset.EDC_ASSET_TYPE; - -@Provides(OntologyValidatorExtension.class) @Extension(OntologyValidatorExtension.NAME) public class OntologyValidatorExtension implements ServiceExtension { public static final String NAME = "Ontology Validator Extension"; @Inject - private JsonObjectValidatorRegistry validatorRegistry; // <---- INYECCIÓN REAL + private WebService webService; + @Inject private Monitor monitor; - @Override - public String name() { - return NAME; - } - @Override public void initialize(ServiceExtensionContext context) { - monitor = context.getMonitor(); - monitor.info("🔥 OntologyValidatorExtension initialized!!"); - } - @Override - public void prepare() { + RdfValidationService validationService = new JenaValidationService(); + context.registerService(RdfValidationService.class, validationService); - String allowedDomain = "http://demo-ontology-hub.demo.svc.cluster.local"; + var controller = new AssetRdfValidationApiController(validationService); - monitor.info("🧪 Registrando OntologyJsonValidator para dominio permitido: " + allowedDomain); + webService.registerResource("management", controller); - // SI ESTA LÍNEA SE EJECUTA VERÁS EL LOG ARRIBA - validatorRegistry.register( - EDC_ASSET_TYPE, - new OntologyJsonValidator(allowedDomain, monitor) - ); + monitor.info("🔥 OntologyValidatorExtension initialized!"); } } \ No newline at end of file diff --git a/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyWarningHolder.java b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyWarningHolder.java deleted file mode 100644 index 5c04a2fc..00000000 --- a/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyWarningHolder.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.upm.inesdata.validator; - -public class OntologyWarningHolder { - private static final ThreadLocal warning = new ThreadLocal<>(); - - public static void set(String msg) { warning.set(msg); } - public static String get() { return warning.get(); } - public static void clear() { warning.remove(); } -} \ No newline at end of file diff --git a/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/controller/AssetRdfValidationApiController.java b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/controller/AssetRdfValidationApiController.java new file mode 100644 index 00000000..83c6f80c --- /dev/null +++ b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/controller/AssetRdfValidationApiController.java @@ -0,0 +1,56 @@ +package org.upm.inesdata.validator.controller; + +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.*; + +import org.eclipse.edc.validator.spi.ValidationResult; +import org.glassfish.jersey.media.multipart.FormDataParam; +import org.upm.inesdata.validator.services.enums.RdfFormat; +import org.upm.inesdata.validator.services.RdfValidationService; + +import java.io.InputStream; + +@Path("/validation") +public class AssetRdfValidationApiController { + + private final RdfValidationService validationService; + + public AssetRdfValidationApiController(RdfValidationService validationService) { + this.validationService = validationService; + } + + @POST + @Path("/rdf_asset") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.APPLICATION_JSON) + public Response assetRdfValidation( + @FormDataParam("ontologyUrl") String ontologyUrl, + @FormDataParam("shaclUrl") String shaclUrl, + @FormDataParam("rdf") InputStream rdfStream, + @FormDataParam("format") String format + ) { + final RdfFormat rdfFormat; + try { + rdfFormat = RdfFormat.fromInput(format); + } catch (IllegalArgumentException e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(e.getMessage()) + .build(); + } + + var result = validationService.validate( + rdfStream, + RdfFormat.valueOf(format.toUpperCase()), + ontologyUrl, + shaclUrl + ); + + if (result.failed()) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(result.toResult().getFailureMessages()) + .build(); + } + + return Response.ok().build(); + } +} diff --git a/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/dtos/ValidationResponse.java b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/dtos/ValidationResponse.java new file mode 100644 index 00000000..ac4d85ce --- /dev/null +++ b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/dtos/ValidationResponse.java @@ -0,0 +1,16 @@ +package org.upm.inesdata.validator.dtos; + +import java.util.List; + +public class ValidationResponse { + + public boolean conforms; + public List violations; + + public static class Violation { + public String message; + public String path; + public String focusNode; + public String severity; + } +} diff --git a/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/services/RdfValidationService.java b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/services/RdfValidationService.java new file mode 100644 index 00000000..507b2ac8 --- /dev/null +++ b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/services/RdfValidationService.java @@ -0,0 +1,17 @@ +package org.upm.inesdata.validator.services; + +import org.eclipse.edc.validator.spi.ValidationResult; + +import java.io.InputStream; + +import org.upm.inesdata.validator.services.enums.RdfFormat; + +public interface RdfValidationService { + + ValidationResult validate( + InputStream rdfStream, + RdfFormat format, + String ontologyUrl, + String shaclUrl + ); +} diff --git a/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/services/enums/RdfFormat.java b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/services/enums/RdfFormat.java new file mode 100644 index 00000000..a3de8d97 --- /dev/null +++ b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/services/enums/RdfFormat.java @@ -0,0 +1,51 @@ +package org.upm.inesdata.validator.services.enums; + +public enum RdfFormat { + + TURTLE("text/turtle"), + RDFXML("application/rdf+xml"), + JSONLD("application/ld+json"), + NTRIPLES("application/n-triples"), + N3("text/n3"); + + private final String mimeType; + + RdfFormat(String mimeType) { + this.mimeType = mimeType; + } + + public String getMimeType() { + return mimeType; + } + + public static RdfFormat fromMimeType(String mimeType) { + if (mimeType == null) { + throw new IllegalArgumentException("mimeType is null"); + } + + for (RdfFormat format : values()) { + if (mimeType.startsWith(format.mimeType)) { + return format; + } + } + + throw new IllegalArgumentException("Unsupported RDF mime type: " + mimeType); + } + + public static RdfFormat fromInput(String input) { + if (input == null || input.isBlank()) { + throw new IllegalArgumentException("format is null or empty"); + } + + var normalized = input.trim().toLowerCase(); + + return switch (normalized) { + case "turtle", "ttl", "text/turtle" -> TURTLE; + case "rdfxml", "rdf/xml", "application/rdf+xml" -> RDFXML; + case "jsonld", "json-ld", "application/ld+json" -> JSONLD; + case "ntriples", "n-triples", "nt", "application/n-triples" -> NTRIPLES; + case "n3", "notation3", "text/n3" -> N3; + default -> throw new IllegalArgumentException("Unsupported RDF format: " + input); + }; + } +} \ No newline at end of file diff --git a/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/services/impl/JenaValidationService.java b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/services/impl/JenaValidationService.java new file mode 100644 index 00000000..526b79dd --- /dev/null +++ b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/services/impl/JenaValidationService.java @@ -0,0 +1,132 @@ +package org.upm.inesdata.validator.services.impl; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.riot.Lang; +import org.apache.jena.riot.RDFDataMgr; +import org.apache.jena.shacl.ShaclValidator; +import org.apache.jena.shacl.Shapes; +import org.apache.jena.shacl.ValidationReport; +import org.apache.jena.reasoner.Reasoner; +import org.apache.jena.reasoner.ReasonerRegistry; +import org.apache.jena.rdf.model.InfModel; + +import org.eclipse.edc.validator.spi.ValidationResult; +import org.eclipse.edc.validator.spi.Violation; +import org.upm.inesdata.validator.services.enums.RdfFormat; +import org.upm.inesdata.validator.services.RdfValidationService; + +import java.io.InputStream; +import java.net.URL; +import java.util.List; + +public class JenaValidationService implements RdfValidationService { + + @Override + public ValidationResult validate( + InputStream rdfStream, + RdfFormat format, + String ontologyUrl, + String shaclUrl + ) { + // Transform URLs for minikube environment + ontologyUrl = transformUrlForMinikube(ontologyUrl); + shaclUrl = transformUrlForMinikube(shaclUrl); + + // 1. Load RDF data + Model dataModel = ModelFactory.createDefaultModel(); + try { + RDFDataMgr.read(dataModel, rdfStream, mapLang(format)); + } catch (Exception e) { + return failure("RDF data parse error", "rdf", e); + } + + // 2. Load ontology (required for validation with inference) + Model ontologyModel = null; + if (ontologyUrl != null && !ontologyUrl.isBlank()) { + try { + ontologyModel = loadOntologyModel(ontologyUrl); + } catch (Exception e) { + return failure("Ontology load error", ontologyUrl, e); + } + } else { + return failure("Ontology URL is required", "", new IllegalArgumentException("ontologyUrl cannot be null or blank")); + } + + // 3. Create inferred model using ontology + Reasoner reasoner = ReasonerRegistry.getOWLReasoner(); + reasoner = reasoner.bindSchema(ontologyModel); + InfModel infModel = ModelFactory.createInfModel(reasoner, dataModel); + + // 4. Load SHACL shapes + Shapes shapes; + try { + Model shapesModel = RDFDataMgr.loadModel(shaclUrl); + shapes = Shapes.parse(shapesModel); + } catch (Exception e) { + return failure("SHACL shapes load error", shaclUrl, e); + } + + // 5. Validate + final ValidationReport report; + try { + report = ShaclValidator.get().validate(shapes, infModel.getGraph()); + } catch (Exception e) { + return failure("SHACL validation execution error", "", e); + } + + if (report.conforms()) { + return ValidationResult.success(); + } + + // 6. Map SHACL violations → EDC Violations + List violations = report.getEntries().stream() + .map(e -> new Violation( + e.message(), + e.resultPath() != null ? e.resultPath().toString() : "", + e.value() != null ? e.value().toString() : "" + )) + .toList(); + + return ValidationResult.failure(violations); + } + + private ValidationResult failure(String message, String path, Exception e) { + var detail = e.getMessage() != null ? e.getMessage() : e.toString(); + return ValidationResult.failure(List.of(new Violation(message, path != null ? path : "", detail))); + } + + private Lang mapLang(RdfFormat format) { + return switch (format) { + case TURTLE -> Lang.TURTLE; + case RDFXML -> Lang.RDFXML; + case JSONLD -> Lang.JSONLD; + case NTRIPLES -> Lang.NTRIPLES; + case N3 -> Lang.N3; + }; + } + + private Model loadOntologyModel(String ontologyUrl) { + try { + Model ontologyModel = RDFDataMgr.loadModel(ontologyUrl); + return ontologyModel; + } catch (Exception firstError) { + // Fallback for ontology endpoints serving N3 with weak/incorrect content-type metadata. + try (InputStream ontologyStream = new URL(ontologyUrl).openStream()) { + Model ontologyModel = ModelFactory.createDefaultModel(); + RDFDataMgr.read(ontologyModel, ontologyStream, Lang.N3); + return ontologyModel; + } catch (Exception fallbackError) { + firstError.addSuppressed(fallbackError); + throw firstError; + } + } + } + + private String transformUrlForMinikube(String url) { + if (url != null) { + return url.replace("ontology-hub-demo.dev.ds.dataspaceunit.upm", "demo-ontology-hub:3333"); + } + return url; + } +} \ No newline at end of file