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/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 12460586..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 @@ -232,6 +232,69 @@ + +
+ Ontology Information + + + URI Ontology + + +
+ +
+ + {{ontology.titles[0]?.value}} + +
+
+ +
+ + + 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 fea7d621..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'; @@ -20,6 +21,10 @@ 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'; +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({ @@ -39,6 +44,11 @@ export class AssetCreateComponent implements OnInit { ] }; + //manage the validation button for semantic files + showTestFileButton = false; + showUploadFileButton = false; + + // Dynamic forms from vocabylary variables vocabularies: Vocabulary[]; selectedVocabularies: Vocabulary[] @@ -75,6 +85,15 @@ export class AssetCreateComponent implements OnInit { keywords: string = ''; format: string = ''; byteSize: string = ''; + ontologyUri: string = ''; + ontologyVersion: string = ''; + ontologyShacle: any = ''; + + ontologies: Ontology[] = []; + filteredOntologies: Ontology[] = []; + ontologyVersions: OntologyVersion[] = []; + ontologyShacles: any[] = []; + // Storage information amazonS3DataAddress: AmazonS3DataAddress = { @@ -90,6 +109,10 @@ export class AssetCreateComponent implements OnInit { type: 'InesDataStore' }; + shaclsInesDataStoreAddress: InesDataStoreAddress = { + type: 'InesDataStore' + }; + assetType: any; assetTypes = Object.entries(ASSET_TYPES); defaultForms: JsonFormData[]; @@ -111,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; @@ -151,7 +182,10 @@ export class AssetCreateComponent implements OnInit { private notificationService: NotificationService, @Inject('STORAGE_TYPES') public storageTypes: StorageType[], private router: Router, - private loadingService: LoadingService) { + private loadingService: LoadingService, + private ontologyService: OntologyService, + private semanticFileValidator: SemanticFileValidatorService, + private dialog: MatDialog) { } async onSave() { @@ -245,7 +279,9 @@ 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["ontologyVersion"] = this.ontologyVersion; + properties["ontologyShacle"] = this.ontologyShacle; this.addKeywords(properties); } @@ -290,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; } @@ -349,12 +389,23 @@ 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) { + this.showUploadFileButton = false; + if (event?.length > 0) { + this.showUploadFileButton = true; + this.shaclsInesDataStoreAddress.file = event[0]; + } else { + delete this.shaclsInesDataStoreAddress.file; + } + } + onSelectionChangeVocabulary() { this.selectedForms = [] @@ -448,6 +499,152 @@ 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 || this.ontologyShacle === 'newShacleFile' || this.ontologyShacle === ''){ + return + } + + await this.semanticFileValidator.isASemanticFile(this.inesDataStoreAddress.file).then(res => { + isSemanticFile = res + }); + + 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; + } + + 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 @@ - + = new EventEmitter() diff --git a/adapters/inesdata/sources/inesdata-connector-interface/src/app/shared/models/ontology.ts b/adapters/inesdata/sources/inesdata-connector-interface/src/app/shared/models/ontology.ts index 9eb081e4..b6444d23 100644 --- a/adapters/inesdata/sources/inesdata-connector-interface/src/app/shared/models/ontology.ts +++ b/adapters/inesdata/sources/inesdata-connector-interface/src/app/shared/models/ontology.ts @@ -3,6 +3,8 @@ export interface Ontology { nsp: string prefix: string titles: OntologyTitle[] + versions: OntologyVersion[] + artifacts: OntologyArtifacts } export interface OntologyTitle { @@ -10,3 +12,14 @@ export interface OntologyTitle { lang: string _id: string } + + +export interface OntologyVersion { + name: string + issued: string + isReviewed: boolean +} + +export interface OntologyArtifacts { + shapes: string[] +} 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..7aec2d94 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 @@ -13,7 +13,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; -import { Observable, from, lastValueFrom } from 'rxjs'; +import { Observable, from, lastValueFrom, map, switchMap } from 'rxjs'; import { expandArray, Asset, EDC_CONTEXT, JSON_LD_DEFAULT_CONTEXT } from '@think-it-labs/edc-connector-client'; import { AssetInput, QuerySpec } from "../models/edc-connector-entities" @@ -28,6 +28,7 @@ export class AssetService { private readonly BASE_URL = `${environment.runtime.managementApiUrl}${environment.runtime.service.asset.baseUrl}`; private readonly UPLOAD_CHUNK_URL = `${environment.runtime.managementApiUrl}${environment.runtime.service.asset.uploadChunk}`; private readonly FINALIZE_UPLOAD_URL = `${environment.runtime.managementApiUrl}${environment.runtime.service.asset.finalizeUpload}`; + private readonly TEST_RDF_ASSET = `${environment.runtime.managementApiUrl}/validation/rdf_asset`; constructor(private http: HttpClient) { } @@ -47,9 +48,7 @@ export class AssetService { } } - return from(lastValueFrom(this.http.post( - `${this.BASE_URL}`, body - ))); + return this.http.post(`${this.BASE_URL}`, body); } /** @@ -61,9 +60,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 +74,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,10 +93,10 @@ export class AssetService { } } - return from(lastValueFrom(this.http.post>( + return this.http.post>( `${this.BASE_URL}${environment.runtime.service.asset.getAll}`, body - )).then(results => { - return expandArray(results, () => new Asset()); + ).pipe(switchMap((results: Array) => { + return from(expandArray(results, () => new Asset())); })); } @@ -114,9 +113,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 { @@ -176,5 +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 new file mode 100644 index 00000000..fac9a140 --- /dev/null +++ b/adapters/inesdata/sources/inesdata-connector-interface/src/app/shared/services/semantic-file-validator.service.ts @@ -0,0 +1,42 @@ +import { Injectable } from "@angular/core"; +import * as $rdf from "rdflib"; + +@Injectable({ + providedIn: "root", +}) +export class SemanticFileValidatorService { + + // 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 + ]; + + private readonly BASE_IRI = "urn:semantic-file:"; // identificador ficticio + + constructor() {} + + 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) { + // 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/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..94e38b92 --- /dev/null +++ b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/build.gradle.kts @@ -0,0 +1,19 @@ +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("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/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..9e66921b --- /dev/null +++ b/adapters/inesdata/sources/inesdata-connector/extensions/ontology-validator/src/main/java/org/upm/inesdata/validator/OntologyValidatorExtension.java @@ -0,0 +1,36 @@ +package org.upm.inesdata.validator; + +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +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.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; + +@Extension(OntologyValidatorExtension.NAME) +public class OntologyValidatorExtension implements ServiceExtension { + + public static final String NAME = "Ontology Validator Extension"; + + @Inject + private WebService webService; + + @Inject + private Monitor monitor; + + @Override + public void initialize(ServiceExtensionContext context) { + + RdfValidationService validationService = new JenaValidationService(); + context.registerService(RdfValidationService.class, validationService); + + var controller = new AssetRdfValidationApiController(validationService); + + webService.registerResource("management", controller); + + 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/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 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")