diff --git a/README.md b/README.md index 2c50462bf08cb4005a582f3239282beddeea26d8..6c118d70fe7a9f36d8a04a0278806fb411c0e564 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ Backend service to convert an input shacl shape into JSON form for the frontend service. - Input is expected to be a MultipartFile file. - Output object is ShaclModel.java. It consists of a list of json objects for prefixes in the input file and another list of json objects for the shapes listed in the input file. +- Alternatively input can be two MultipartFiles (one .ttl SHACL Shape, one .json Claim-File), behaviour is the same, but the answer would be a ResponseShaclJsonPair.java, which consists out of a ShaclModel.java and a Map of Strings containing overlapping attributes. This would be done such that the Frontend can prefill the displayed FormFields. - Each shape is depicted in VicShape.java. Shape name is stored in variable schema. Properties of the shape are stored in variable called 'constraints' which is of type ShapeProperties.java. - Test instances can be found under src/test/resources. - JUnit test cases can be found under src/test/java. diff --git a/src/main/java/eu/gaiax/sdcreationwizard/api/ConversionController.java b/src/main/java/eu/gaiax/sdcreationwizard/api/ConversionController.java index 4018686decfeb3d408f21574cf928e9dc5aa7b70..7d34fded178cf318979d815fdd817be440268a9e 100644 --- a/src/main/java/eu/gaiax/sdcreationwizard/api/ConversionController.java +++ b/src/main/java/eu/gaiax/sdcreationwizard/api/ConversionController.java @@ -1,6 +1,8 @@ package eu.gaiax.sdcreationwizard.api; import com.fasterxml.jackson.databind.ObjectMapper; + +import eu.gaiax.sdcreationwizard.api.dto.ResponseShaclJsonPair; import eu.gaiax.sdcreationwizard.api.dto.ShaclFileUtils; import eu.gaiax.sdcreationwizard.api.dto.ShaclModel; import org.apache.commons.io.FileUtils; @@ -182,6 +184,29 @@ public class ConversionController { ? HttpStatus.BAD_REQUEST : HttpStatus.OK); } + /** + * Converts a given SHACL file in conjunction with a json file to a JSON object (ResponseShaclJsonPair) including + * Shacl Model and a Map of Subjects and their values, which are present in both files + * + * @param shaclFile a multipart SHACL file with *.ttl extension + * @param jsonFile a multipart json file with *.json extension + * @return a JSON serialization of the processed content + */ + @PostMapping(value = "/convertAndPrefillFile", produces = "application/json") + @CrossOrigin(origins = "*") + public ResponseEntity<Object> convertShaclAndJson(@RequestParam("file") MultipartFile shaclFile, @RequestParam("jsonFile") MultipartFile jsonFile) { + logger.info("Converting JSON and SHACL file: {}, {}", jsonFile.getOriginalFilename(), shaclFile.getOriginalFilename()); + ResponseShaclJsonPair response; + try { + response = ConversionService.checkShaclForJson(shaclFile, jsonFile); + } catch (IOException e) { + return new ResponseEntity<>("Shacl conversion failed <-> Bad Input", HttpStatus.BAD_REQUEST); + } catch (Exception e) { + return new ResponseEntity<>("Error: " + e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } + return new ResponseEntity<>(response, HttpStatus.OK); + } + /** * To be used after /getAvailableShapes. Returns the content of a predefined JSON file. @@ -277,4 +302,4 @@ public class ConversionController { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } } -} +} \ No newline at end of file diff --git a/src/main/java/eu/gaiax/sdcreationwizard/api/ConversionService.java b/src/main/java/eu/gaiax/sdcreationwizard/api/ConversionService.java index 37e110c68256a668e18995f4f60b2c79574e18c7..4621bd3b3aa6f0f3a9f6d631168607b1a37cde36 100644 --- a/src/main/java/eu/gaiax/sdcreationwizard/api/ConversionService.java +++ b/src/main/java/eu/gaiax/sdcreationwizard/api/ConversionService.java @@ -3,6 +3,9 @@ package eu.gaiax.sdcreationwizard.api; import eu.gaiax.sdcreationwizard.api.dto.*; import org.apache.jena.rdf.model.*; import org.apache.jena.vocabulary.SKOS; +import org.apache.jena.riot.Lang; +import org.apache.jena.riot.RDFDataMgr; +import org.apache.jena.shacl.vocabulary.SHACL; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -63,6 +66,39 @@ public class ConversionService { return new ShaclModel(prefixList, shapesSorted); } + public static ResponseShaclJsonPair checkShaclForJson(final MultipartFile shaclFile, final MultipartFile jsonFile) throws IOException { + Map<String, String> matchedSubjects = new HashMap<>(); + + try(InputStream inputStreamJson = jsonFile.getInputStream(); + InputStream inputStreamShacl = shaclFile.getInputStream()) { + + Model modelJson = ModelFactory.createDefaultModel(); + Model modelShacl = ModelFactory.createDefaultModel(); + RDFDataMgr.read(modelShacl, inputStreamShacl, Lang.TTL); + RDFDataMgr.read(modelJson, inputStreamJson, Lang.JSONLD); + + // Extract paths from SHACL shapes + Set<String> shaclPaths = new HashSet<>(); + Property shaclPath = modelShacl.createProperty(SHACL.path.getURI()); + StmtIterator it = modelShacl.listStatements((Resource) null, shaclPath, (RDFNode) null); + while (it.hasNext()) { + Statement stmt = it.nextStatement(); + shaclPaths.add(stmt.getResource().getURI()); + } + + // Check, if any subject in JSON-LD matches a SHACL path and add matches to a list + StmtIterator jsonldIt = modelJson.listStatements(); + while (jsonldIt.hasNext()) { + Statement stmt = jsonldIt.nextStatement(); + Property predicate = stmt.getPredicate(); + if (shaclPaths.contains(predicate.getURI())) { + matchedSubjects.put(predicate.getURI(), stmt.getObject().toString()); + } + } + } + return new ResponseShaclJsonPair(convertToJson(shaclFile.getInputStream()), matchedSubjects); + } + private static int calculateDepth(VicShape shape, List<VicShape> shapes, Map<VicShape, Integer> visitedShapes) { if (visitedShapes.containsKey(shape)) { return visitedShapes.get(shape); diff --git a/src/main/java/eu/gaiax/sdcreationwizard/api/dto/ResponseShaclJsonPair.java b/src/main/java/eu/gaiax/sdcreationwizard/api/dto/ResponseShaclJsonPair.java new file mode 100644 index 0000000000000000000000000000000000000000..a043705d8f381983ff11e6ee766542a1100200f3 --- /dev/null +++ b/src/main/java/eu/gaiax/sdcreationwizard/api/dto/ResponseShaclJsonPair.java @@ -0,0 +1,25 @@ +package eu.gaiax.sdcreationwizard.api.dto; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ResponseShaclJsonPair{ + + private final ShaclModel shaclModel; + private final Map<String, String> matchedSubjects; + + public ResponseShaclJsonPair(ShaclModel shaclModel, Map<String, String> matchedSubjects) { + this.shaclModel = shaclModel; + this.matchedSubjects = matchedSubjects; + } + + public ShaclModel getShaclModel() { + return this.shaclModel; + } + + public Map<String, String> getMatchedSubjects() { + return this.matchedSubjects; + } +} diff --git a/src/test/java/eu/gaiax/sdcreationwizard/api/ConversionServicePositiveTests.java b/src/test/java/eu/gaiax/sdcreationwizard/api/ConversionServicePositiveTests.java index 30a4003672dfcc42c3dba5d946162092fee67b08..4944e4e164a703a9e2bab6f8ea20a7cd2a6b830d 100644 --- a/src/test/java/eu/gaiax/sdcreationwizard/api/ConversionServicePositiveTests.java +++ b/src/test/java/eu/gaiax/sdcreationwizard/api/ConversionServicePositiveTests.java @@ -28,10 +28,16 @@ import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; +import eu.gaiax.sdcreationwizard.api.dto.ResponseShaclJsonPair; + +import static org.junit.jupiter.api.Assertions.assertTrue; + import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; +import java.util.Map; @RunWith(Parameterized.class) @WebAppConfiguration @@ -191,4 +197,40 @@ public class ConversionServicePositiveTests { JSONAssert.assertEquals(outputFile, mvcResult.getResponse().getContentAsString(), strictComparison); } + + /** + * individual test to test "checkShaclForJson" method + * takes two files (test-checkShaclForJson.ttl and test-checkShaclForJson.json) + * checks whether all the keyvalue pairs that match are indeed included in the result + */ + @Test + public void testCheckShaclForJson() throws Exception { + // prepare + Map<String, String> expectedMap = new HashMap<>(); + expectedMap.put("http://schema.org/name", "msg Systems hospital"); + expectedMap.put("http://schema.org/streetAddress", "Robert-Bürkle-Straße 1"); + expectedMap.put("http://w3id.org/gaia-x/ex#locality", "Ismaning"); + expectedMap.put("http://schema.org/postalCode", "85737^^http://www.w3.org/2001/XMLSchema#integer"); + expectedMap.put("http://w3id.org/gaia-x/ex#countryCode", "DE"); + expectedMap.put("http://w3id.org/gaia-x/ex#number", "123456789^^http://www.w3.org/2001/XMLSchema#integer"); + + String individualInputFilename = "test-checkShaclForJson.ttl"; + String individualInputJsonFilname = "test-checkShaclForJson.json"; + + ClassLoader classloader = Thread.currentThread().getContextClassLoader(); + InputStream inputFile = classloader.getResourceAsStream(individualInputFilename); + InputStream inputJsonFile = classloader.getResourceAsStream(individualInputJsonFilname); + + MockMultipartFile mockMultipartFileTtl = new MockMultipartFile("fileTtl", individualInputFilename, + MediaType.MULTIPART_FORM_DATA_VALUE, inputFile); + MockMultipartFile mockMultipartFileJson = new MockMultipartFile("fileJson", individualInputFilename, + MediaType.MULTIPART_FORM_DATA_VALUE, inputJsonFile); + + // action + ResponseShaclJsonPair responseShaclJsonPairFromTestFiles = ConversionService.checkShaclForJson(mockMultipartFileTtl, mockMultipartFileJson); + Map<String, String> actualMap = responseShaclJsonPairFromTestFiles.getMatchedSubjects(); + + // test + assertTrue(actualMap.entrySet().containsAll(expectedMap.entrySet())); + } } diff --git a/src/test/resources/test-checkShaclForJson.json b/src/test/resources/test-checkShaclForJson.json new file mode 100644 index 0000000000000000000000000000000000000000..367a98eab567d06047f28e7647977887969d4a58 --- /dev/null +++ b/src/test/resources/test-checkShaclForJson.json @@ -0,0 +1,31 @@ +{ + "@context": { + "schema": "http://schema.org/", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "ex": "http://w3id.org/gaia-x/ex#", + "sh": "http://www.w3.org/ns/shacl#" + }, + "@id": "did:example:1234", + "@type": "schema:Hospital", + "schema:name": { + "@value": "msg Systems hospital", + "@type": "xsd:string" + }, + "schema:address": { + "@type": "schema:address", + "schema:streetAddress": { + "@value": "Robert-Bürkle-Straße 1", + "@type": "xsd:string" + }, + "ex:locality": { + "@value": "Ismaning", + "@type": "xsd:string" + }, + "schema:postalCode": 85737 + }, + "schema:telephone": { + "@type": "ex:phone", + "ex:number": 123456789, + "ex:countryCode": "DE" + } +} \ No newline at end of file diff --git a/src/test/resources/test-checkShaclForJson.ttl b/src/test/resources/test-checkShaclForJson.ttl new file mode 100644 index 0000000000000000000000000000000000000000..336e397e9964e66f0e22ea2a477bbf7ee5d224b8 --- /dev/null +++ b/src/test/resources/test-checkShaclForJson.ttl @@ -0,0 +1,134 @@ +@prefix schema: <http://schema.org/> . +@prefix sh: <http://www.w3.org/ns/shacl#> . +@prefix xsd: <http://www.w3.org/2001/XMLSchema#> . +@prefix ex: <http://w3id.org/gaia-x/ex#> . + +ex:HospitalShape + a sh:NodeShape; + sh:targetClass schema:Hospital; + sh:property [ + sh:path schema:name ; + sh:name "name" ; + sh:datatype xsd:string ; + sh:minCount 1; + sh:description "Name of the hospital" ; + sh:order 1 + ]; + sh:property [ + sh:path schema:address ; + sh:node ex:AddressShape ; + sh:minCount 1; + sh:order 2; + ] ; + sh:property [ + sh:path schema:telephone ; + sh:node ex:TelephoneShape ; + sh:minCount 1; + sh:order 3; + ]; + sh:property [ + sh:path schema:Person ; + sh:node ex:PersonShape ; + sh:order 4 ; + ]. + +ex:PersonShape + a sh:NodeShape ; + sh:targetClass schema:Person ; + sh:property [ + sh:path schema:givenName ; + sh:name "given name" ; + sh:datatype xsd:string ; + sh:minCount 1; + sh:maxCount 1; + sh:minLength 3; + sh:maxLength 8; + sh:order 31 + + ] ; + sh:property [ + sh:path schema:gender ; + sh:in ( "female" "male" ) ; + sh:minCount 1; + sh:maxCount 1; + sh:order 32; + ] ; + sh:property [ + sh:path schema:birthDate ; + sh:datatype xsd:date ; + sh:minCount 1; + sh:maxCount 1; + sh:order 33; + ] ; + + sh:property [ + sh:path ex:age ; + sh:datatype xsd:integer ; + sh:minCount 1; + sh:maxCount 1; + sh:minInclusive 18 ; + sh:maxInclusive 100 ; + sh:order 34; + ] ; + sh:property [ + sh:path schema:address ; + sh:node ex:AddressShape ; + sh:minCount 1; + sh:order 35; + ] ; + sh:property [ + sh:path schema:telephone ; + sh:node ex:TelephoneShape ; + sh:minCount 1; + sh:order 36; + ]. + +ex:AddressShape + a sh:NodeShape ; + sh:targetClass schema:address ; + sh:property [ + sh:path schema:streetAddress ; + sh:name "street address" ; + sh:datatype xsd:string ; + sh:order 11 ; + sh:minLength 3; + sh:description "The street address including number" ; + sh:group ex:AddressGroup ; + ] ; + sh:property [ + sh:path ex:locality ; + sh:name "locality" ; + sh:datatype xsd:string ; + sh:order 12 ; + sh:description "The suburb, city or town of the address" ; + sh:group ex:AddressGroup ; + ]; + sh:property [ + sh:path schema:postalCode ; + sh:name "postal code" ; + sh:datatype xsd:integer ; + sh:order 13 ; + sh:description "The postal code of the locality" ; + sh:group ex:AddressGroup ; + ] . + +ex:TelephoneShape + a sh:NodeShape; + sh:targetClass ex:phone ; + sh:property [ + sh:path ex:countryCode ; + sh:name "country code" ; + sh:minCount 1; + sh:maxCount 1; + sh:description "Country code" ; + sh:group ex:PhoneGroup; + ]; + sh:property [ + sh:path ex:number ; + sh:name "number" ; + sh:datatype xsd:integer ; + sh:minCount 1; + sh:description "The phone number" ; + sh:group ex:PhoneGroup; + ]. +