diff --git a/docs/05-internals/development/updating-repositories.md b/docs/05-internals/development/updating-repositories.md index 8ce655d439..4f9271f8bf 100644 --- a/docs/05-internals/development/updating-repositories.md +++ b/docs/05-internals/development/updating-repositories.md @@ -49,14 +49,14 @@ it to `org.knora.webapi.store.triplestore.upgrade.RepositoryUpdater`. 2. Consult `org.knora.webapi.store.triplestore.upgrade.RepositoryUpdatePlan` to see which transformations are needed. -3. Download the entire repository from the triplestore into a TriG file. +3. Download the entire repository from the triplestore into an N-Quads file. -4. Read the TriG file into an `RdfModel`. +4. Read the N-Quads file into an `RdfModel`. 5. Update the `RdfModel` by running the necessary transformations, and replacing the built-in Knora ontologies with the current ones. -6. Save the `RdfModel` to a new TriG file. +6. Save the `RdfModel` to a new N-Quads file. 7. Empty the repository in the triplestore. @@ -115,6 +115,6 @@ has been carried out. If the requirement is not met, the plugin can throw ## Testing Update Plugins Each plugin should have a unit test that extends `UpgradePluginSpec`. A typical -test loads a TriG file containing test data into a `RdfModel`, runs the plugin, +test loads a file containing RDF test data into a `RdfModel`, runs the plugin, makes an `RdfRepository` containing the transformed `RdfModel`, and uses SPARQL to check the result. diff --git a/test_data/rdfFormatUtil/BookReiseInsHeiligeLand.nq b/test_data/rdfFormatUtil/BookReiseInsHeiligeLand.nq new file mode 100644 index 0000000000..1849aa1c38 --- /dev/null +++ b/test_data/rdfFormatUtil/BookReiseInsHeiligeLand.nq @@ -0,0 +1,254 @@ + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W"^^ . + . + . + "2016-03-02T15:05:21Z"^^ . + "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|RV knora-admin:UnknownUser" . + "RV" . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W.20160302T150521Z"^^ . + "Reise ins Heilige Land" . + . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/000000000056c287fc950w5"^^ . + . + "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" . + "V" . + "Katalogaufnahme anhand ISTC und v.d.Haegen" . + "2016-03-02T15:05:21Z"^^ . + "000000000056c287fc950w" . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/000000000056c287fc950w5.20160302T150521Z"^^ . + . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000007b4a9bf8930w2"^^ . + . + "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" . + "V" . + "Van der Haegen I: 9,14" . + "2016-03-02T15:05:20Z"^^ . + "00000000007b4a9bf8930w" . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000007b4a9bf8930w2.20160302T150520Z"^^ . + . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000003e74ee31940w2"^^ . + . + "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" . + "V" . + "Goff M165" . + "2016-03-02T15:05:20Z"^^ . + "00000000003e74ee31940w" . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000003e74ee31940w2.20160302T150520Z"^^ . + . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000019e416b940wc"^^ . + . + "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" . + "V" . + "C 3833" . + "2016-03-02T15:05:20Z"^^ . + "0000000000019e416b940w" . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000019e416b940wc.20160302T150520Z"^^ . + . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000c4c794a4940wm"^^ . + . + "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" . + "V" . + "Klebs 651.2" . + "2016-03-02T15:05:21Z"^^ . + "0000000000c4c794a4940w" . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000c4c794a4940wm.20160302T150521Z"^^ . + . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/000000000087f1e7dd940wJ"^^ . + . + "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" . + "V" . + "Schr 4799" . + "2016-03-02T15:05:21Z"^^ . + "000000000087f1e7dd940w" . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/000000000087f1e7dd940wJ.20160302T150521Z"^^ . + . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000004a1b3b17950wa"^^ . + . + "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" . + "V" . + "Schramm XXI p. 9 & 26" . + "2016-03-02T15:05:21Z"^^ . + "00000000004a1b3b17950w" . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000004a1b3b17950wa.20160302T150521Z"^^ . + . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000000d458e50950wm"^^ . + . + "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" . + "V" . + "FairMur(G) 283" . + "2016-03-02T15:05:21Z"^^ . + "00000000000d458e50950w" . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000000d458e50950wm.20160302T150521Z"^^ . + . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000d06ee189950wA"^^ . + . + "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" . + "V" . + "IBP 3556" . + "2016-03-02T15:05:21Z"^^ . + "0000000000d06ee189950w" . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000d06ee189950wA.20160302T150521Z"^^ . + . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000939834c3950w6"^^ . + . + "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" . + "V" . + "Borm 1751" . + "2016-03-02T15:05:21Z"^^ . + "0000000000939834c3950w" . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000939834c3950w6.20160302T150521Z"^^ . + . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/000000000026265567920wa"^^ . + . + "CR knora-admin:Creator|D knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" . + "V" . + "Basel UB, Sign: Aleph D III 13:1" . + "2016-03-02T15:05:20Z"^^ . + "000000000026265567920w" . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/000000000026265567920wa.20160302T150520Z"^^ . + . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000ac79fbd9920wy"^^ . + . + "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" . + "V" . + "deutsch von Otto von Diemeringen" . + "2016-03-02T15:05:20Z"^^ . + "0000000000ac79fbd9920w" . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000ac79fbd9920wy.20160302T150520Z"^^ . + . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000006fa34e13930wU"^^ . + . + "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" . + "V" . + "Rubr. mit Init. J zu Beginn" . + "2016-03-02T15:05:20Z"^^ . + "00000000006fa34e13930w" . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000006fa34e13930wU.20160302T150520Z"^^ . + . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/000000000032cda14c930w4"^^ . + . + "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" . + "V" . + "Holzschnitte nicht koloriert" . + "2016-03-02T15:05:20Z"^^ . + "000000000032cda14c930w" . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/000000000032cda14c930w4.20160302T150520Z"^^ . + . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000f5f6f485930w6"^^ . + . + "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" . + "V" . + "Besitzervermerke: Kartause, H. Zscheckenbürlin" . + "2016-03-02T15:05:20Z"^^ . + "0000000000f5f6f485930w" . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000f5f6f485930w6.20160302T150520Z"^^ . + . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000b82048bf930wL"^^ . + . + "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" . + "V" . + "Zusammengebunden mit: Die zehen Gebote ; Was und wie man beten soll und Auslegung des hlg. Pater nosters / Hans von Warmont. Strassburg, 1516" . + "2016-03-02T15:05:20Z"^^ . + "0000000000b82048bf930w" . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000b82048bf930wL.20160302T150520Z"^^ . + . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000e94fa8a0920wv"^^ . + . + "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" . + "V" . + "Extent: 1 Bd.; Dimensions: f°" . + "2016-03-02T15:05:20Z"^^ . + "0000000000e94fa8a0920w" . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000e94fa8a0920wv.20160302T150520Z"^^ . + . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/000000000063fc012e920wK"^^ . + . + "JULIAN" . + "CE" . + "1481"^^ . + "CE" . + "1481"^^ . + "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" . + "V" . + "JULIAN:1481 CE" . + "2016-03-02T15:05:20Z"^^ . + "000000000063fc012e920w" . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/000000000063fc012e920wK.20160302T150520Z"^^ . + . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000dda85bbb910wo"^^ . + . + "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" . + "V" . + "Bernhard Richel" . + "2016-03-02T15:05:20Z"^^ . + "0000000000dda85bbb910w" . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000dda85bbb910wo.20160302T150520Z"^^ . + . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000a0d2aef4910wL"^^ . + . + "CR knora-admin:Creator|V knora-admin:ProjectMember,knora-admin:KnownUser,knora-admin:UnknownUser" . + "V" . + "Basel" . + "2016-03-02T15:05:20Z"^^ . + "0000000000a0d2aef4910w" . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000a0d2aef4910wL.20160302T150520Z"^^ . + . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000d1010fd6900w5"^^ . + . + "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" . + "V" . + "Reise ins Heilige Land" . + "2016-03-02T15:05:20Z"^^ . + "0000000000d1010fd6900w" . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000d1010fd6900w5.20160302T150520Z"^^ . + . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000942b620f910wy"^^ . + . + "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" . + "V" . + "Reysen und wanderschafften durch das Gelobte Land" . + "2016-03-02T15:05:20Z"^^ . + "0000000000942b620f910w" . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000942b620f910wy.20160302T150520Z"^^ . + . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000005755b548910wq"^^ . + . + "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" . + "V" . + "Itinerarius" . + "2016-03-02T15:05:20Z"^^ . + "00000000005755b548910w" . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000005755b548910wq.20160302T150520Z"^^ . + . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000f89173afca270w7"^^ . + . + "CR knora-admin:Creator|D knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" . + "V" . + "http://aleph.unibas.ch/F/?local_base=DSV01&con_lng=GER&func=find-b&find_code=SYS&request=002610320" . + "2016-03-02T15:05:20Z"^^ . + "00000000f89173afca270w" . + "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000f89173afca270w7.20160302T150520Z"^^ . diff --git a/test_data/rdfFormatUtil/BookReiseInsHeiligeLand.trig b/test_data/rdfFormatUtil/BookReiseInsHeiligeLand.trig new file mode 100644 index 0000000000..a6dccb1c81 --- /dev/null +++ b/test_data/rdfFormatUtil/BookReiseInsHeiligeLand.trig @@ -0,0 +1,287 @@ +@prefix incunabula: . +@prefix knora-api: . +@prefix rdf: . +@prefix rdfs: . +@prefix xsd: . + + { + + rdf:type incunabula:book ; + incunabula:book_comment ; + incunabula:citation ; + incunabula:citation ; + incunabula:citation ; + incunabula:citation ; + incunabula:citation ; + incunabula:citation ; + incunabula:citation ; + incunabula:citation ; + incunabula:citation ; + incunabula:location ; + incunabula:note ; + incunabula:note ; + incunabula:note ; + incunabula:note ; + incunabula:note ; + incunabula:physical_desc ; + incunabula:pubdate ; + incunabula:publisher ; + incunabula:publoc ; + incunabula:title ; + incunabula:title ; + incunabula:title ; + incunabula:url ; + knora-api:arkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W"^^xsd:anyURI ; + knora-api:attachedToProject ; + knora-api:attachedToUser ; + knora-api:creationDate "2016-03-02T15:05:21Z"^^xsd:dateTimeStamp ; + knora-api:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|RV knora-admin:UnknownUser" ; + knora-api:userHasPermission "RV" ; + knora-api:versionArkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W.20160302T150521Z"^^xsd:anyURI ; + rdfs:label "Reise ins Heilige Land" . + + rdf:type knora-api:TextValue ; + knora-api:arkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/000000000056c287fc950w5"^^xsd:anyURI ; + knora-api:attachedToUser ; + knora-api:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" ; + knora-api:userHasPermission "V" ; + knora-api:valueAsString "Katalogaufnahme anhand ISTC und v.d.Haegen" ; + knora-api:valueCreationDate "2016-03-02T15:05:21Z"^^xsd:dateTimeStamp ; + knora-api:valueHasUUID "000000000056c287fc950w" ; + knora-api:versionArkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/000000000056c287fc950w5.20160302T150521Z"^^xsd:anyURI . + + rdf:type knora-api:TextValue ; + knora-api:arkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000007b4a9bf8930w2"^^xsd:anyURI ; + knora-api:attachedToUser ; + knora-api:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" ; + knora-api:userHasPermission "V" ; + knora-api:valueAsString "Van der Haegen I: 9,14" ; + knora-api:valueCreationDate "2016-03-02T15:05:20Z"^^xsd:dateTimeStamp ; + knora-api:valueHasUUID "00000000007b4a9bf8930w" ; + knora-api:versionArkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000007b4a9bf8930w2.20160302T150520Z"^^xsd:anyURI . + + rdf:type knora-api:TextValue ; + knora-api:arkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000003e74ee31940w2"^^xsd:anyURI ; + knora-api:attachedToUser ; + knora-api:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" ; + knora-api:userHasPermission "V" ; + knora-api:valueAsString "Goff M165" ; + knora-api:valueCreationDate "2016-03-02T15:05:20Z"^^xsd:dateTimeStamp ; + knora-api:valueHasUUID "00000000003e74ee31940w" ; + knora-api:versionArkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000003e74ee31940w2.20160302T150520Z"^^xsd:anyURI . + + rdf:type knora-api:TextValue ; + knora-api:arkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000019e416b940wc"^^xsd:anyURI ; + knora-api:attachedToUser ; + knora-api:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" ; + knora-api:userHasPermission "V" ; + knora-api:valueAsString "C 3833" ; + knora-api:valueCreationDate "2016-03-02T15:05:20Z"^^xsd:dateTimeStamp ; + knora-api:valueHasUUID "0000000000019e416b940w" ; + knora-api:versionArkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000019e416b940wc.20160302T150520Z"^^xsd:anyURI . + + rdf:type knora-api:TextValue ; + knora-api:arkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000c4c794a4940wm"^^xsd:anyURI ; + knora-api:attachedToUser ; + knora-api:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" ; + knora-api:userHasPermission "V" ; + knora-api:valueAsString "Klebs 651.2" ; + knora-api:valueCreationDate "2016-03-02T15:05:21Z"^^xsd:dateTimeStamp ; + knora-api:valueHasUUID "0000000000c4c794a4940w" ; + knora-api:versionArkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000c4c794a4940wm.20160302T150521Z"^^xsd:anyURI . + + rdf:type knora-api:TextValue ; + knora-api:arkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/000000000087f1e7dd940wJ"^^xsd:anyURI ; + knora-api:attachedToUser ; + knora-api:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" ; + knora-api:userHasPermission "V" ; + knora-api:valueAsString "Schr 4799" ; + knora-api:valueCreationDate "2016-03-02T15:05:21Z"^^xsd:dateTimeStamp ; + knora-api:valueHasUUID "000000000087f1e7dd940w" ; + knora-api:versionArkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/000000000087f1e7dd940wJ.20160302T150521Z"^^xsd:anyURI . + + rdf:type knora-api:TextValue ; + knora-api:arkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000004a1b3b17950wa"^^xsd:anyURI ; + knora-api:attachedToUser ; + knora-api:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" ; + knora-api:userHasPermission "V" ; + knora-api:valueAsString "Schramm XXI p. 9 & 26" ; + knora-api:valueCreationDate "2016-03-02T15:05:21Z"^^xsd:dateTimeStamp ; + knora-api:valueHasUUID "00000000004a1b3b17950w" ; + knora-api:versionArkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000004a1b3b17950wa.20160302T150521Z"^^xsd:anyURI . + + rdf:type knora-api:TextValue ; + knora-api:arkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000000d458e50950wm"^^xsd:anyURI ; + knora-api:attachedToUser ; + knora-api:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" ; + knora-api:userHasPermission "V" ; + knora-api:valueAsString "FairMur(G) 283" ; + knora-api:valueCreationDate "2016-03-02T15:05:21Z"^^xsd:dateTimeStamp ; + knora-api:valueHasUUID "00000000000d458e50950w" ; + knora-api:versionArkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000000d458e50950wm.20160302T150521Z"^^xsd:anyURI . + + rdf:type knora-api:TextValue ; + knora-api:arkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000d06ee189950wA"^^xsd:anyURI ; + knora-api:attachedToUser ; + knora-api:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" ; + knora-api:userHasPermission "V" ; + knora-api:valueAsString "IBP 3556" ; + knora-api:valueCreationDate "2016-03-02T15:05:21Z"^^xsd:dateTimeStamp ; + knora-api:valueHasUUID "0000000000d06ee189950w" ; + knora-api:versionArkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000d06ee189950wA.20160302T150521Z"^^xsd:anyURI . + + rdf:type knora-api:TextValue ; + knora-api:arkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000939834c3950w6"^^xsd:anyURI ; + knora-api:attachedToUser ; + knora-api:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" ; + knora-api:userHasPermission "V" ; + knora-api:valueAsString "Borm 1751" ; + knora-api:valueCreationDate "2016-03-02T15:05:21Z"^^xsd:dateTimeStamp ; + knora-api:valueHasUUID "0000000000939834c3950w" ; + knora-api:versionArkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000939834c3950w6.20160302T150521Z"^^xsd:anyURI . + + rdf:type knora-api:TextValue ; + knora-api:arkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/000000000026265567920wa"^^xsd:anyURI ; + knora-api:attachedToUser ; + knora-api:hasPermissions "CR knora-admin:Creator|D knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" ; + knora-api:userHasPermission "V" ; + knora-api:valueAsString "Basel UB, Sign: Aleph D III 13:1" ; + knora-api:valueCreationDate "2016-03-02T15:05:20Z"^^xsd:dateTimeStamp ; + knora-api:valueHasUUID "000000000026265567920w" ; + knora-api:versionArkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/000000000026265567920wa.20160302T150520Z"^^xsd:anyURI . + + rdf:type knora-api:TextValue ; + knora-api:arkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000ac79fbd9920wy"^^xsd:anyURI ; + knora-api:attachedToUser ; + knora-api:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" ; + knora-api:userHasPermission "V" ; + knora-api:valueAsString "deutsch von Otto von Diemeringen" ; + knora-api:valueCreationDate "2016-03-02T15:05:20Z"^^xsd:dateTimeStamp ; + knora-api:valueHasUUID "0000000000ac79fbd9920w" ; + knora-api:versionArkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000ac79fbd9920wy.20160302T150520Z"^^xsd:anyURI . + + rdf:type knora-api:TextValue ; + knora-api:arkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000006fa34e13930wU"^^xsd:anyURI ; + knora-api:attachedToUser ; + knora-api:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" ; + knora-api:userHasPermission "V" ; + knora-api:valueAsString "Rubr. mit Init. J zu Beginn" ; + knora-api:valueCreationDate "2016-03-02T15:05:20Z"^^xsd:dateTimeStamp ; + knora-api:valueHasUUID "00000000006fa34e13930w" ; + knora-api:versionArkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000006fa34e13930wU.20160302T150520Z"^^xsd:anyURI . + + rdf:type knora-api:TextValue ; + knora-api:arkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/000000000032cda14c930w4"^^xsd:anyURI ; + knora-api:attachedToUser ; + knora-api:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" ; + knora-api:userHasPermission "V" ; + knora-api:valueAsString "Holzschnitte nicht koloriert" ; + knora-api:valueCreationDate "2016-03-02T15:05:20Z"^^xsd:dateTimeStamp ; + knora-api:valueHasUUID "000000000032cda14c930w" ; + knora-api:versionArkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/000000000032cda14c930w4.20160302T150520Z"^^xsd:anyURI . + + rdf:type knora-api:TextValue ; + knora-api:arkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000f5f6f485930w6"^^xsd:anyURI ; + knora-api:attachedToUser ; + knora-api:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" ; + knora-api:userHasPermission "V" ; + knora-api:valueAsString "Besitzervermerke: Kartause, H. Zscheckenbürlin" ; + knora-api:valueCreationDate "2016-03-02T15:05:20Z"^^xsd:dateTimeStamp ; + knora-api:valueHasUUID "0000000000f5f6f485930w" ; + knora-api:versionArkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000f5f6f485930w6.20160302T150520Z"^^xsd:anyURI . + + rdf:type knora-api:TextValue ; + knora-api:arkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000b82048bf930wL"^^xsd:anyURI ; + knora-api:attachedToUser ; + knora-api:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" ; + knora-api:userHasPermission "V" ; + knora-api:valueAsString "Zusammengebunden mit: Die zehen Gebote ; Was und wie man beten soll und Auslegung des hlg. Pater nosters / Hans von Warmont. Strassburg, 1516" ; + knora-api:valueCreationDate "2016-03-02T15:05:20Z"^^xsd:dateTimeStamp ; + knora-api:valueHasUUID "0000000000b82048bf930w" ; + knora-api:versionArkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000b82048bf930wL.20160302T150520Z"^^xsd:anyURI . + + rdf:type knora-api:TextValue ; + knora-api:arkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000e94fa8a0920wv"^^xsd:anyURI ; + knora-api:attachedToUser ; + knora-api:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" ; + knora-api:userHasPermission "V" ; + knora-api:valueAsString "Extent: 1 Bd.; Dimensions: f°" ; + knora-api:valueCreationDate "2016-03-02T15:05:20Z"^^xsd:dateTimeStamp ; + knora-api:valueHasUUID "0000000000e94fa8a0920w" ; + knora-api:versionArkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000e94fa8a0920wv.20160302T150520Z"^^xsd:anyURI . + + rdf:type knora-api:DateValue ; + knora-api:arkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/000000000063fc012e920wK"^^xsd:anyURI ; + knora-api:attachedToUser ; + knora-api:dateValueHasCalendar "JULIAN" ; + knora-api:dateValueHasEndEra "CE" ; + knora-api:dateValueHasEndYear 1481 ; + knora-api:dateValueHasStartEra "CE" ; + knora-api:dateValueHasStartYear 1481 ; + knora-api:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" ; + knora-api:userHasPermission "V" ; + knora-api:valueAsString "JULIAN:1481 CE" ; + knora-api:valueCreationDate "2016-03-02T15:05:20Z"^^xsd:dateTimeStamp ; + knora-api:valueHasUUID "000000000063fc012e920w" ; + knora-api:versionArkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/000000000063fc012e920wK.20160302T150520Z"^^xsd:anyURI . + + rdf:type knora-api:TextValue ; + knora-api:arkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000dda85bbb910wo"^^xsd:anyURI ; + knora-api:attachedToUser ; + knora-api:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" ; + knora-api:userHasPermission "V" ; + knora-api:valueAsString "Bernhard Richel" ; + knora-api:valueCreationDate "2016-03-02T15:05:20Z"^^xsd:dateTimeStamp ; + knora-api:valueHasUUID "0000000000dda85bbb910w" ; + knora-api:versionArkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000dda85bbb910wo.20160302T150520Z"^^xsd:anyURI . + + rdf:type knora-api:TextValue ; + knora-api:arkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000a0d2aef4910wL"^^xsd:anyURI ; + knora-api:attachedToUser ; + knora-api:hasPermissions "CR knora-admin:Creator|V knora-admin:ProjectMember,knora-admin:KnownUser,knora-admin:UnknownUser" ; + knora-api:userHasPermission "V" ; + knora-api:valueAsString "Basel" ; + knora-api:valueCreationDate "2016-03-02T15:05:20Z"^^xsd:dateTimeStamp ; + knora-api:valueHasUUID "0000000000a0d2aef4910w" ; + knora-api:versionArkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000a0d2aef4910wL.20160302T150520Z"^^xsd:anyURI . + + rdf:type knora-api:TextValue ; + knora-api:arkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000d1010fd6900w5"^^xsd:anyURI ; + knora-api:attachedToUser ; + knora-api:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" ; + knora-api:userHasPermission "V" ; + knora-api:valueAsString "Reise ins Heilige Land" ; + knora-api:valueCreationDate "2016-03-02T15:05:20Z"^^xsd:dateTimeStamp ; + knora-api:valueHasUUID "0000000000d1010fd6900w" ; + knora-api:versionArkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000d1010fd6900w5.20160302T150520Z"^^xsd:anyURI . + + rdf:type knora-api:TextValue ; + knora-api:arkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000942b620f910wy"^^xsd:anyURI ; + knora-api:attachedToUser ; + knora-api:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" ; + knora-api:userHasPermission "V" ; + knora-api:valueAsString "Reysen und wanderschafften durch das Gelobte Land" ; + knora-api:valueCreationDate "2016-03-02T15:05:20Z"^^xsd:dateTimeStamp ; + knora-api:valueHasUUID "0000000000942b620f910w" ; + knora-api:versionArkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/0000000000942b620f910wy.20160302T150520Z"^^xsd:anyURI . + + rdf:type knora-api:TextValue ; + knora-api:arkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000005755b548910wq"^^xsd:anyURI ; + knora-api:attachedToUser ; + knora-api:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" ; + knora-api:userHasPermission "V" ; + knora-api:valueAsString "Itinerarius" ; + knora-api:valueCreationDate "2016-03-02T15:05:20Z"^^xsd:dateTimeStamp ; + knora-api:valueHasUUID "00000000005755b548910w" ; + knora-api:versionArkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000005755b548910wq.20160302T150520Z"^^xsd:anyURI . + + rdf:type knora-api:TextValue ; + knora-api:arkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000f89173afca270w7"^^xsd:anyURI ; + knora-api:attachedToUser ; + knora-api:hasPermissions "CR knora-admin:Creator|D knora-admin:ProjectMember|V knora-admin:KnownUser,knora-admin:UnknownUser" ; + knora-api:userHasPermission "V" ; + knora-api:valueAsString "http://aleph.unibas.ch/F/?local_base=DSV01&con_lng=GER&func=find-b&find_code=SYS&request=002610320" ; + knora-api:valueCreationDate "2016-03-02T15:05:20Z"^^xsd:dateTimeStamp ; + knora-api:valueHasUUID "00000000f89173afca270w" ; + knora-api:versionArkUrl "http://0.0.0.0:3336/ark:/72163/1/0803/2a6221216701W/00000000f89173afca270w7.20160302T150520Z"^^xsd:anyURI . +} diff --git a/webapi/src/main/scala/org/knora/webapi/RdfMediaTypes.scala b/webapi/src/main/scala/org/knora/webapi/RdfMediaTypes.scala index 6f56ea3640..24457bdd58 100644 --- a/webapi/src/main/scala/org/knora/webapi/RdfMediaTypes.scala +++ b/webapi/src/main/scala/org/knora/webapi/RdfMediaTypes.scala @@ -55,6 +55,13 @@ object RdfMediaTypes { fileExtensions = List("trig") ) + val `application/n-quads`: MediaType.WithFixedCharset = MediaType.customWithFixedCharset( + mainType = "application", + subType = "n-quads", + charset = HttpCharsets.`UTF-8`, + fileExtensions = List("nq") + ) + /** * A map of MIME types (strings) to supported RDF media types. */ @@ -63,7 +70,8 @@ object RdfMediaTypes { `application/ld+json`, `text/turtle`, `application/trig`, - `application/rdf+xml` + `application/rdf+xml`, + `application/n-quads` ).map { mediaType => mediaType.toString -> mediaType }.toMap diff --git a/webapi/src/main/scala/org/knora/webapi/messages/store/triplestoremessages/TriplestoreMessages.scala b/webapi/src/main/scala/org/knora/webapi/messages/store/triplestoremessages/TriplestoreMessages.scala index 7b59d40343..a841e5180f 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/store/triplestoremessages/TriplestoreMessages.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/store/triplestoremessages/TriplestoreMessages.scala @@ -78,16 +78,18 @@ case class SparqlConstructRequest(sparql: String, /** * Represents a SPARQL CONSTRUCT query to be sent to the triplestore. The triplestore's will be - * written to the specified file in Trig format. A successful response message will be a [[FileWrittenResponse]]. + * written to the specified file in a quad format. A successful response message will be a [[FileWrittenResponse]]. * * @param sparql the SPARQL string. * @param graphIri the named graph IRI to be used in the TriG file. * @param outputFile the file to be written. + * @param outputFormat the output file format. * @param featureFactoryConfig the feature factory configuration. */ case class SparqlConstructFileRequest(sparql: String, graphIri: IRI, outputFile: File, + outputFormat: QuadFormat, featureFactoryConfig: FeatureFactoryConfig) extends TriplestoreRequest /** @@ -212,15 +214,17 @@ object SparqlExtendedConstructResponse { case class SparqlExtendedConstructResponse(statements: Map[SubjectV2, SparqlExtendedConstructResponse.ConstructPredicateObjects]) /** - * Requests a named graph, which will be written to the specified file in Trig format. A successful response + * Requests a named graph, which will be written to the specified file. A successful response * will be a [[FileWrittenResponse]]. * * @param graphIri the IRI of the named graph. * @param outputFile the destination file. + * @param outputFormat the output file format. * @param featureFactoryConfig the feature factory configuration. */ case class NamedGraphFileRequest(graphIri: IRI, outputFile: File, + outputFormat: QuadFormat, featureFactoryConfig: FeatureFactoryConfig) extends TriplestoreRequest /** @@ -343,7 +347,7 @@ case class CheckTriplestoreResponse(triplestoreStatus: TriplestoreStatus, msg: S case class UpdateRepositoryRequest() extends TriplestoreRequest /** - * Requests that the repository is downloaded to a TriG file. A successful response will be a [[FileWrittenResponse]]. + * Requests that the repository is downloaded to an N-Quads file. A successful response will be a [[FileWrittenResponse]]. * * @param outputFile the output file. * @param featureFactoryConfig the feature factory configuration. @@ -356,7 +360,7 @@ case class DownloadRepositoryRequest(outputFile: File, featureFactoryConfig: Fea case class FileWrittenResponse() /** - * Requests that repository content is uploaded from a TriG file. A successful response will be a + * Requests that repository content is uploaded from an N-Quads. A successful response will be a * [[RepositoryUploadedResponse]]. * * @param inputFile a TriG file containing the content to be uploaded to the repository. diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/JsonLDUtil.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/JsonLDUtil.scala index eaab17405e..ee2baa1a90 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/JsonLDUtil.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/JsonLDUtil.scala @@ -1698,9 +1698,8 @@ object JsonLDUtil { } case blankNode: BlankNode => - // It's a blank node. It should be possible to inline it. If not, the input model is invalid; - // return an error. - inlineResource(throw InvalidRdfException(s"Blank node ${blankNode.id} was not found or is referenced in more than one place")) + // It's a blank node. It should be possible to inline it. If not, return an empty blank node. + inlineResource(JsonLDObject(Map.empty)) } } } diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/RdfFeatureFactory.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/RdfFeatureFactory.scala index cfcf92c707..87d7c8708e 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/RdfFeatureFactory.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/RdfFeatureFactory.scala @@ -37,7 +37,7 @@ object RdfFeatureFactory extends FeatureFactory { // Jena singletons. private val jenaNodeFactory = new JenaNodeFactory private val jenaModelFactory = new JenaModelFactory(jenaNodeFactory) - private val jenaFormatUtil = new JenaFormatUtil(jenaModelFactory) + private val jenaFormatUtil = new JenaFormatUtil(modelFactory = jenaModelFactory, nodeFactory = jenaNodeFactory) private var jenaShaclValidator: Option[JenaShaclValidator] = None // RDF4J singletons. diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/RdfFormatUtil.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/RdfFormatUtil.scala index d3a280562a..1565f0572b 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/RdfFormatUtil.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/RdfFormatUtil.scala @@ -31,11 +31,6 @@ import scala.util.{Failure, Success, Try} * A trait for supported RDF formats. */ sealed trait RdfFormat { - /** - * `true` if this format supports named graphs. - */ - val supportsNamedGraphs: Boolean - /** * The [[MediaType]] that represents this format. */ @@ -45,7 +40,17 @@ sealed trait RdfFormat { /** * A trait for formats other than JSON-LD. */ -sealed trait NonJsonLD extends RdfFormat +sealed trait NonJsonLD extends RdfFormat { + /** + * `true` if this format supports pretty-printing. + */ + def supportsPrettyPrinting: Boolean +} + +/** + * Represents a format that supports quads. + */ +sealed trait QuadFormat extends NonJsonLD object RdfFormat { /** @@ -60,6 +65,7 @@ object RdfFormat { case RdfMediaTypes.`text/turtle` => Turtle case RdfMediaTypes.`application/trig` => TriG case RdfMediaTypes.`application/rdf+xml` => RdfXml + case RdfMediaTypes.`application/n-quads` => NQuads case other => throw InvalidRdfException(s"Unsupported RDF media type: $other") } } @@ -72,9 +78,6 @@ case object JsonLD extends RdfFormat { override def toString: String = "JSON-LD" override val toMediaType: MediaType = RdfMediaTypes.`application/ld+json` - - // We don't support named graphs in JSON-LD. - override val supportsNamedGraphs: Boolean = false } /** @@ -85,18 +88,18 @@ case object Turtle extends NonJsonLD { override val toMediaType: MediaType = RdfMediaTypes.`text/turtle` - override val supportsNamedGraphs: Boolean = false + override val supportsPrettyPrinting: Boolean = true } /** * Represents TriG format. */ -case object TriG extends NonJsonLD { +case object TriG extends QuadFormat { override def toString: String = "TriG" override val toMediaType: MediaType = RdfMediaTypes.`application/trig` - override val supportsNamedGraphs: Boolean = true + override val supportsPrettyPrinting: Boolean = true } /** @@ -107,7 +110,18 @@ case object RdfXml extends NonJsonLD { override val toMediaType: MediaType = RdfMediaTypes.`application/rdf+xml` - override val supportsNamedGraphs: Boolean = false + override val supportsPrettyPrinting: Boolean = true +} + +/** + * Represents N-Quads format. + */ +case object NQuads extends QuadFormat { + override def toString: String = "N-Quads" + + override val toMediaType: MediaType = RdfMediaTypes.`application/n-quads` + + override val supportsPrettyPrinting: Boolean = false } /** @@ -237,8 +251,11 @@ trait RdfFormatUtil { case nonJsonLD: NonJsonLD => // Some formats can't represent named graphs. - if (rdfModel.getContexts.nonEmpty && !nonJsonLD.supportsNamedGraphs) { - throw BadRequestException(s"Named graphs are not supported in $rdfFormat") + if (rdfModel.getContexts.nonEmpty) { + nonJsonLD match { + case _: QuadFormat => () + case _ => throw BadRequestException(s"Named graphs are not supported in $rdfFormat") + } } // Use an implementation-specific function to convert to formats other than JSON-LD. @@ -258,16 +275,10 @@ trait RdfFormatUtil { * @return a [[RdfModel]] representing the contents of the file. */ def fileToRdfModel(file: File, rdfFormat: NonJsonLD): RdfModel = { - var maybeFileInputStream: Option[InputStream] = None - - val modelTry = Try { - val fileInputStream = new BufferedInputStream(new FileInputStream(file)) - maybeFileInputStream = Some(fileInputStream) - inputStreamToRdfModel(inputStream = fileInputStream, rdfFormat = rdfFormat) - } - - maybeFileInputStream.foreach(_.close()) - modelTry.get + inputStreamToRdfModel( + inputStream = new BufferedInputStream(new FileInputStream(file)), + rdfFormat = rdfFormat + ) } /** @@ -278,29 +289,17 @@ trait RdfFormatUtil { * @param rdfFormat the file format. */ def rdfModelToFile(rdfModel: RdfModel, file: File, rdfFormat: NonJsonLD): Unit = { - var maybeFileOutputStream: Option[OutputStream] = None - - val writeTry: Try[Unit] = Try { - val fileOutputStream = new BufferedOutputStream(new FileOutputStream(file)) - maybeFileOutputStream = Some(fileOutputStream) - - rdfModelToOutputStream( - rdfModel = rdfModel, - outputStream = fileOutputStream, - rdfFormat = rdfFormat - ) - } - - maybeFileOutputStream.foreach(_.close()) - - writeTry match { - case Success(()) => () - case Failure(ex) => throw ex - } + rdfModelToOutputStream( + rdfModel = rdfModel, + outputStream = new BufferedOutputStream(new FileOutputStream(file)), + rdfFormat = rdfFormat + ) } /** - * Parses RDF input, processing it with an [[RdfStreamProcessor]]. + * Parses RDF input, processing it with an [[RdfStreamProcessor]]. If the source is an input + * stream, this method closes it before returning. The caller must close any output stream + * used by the [[RdfStreamProcessor]]. * * @param rdfSource the input source from which the RDF data should be read. * @param rdfFormat the input format. @@ -311,7 +310,8 @@ trait RdfFormatUtil { rdfStreamProcessor: RdfStreamProcessor): Unit /** - * Reads RDF data from an [[InputStream]] and returns it as an [[RdfModel]]. + * Reads RDF data from an [[InputStream]] and returns it as an [[RdfModel]]. Closes the input stream + * before returning. * * @param inputStream the input stream. * @param rdfFormat the data format. @@ -320,7 +320,8 @@ trait RdfFormatUtil { def inputStreamToRdfModel(inputStream: InputStream, rdfFormat: NonJsonLD): RdfModel /** - * Formats an [[RdfModel]], writing the output to an [[OutputStream]]. + * Formats an [[RdfModel]], writing the output to an [[OutputStream]]. Closes the output stream before + * returning. * * @param rdfModel the model to be written. * @param outputStream the output stream. @@ -337,11 +338,91 @@ trait RdfFormatUtil { */ def makeFormattingStreamProcessor(outputStream: OutputStream, rdfFormat: NonJsonLD): RdfStreamProcessor + /** + * Adds a context IRI to RDF statements. + * + * @param graphIri the IRI of the named graph. + * @param formattingStreamProcessor an [[RdfStreamProcessor]] for writing the result. + */ + private class ContextAddingProcessor(graphIri: IRI, + formattingStreamProcessor: RdfStreamProcessor) extends RdfStreamProcessor { + private val nodeFactory: RdfNodeFactory = getRdfNodeFactory + + override def start(): Unit = formattingStreamProcessor.start() + + override def processNamespace(prefix: String, namespace: IRI): Unit = { + formattingStreamProcessor.processNamespace(prefix = prefix, namespace = namespace) + } + + override def processStatement(statement: Statement): Unit = { + val outputStatement = nodeFactory.makeStatement( + subj = statement.subj, + pred = statement.pred, + obj = statement.obj, + context = Some(graphIri) + ) + + formattingStreamProcessor.processStatement(outputStatement) + } + + override def finish(): Unit = formattingStreamProcessor.finish() + } + + /** + * Reads RDF data in Turtle format from an [[RdfSource]], adds a named graph IRI to each statement, + * and writes the result to a file in a format that supports quads. If the source is an input + * stream, this method closes it before returning. + * + * @param rdfSource the RDF data source. + * @param graphIri the named graph IRI to be added. + * @param outputFile the output file. + * @param outputFormat the output file format. + */ + def turtleToQuadsFile(rdfSource: RdfSource, + graphIri: IRI, + outputFile: File, + outputFormat: QuadFormat): Unit = { + var maybeBufferedFileOutputStream: Option[BufferedOutputStream] = None + + val processingTry: Try[Unit] = Try { + val bufferedFileOutputStream = new BufferedOutputStream(new FileOutputStream(outputFile)) + maybeBufferedFileOutputStream = Some(bufferedFileOutputStream) + + val formattingStreamProcessor: RdfStreamProcessor = makeFormattingStreamProcessor( + outputStream = bufferedFileOutputStream, + rdfFormat = outputFormat + ) + + val contextAddingProcessor = new ContextAddingProcessor( + graphIri = graphIri, + formattingStreamProcessor = formattingStreamProcessor + ) + + parseWithStreamProcessor( + rdfSource = rdfSource, + rdfFormat = Turtle, + rdfStreamProcessor = contextAddingProcessor + ) + } + + maybeBufferedFileOutputStream.foreach(_.close) + + processingTry match { + case Success(_) => () + case Failure(ex) => throw ex + } + } + /** * Returns an [[RdfModelFactory]] with the same underlying implementation as this [[RdfFormatUtil]]. */ def getRdfModelFactory: RdfModelFactory + /** + * Returns an [[RdfNodeFactory]] with the same underlying implementation as this [[RdfFormatUtil]]. + */ + def getRdfNodeFactory: RdfNodeFactory + /** * Parses RDF in a format other than JSON-LD to an [[RdfModel]]. * diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/jenaimpl/JenaFormatUtil.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/jenaimpl/JenaFormatUtil.scala index fc4e244ca0..1779730288 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/jenaimpl/JenaFormatUtil.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/jenaimpl/JenaFormatUtil.scala @@ -26,6 +26,8 @@ import org.knora.webapi.IRI import org.knora.webapi.feature.Feature import org.knora.webapi.messages.util.rdf._ +import scala.util.{Failure, Success, Try} + /** * Wraps an [[RdfStreamProcessor]] in a [[jena.riot.system.StreamRDF]]. */ @@ -74,14 +76,18 @@ class StreamRDFAsStreamProcessor(streamRDF: jena.riot.system.StreamRDF) extends /** * An implementation of [[RdfFormatUtil]] that uses the Jena API. */ -class JenaFormatUtil(private val modelFactory: JenaModelFactory) extends RdfFormatUtil with Feature { +class JenaFormatUtil(private val modelFactory: JenaModelFactory, + private val nodeFactory: JenaNodeFactory) extends RdfFormatUtil with Feature { override def getRdfModelFactory: RdfModelFactory = modelFactory + override def getRdfNodeFactory: RdfNodeFactory = nodeFactory + private def rdfFormatToJenaParsingLang(rdfFormat: NonJsonLD): jena.riot.Lang = { rdfFormat match { case Turtle => jena.riot.RDFLanguages.TURTLE case TriG => jena.riot.RDFLanguages.TRIG case RdfXml => jena.riot.RDFLanguages.RDFXML + case NQuads => jena.riot.RDFLanguages.NQUADS } } @@ -130,6 +136,9 @@ class JenaFormatUtil(private val modelFactory: JenaModelFactory) extends RdfForm } jena.riot.RDFDataMgr.write(stringWriter, datasetGraph, jenaRdfFormat) + + case NQuads => + jena.riot.RDFDataMgr.write(stringWriter, datasetGraph, jena.riot.RDFFormat.NQUADS) } stringWriter.toString @@ -150,22 +159,39 @@ class JenaFormatUtil(private val modelFactory: JenaModelFactory) extends RdfForm case RdfInputStreamSource(inputStream) => parser.source(inputStream) } - // Add the other configuration and run the parser. - parser.lang(rdfFormatToJenaParsingLang(rdfFormat)) - .errorHandler(jena.riot.system.ErrorHandlerFactory.errorHandlerStrictNoLogging) - .parse(streamRDF) + val parseTry: Try[Unit] = Try { + // Add the other configuration and run the parser. + parser.lang(rdfFormatToJenaParsingLang(rdfFormat)) + .errorHandler(jena.riot.system.ErrorHandlerFactory.errorHandlerStrictNoLogging) + .parse(streamRDF) + } + + rdfSource match { + case RdfInputStreamSource(inputStream) => inputStream.close() + case _ => () + } + + parseTry match { + case Success(_) => () + case Failure(ex) => throw ex + } } override def inputStreamToRdfModel(inputStream: InputStream, rdfFormat: NonJsonLD): RdfModel = { - val model: JenaModel = modelFactory.makeEmptyModel + val parseTry: Try[RdfModel] = Try { + val model: JenaModel = modelFactory.makeEmptyModel - jena.riot.RDFDataMgr.read( - model.getDataset.asDatasetGraph, - inputStream, - rdfFormatToJenaParsingLang(rdfFormat) - ) + jena.riot.RDFDataMgr.read( + model.getDataset.asDatasetGraph, + inputStream, + rdfFormatToJenaParsingLang(rdfFormat) + ) - model + model + } + + inputStream.close() + parseTry.get } override def makeFormattingStreamProcessor(outputStream: OutputStream, @@ -183,17 +209,29 @@ class JenaFormatUtil(private val modelFactory: JenaModelFactory) extends RdfForm override def rdfModelToOutputStream(rdfModel: RdfModel, outputStream: OutputStream, rdfFormat: NonJsonLD): Unit = { import JenaConversions._ - val datasetGraph: jena.sparql.core.DatasetGraph = rdfModel.asJenaDataset.asDatasetGraph + val formatTry: Try[Unit] = Try { + val datasetGraph: jena.sparql.core.DatasetGraph = rdfModel.asJenaDataset.asDatasetGraph - rdfFormat match { - case Turtle => - jena.riot.RDFDataMgr.write(outputStream, datasetGraph.getDefaultGraph, jena.riot.RDFFormat.TURTLE_FLAT) + rdfFormat match { + case Turtle => + jena.riot.RDFDataMgr.write(outputStream, datasetGraph.getDefaultGraph, jena.riot.RDFFormat.TURTLE_FLAT) - case RdfXml => - jena.riot.RDFDataMgr.write(outputStream, datasetGraph.getDefaultGraph, jena.riot.RDFFormat.RDFXML_PLAIN) + case RdfXml => + jena.riot.RDFDataMgr.write(outputStream, datasetGraph.getDefaultGraph, jena.riot.RDFFormat.RDFXML_PLAIN) - case TriG => - jena.riot.RDFDataMgr.write(outputStream, datasetGraph, jena.riot.RDFFormat.TRIG_FLAT) + case TriG => + jena.riot.RDFDataMgr.write(outputStream, datasetGraph, jena.riot.RDFFormat.TRIG_FLAT) + + case NQuads => + jena.riot.RDFDataMgr.write(outputStream, datasetGraph, jena.riot.RDFFormat.NQUADS) + } + } + + outputStream.close() + + formatTry match { + case Success(_) => () + case Failure(ex) => throw ex } } } diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/rdf4jimpl/RDF4JFormatUtil.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/rdf4jimpl/RDF4JFormatUtil.scala index 96d3ea0f60..b81fd036a6 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/rdf4jimpl/RDF4JFormatUtil.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/rdf4jimpl/RDF4JFormatUtil.scala @@ -26,6 +26,8 @@ import org.knora.webapi.IRI import org.knora.webapi.feature.Feature import org.knora.webapi.messages.util.rdf._ +import scala.util.{Failure, Success, Try} + /** * Wraps an [[RdfStreamProcessor]] in an [[rdf4j.rio.RDFHandler]]. */ @@ -72,11 +74,14 @@ class RDF4JFormatUtil(private val modelFactory: RDF4JModelFactory, private val nodeFactory: RDF4JNodeFactory) extends RdfFormatUtil with Feature { override def getRdfModelFactory: RdfModelFactory = modelFactory + override def getRdfNodeFactory: RdfNodeFactory = nodeFactory + private def rdfFormatToRDF4JFormat(rdfFormat: NonJsonLD): rdf4j.rio.RDFFormat = { rdfFormat match { case Turtle => rdf4j.rio.RDFFormat.TURTLE case TriG => rdf4j.rio.RDFFormat.TRIG case RdfXml => rdf4j.rio.RDFFormat.RDFXML + case NQuads => rdf4j.rio.RDFFormat.NQUADS } } @@ -95,15 +100,9 @@ class RDF4JFormatUtil(private val modelFactory: RDF4JModelFactory, import RDF4JConversions._ val stringWriter = new StringWriter + val rdfWriter: rdf4j.rio.RDFWriter = rdf4j.rio.Rio.createWriter(rdfFormatToRDF4JFormat(rdfFormat), stringWriter) - val rdfWriter: rdf4j.rio.RDFWriter = rdfFormat match { - case Turtle => rdf4j.rio.Rio.createWriter(rdf4j.rio.RDFFormat.TURTLE, stringWriter) - case TriG => rdf4j.rio.Rio.createWriter(rdf4j.rio.RDFFormat.TRIG, stringWriter) - case RdfXml => rdf4j.rio.Rio.createWriter(rdf4j.rio.RDFFormat.RDFXML, stringWriter) - } - - // Configure the RDFWriter. - if (prettyPrint) { + if (prettyPrint && rdfFormat.supportsPrettyPrinting) { rdfWriter.getWriterConfig. set[java.lang.Boolean](rdf4j.rio.helpers.BasicWriterSettings.INLINE_BLANK_NODES, true). set[java.lang.Boolean](rdf4j.rio.helpers.BasicWriterSettings.PRETTY_PRINT, prettyPrint) @@ -123,24 +122,41 @@ class RDF4JFormatUtil(private val modelFactory: RDF4JModelFactory, // Wrap the RdfStreamProcessor in a StreamProcessorAsRDFHandler and set it as the parser's RDFHandler. parser.setRDFHandler(new StreamProcessorAsRDFHandler(rdfStreamProcessor)) - // Parse from the input source. + val parseTry: Try[Unit] = Try { + // Parse from the input source. + rdfSource match { + case RdfStringSource(rdfStr) => parser.parse(new StringReader(rdfStr), "") + case RdfInputStreamSource(inputStream) => parser.parse(inputStream, "") + } + } + rdfSource match { - case RdfStringSource(rdfStr) => parser.parse(new StringReader(rdfStr), "") - case RdfInputStreamSource(inputStream) => parser.parse(inputStream, "") + case RdfInputStreamSource(inputStream) => inputStream.close() + case _ => () + } + + parseTry match { + case Success(_) => () + case Failure(ex) => throw ex } } override def inputStreamToRdfModel(inputStream: InputStream, rdfFormat: NonJsonLD): RdfModel = { - val model: rdf4j.model.Model = rdf4j.rio.Rio.parse( - inputStream, - "", - rdfFormatToRDF4JFormat(rdfFormat) - ) + val parseTry: Try[RdfModel] = Try { + val model: rdf4j.model.Model = rdf4j.rio.Rio.parse( + inputStream, + "", + rdfFormatToRDF4JFormat(rdfFormat) + ) - new RDF4JModel( - model = model, - nodeFactory = nodeFactory - ) + new RDF4JModel( + model = model, + nodeFactory = nodeFactory + ) + } + + inputStream.close() + parseTry.get } override def makeFormattingStreamProcessor(outputStream: OutputStream, @@ -155,10 +171,19 @@ class RDF4JFormatUtil(private val modelFactory: RDF4JModelFactory, override def rdfModelToOutputStream(rdfModel: RdfModel, outputStream: OutputStream, rdfFormat: NonJsonLD): Unit = { import RDF4JConversions._ - // Construct an RDF4J writer for the requested format. - val rdfWriter: rdf4j.rio.RDFWriter = rdf4j.rio.Rio.createWriter(rdfFormatToRDF4JFormat(rdfFormat), outputStream) + val formatTry: Try[Unit] = Try { + // Construct an RDF4J writer for the requested format. + val rdfWriter: rdf4j.rio.RDFWriter = rdf4j.rio.Rio.createWriter(rdfFormatToRDF4JFormat(rdfFormat), outputStream) - // Format the RDF. - rdf4j.rio.Rio.write(rdfModel.asRDF4JModel, rdfWriter) + // Format the RDF. + rdf4j.rio.Rio.write(rdfModel.asRDF4JModel, rdfWriter) + } + + outputStream.close() + + formatTry match { + case Success(_) => () + case Failure(ex) => throw ex + } } } diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala index f46ff63b40..545f09a075 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala @@ -505,21 +505,15 @@ class ProjectsResponderADM(responderData: ResponderData) extends Responder(respo formattingStreamProcessor.start() for (namedGraphTrigFile: NamedGraphTrigFile <- namedGraphTrigFiles) { - var maybeBufferedFileInputStream: Option[BufferedInputStream] = None - val namedGraphTry: Try[Unit] = Try { - maybeBufferedFileInputStream = Some(new BufferedInputStream(new FileInputStream(namedGraphTrigFile.dataFile))) - rdfFormatUtil.parseWithStreamProcessor( - rdfSource = RdfInputStreamSource(maybeBufferedFileInputStream.get), + rdfSource = RdfInputStreamSource(new BufferedInputStream(new FileInputStream(namedGraphTrigFile.dataFile))), rdfFormat = TriG, rdfStreamProcessor = combiningRdfProcessor ) - - namedGraphTrigFile.dataFile.delete } - maybeBufferedFileInputStream.foreach(_.close) + namedGraphTrigFile.dataFile.delete namedGraphTry match { case Success(_) => () @@ -571,6 +565,7 @@ class ProjectsResponderADM(responderData: ResponderData) extends Responder(respo NamedGraphFileRequest( graphIri = trigFile.graphIri, outputFile = trigFile.dataFile, + outputFormat = TriG, featureFactoryConfig = featureFactoryConfig ) ).mapTo[FileWrittenResponse] @@ -592,6 +587,7 @@ class ProjectsResponderADM(responderData: ResponderData) extends Responder(respo sparql = adminDataSparql, graphIri = adminDataNamedGraphTrigFile.graphIri, outputFile = adminDataNamedGraphTrigFile.dataFile, + outputFormat = TriG, featureFactoryConfig = featureFactoryConfig )).mapTo[FileWrittenResponse] @@ -608,6 +604,7 @@ class ProjectsResponderADM(responderData: ResponderData) extends Responder(respo sparql = permissionDataSparql, graphIri = permissionDataNamedGraphTrigFile.graphIri, outputFile = permissionDataNamedGraphTrigFile.dataFile, + outputFormat = TriG, featureFactoryConfig = featureFactoryConfig )).mapTo[FileWrittenResponse] diff --git a/webapi/src/main/scala/org/knora/webapi/store/triplestore/http/HttpTriplestoreConnector.scala b/webapi/src/main/scala/org/knora/webapi/store/triplestore/http/HttpTriplestoreConnector.scala index 1260d3b46b..a0ac024ec2 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/triplestore/http/HttpTriplestoreConnector.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/triplestore/http/HttpTriplestoreConnector.scala @@ -70,7 +70,7 @@ class HttpTriplestoreConnector extends Actor with ActorLogging with Instrumentat private val mimeTypeApplicationSparqlResultsJson = "application/sparql-results+json" private val mimeTypeTextTurtle = "text/turtle" private val mimeTypeApplicationSparqlUpdate = "application/sparql-update" - private val mimeTypeApplicationTrig = "application/trig" + private val mimeTypeApplicationNQuads = "application/n-quads" private implicit val system: ActorSystem = context.system private val settings = KnoraSettings(system) @@ -187,8 +187,8 @@ class HttpTriplestoreConnector extends Actor with ActorLogging with Instrumentat case SparqlSelectRequest(sparql: String) => try2Message(sender(), sparqlHttpSelect(sparql), log) case sparqlConstructRequest: SparqlConstructRequest => try2Message(sender(), sparqlHttpConstruct(sparqlConstructRequest), log) case sparqlExtendedConstructRequest: SparqlExtendedConstructRequest => try2Message(sender(), sparqlHttpExtendedConstruct(sparqlExtendedConstructRequest), log) - case SparqlConstructFileRequest(sparql: String, graphIri: IRI, outputFile: File, featureFactoryConfig: FeatureFactoryConfig) => try2Message(sender(), sparqlHttpConstructFile(sparql, graphIri, outputFile, featureFactoryConfig), log) - case NamedGraphFileRequest(graphIri: IRI, outputFile: File, featureFactoryConfig: FeatureFactoryConfig) => try2Message(sender(), sparqlHttpGraphFile(graphIri, outputFile, featureFactoryConfig), log) + case SparqlConstructFileRequest(sparql: String, graphIri: IRI, outputFile: File, outputFormat: QuadFormat, featureFactoryConfig: FeatureFactoryConfig) => try2Message(sender(), sparqlHttpConstructFile(sparql, graphIri, outputFile, outputFormat, featureFactoryConfig), log) + case NamedGraphFileRequest(graphIri: IRI, outputFile: File, outputFormat: QuadFormat, featureFactoryConfig: FeatureFactoryConfig) => try2Message(sender(), sparqlHttpGraphFile(graphIri, outputFile, outputFormat, featureFactoryConfig), log) case NamedGraphDataRequest(graphIri: IRI) => try2Message(sender(), sparqlHttpGraphData(graphIri), log) case SparqlUpdateRequest(sparql: String) => try2Message(sender(), sparqlHttpUpdate(sparql), log) case SparqlAskRequest(sparql: String) => try2Message(sender(), sparqlHttpAsk(sparql), log) @@ -295,102 +295,29 @@ class HttpTriplestoreConnector extends Actor with ActorLogging with Instrumentat } /** - * Adds a context IRI to RDF statements. + * Given a SPARQL CONSTRUCT query string, runs the query, saving the result in a file. * - * @param graphIri the IRI of the named graph. - * @param formattingStreamProcessor an [[RdfStreamProcessor]] for writing the result. - * @param featureFactoryConfig the feature factory configuration. - */ - private class ContextAddingProcessor(graphIri: IRI, - formattingStreamProcessor: RdfStreamProcessor, - featureFactoryConfig: FeatureFactoryConfig) extends RdfStreamProcessor { - private val nodeFactory: RdfNodeFactory = RdfFeatureFactory.getRdfNodeFactory(featureFactoryConfig) - - override def start(): Unit = formattingStreamProcessor.start() - - override def processNamespace(prefix: String, namespace: IRI): Unit = { - formattingStreamProcessor.processNamespace(prefix = prefix, namespace = namespace) - } - - override def processStatement(statement: Statement): Unit = { - val outputStatement = nodeFactory.makeStatement( - subj = statement.subj, - pred = statement.pred, - obj = statement.obj, - context = Some(graphIri) - ) - - formattingStreamProcessor.processStatement(outputStatement) - } - - override def finish(): Unit = formattingStreamProcessor.finish() - } - - /** - * Reads RDF data in Turtle format from an [[RdfSource]], adds a named graph IRI to each statement, - * and writes the result to a file in TriG format. - * - * @param rdfSource the RDF data source. - * @param graphIri the named graph IRI to be added. - * @param outputFile the output file. - * @param featureFactoryConfig the feature factory configuration. - */ - private def turtleToTrig(rdfSource: RdfSource, - graphIri: IRI, - outputFile: File, - featureFactoryConfig: FeatureFactoryConfig): Unit = { - val rdfFormatUtil: RdfFormatUtil = RdfFeatureFactory.getRdfFormatUtil(featureFactoryConfig) - var maybeBufferedFileOutputStream: Option[BufferedOutputStream] = None - - val parseTry: Try[Unit] = Try { - maybeBufferedFileOutputStream = Some(new BufferedOutputStream(new FileOutputStream(outputFile))) - - val formattingStreamProcessor: RdfStreamProcessor = rdfFormatUtil.makeFormattingStreamProcessor( - outputStream = maybeBufferedFileOutputStream.get, - rdfFormat = TriG - ) - - val contextAddingProcessor = new ContextAddingProcessor( - graphIri = graphIri, - formattingStreamProcessor = formattingStreamProcessor, - featureFactoryConfig = featureFactoryConfig - ) - - rdfFormatUtil.parseWithStreamProcessor( - rdfSource = rdfSource, - rdfFormat = Turtle, - rdfStreamProcessor = contextAddingProcessor - ) - } - - maybeBufferedFileOutputStream.foreach(_.close) - - parseTry match { - case Success(_) => () - case Failure(ex) => throw ex - } - } - - /** - * Given a SPARQL CONSTRUCT query string, runs the query, saving the result as a TriG file. - * - * @param sparql the SPARQL CONSTRUCT query string. - * @param graphIri the named graph IRI to be used in the TriG file. - * @param outputFile the output file. + * @param sparql the SPARQL CONSTRUCT query string. + * @param graphIri the named graph IRI to be used in the output file. + * @param outputFile the output file. + * @param outputFormat the output file format. * @return a [[FileWrittenResponse]]. */ private def sparqlHttpConstructFile(sparql: String, graphIri: IRI, outputFile: File, + outputFormat: QuadFormat, featureFactoryConfig: FeatureFactoryConfig): Try[FileWrittenResponse] = { + val rdfFormatUtil: RdfFormatUtil = RdfFeatureFactory.getRdfFormatUtil(featureFactoryConfig) + for { turtleStr <- getSparqlHttpResponse(sparql, isUpdate = false, acceptMimeType = mimeTypeTextTurtle) - _ = turtleToTrig( + _ = rdfFormatUtil.turtleToQuadsFile( rdfSource = RdfStringSource(turtleStr), graphIri = graphIri, outputFile = outputFile, - featureFactoryConfig = featureFactoryConfig + outputFormat = outputFormat ) } yield FileWrittenResponse() } @@ -687,7 +614,7 @@ class HttpTriplestoreConnector extends Actor with ActorLogging with Instrumentat val httpContext: HttpClientContext = makeHttpContext val httpPost: HttpPost = new HttpPost("/$/datasets") - val stringEntity = new StringEntity(triplestoreConfig, ContentType.create(mimeTypeApplicationTrig)) + val stringEntity = new StringEntity(triplestoreConfig, ContentType.create(mimeTypeTextTurtle)) httpPost.setEntity(stringEntity) doHttpRequest( @@ -777,15 +704,17 @@ class HttpTriplestoreConnector extends Actor with ActorLogging with Instrumentat } /** - * Requests the contents of a named graph in TriG format, saving the response in a file. + * Requests the contents of a named graph, saving the response in a file. * * @param graphIri the IRI of the named graph. * @param outputFile the file to be written. + * @param outputFormat the output file format. * @param featureFactoryConfig the feature factory configuration. - * @return a string containing the contents of the graph in TriG format. + * @return a string containing the contents of the graph in N-Quads format. */ private def sparqlHttpGraphFile(graphIri: IRI, outputFile: File, + outputFormat: QuadFormat, featureFactoryConfig: FeatureFactoryConfig): Try[FileWrittenResponse] = { val httpContext: HttpClientContext = makeHttpContext val httpGet = new HttpGet(makeNamedGraphDownloadUri(graphIri)) @@ -794,8 +723,7 @@ class HttpTriplestoreConnector extends Actor with ActorLogging with Instrumentat val makeResponse: CloseableHttpResponse => FileWrittenResponse = writeResponseFile( outputFile = outputFile, featureFactoryConfig = featureFactoryConfig, - maybeGraphIri = Some(graphIri), - convertToTrig = true + maybeGraphIriAndFormat = Some(GraphIriAndFormat(graphIri = graphIri, quadFormat = outputFormat)) ) doHttpRequest( @@ -873,11 +801,11 @@ class HttpTriplestoreConnector extends Actor with ActorLogging with Instrumentat } /** - * Dumps the whole repository in TriG format, saving the response in a file. + * Dumps the whole repository in N-Quads format, saving the response in a file. * * @param outputFile the output file. * @param featureFactoryConfig the feature factory configuration. - * @return a string containing the contents of the graph in TriG format. + * @return a string containing the contents of the graph in N-Quads format. */ private def downloadRepository(outputFile: File, featureFactoryConfig: FeatureFactoryConfig): Try[FileWrittenResponse] = { val httpContext: HttpClientContext = makeHttpContext @@ -893,7 +821,7 @@ class HttpTriplestoreConnector extends Actor with ActorLogging with Instrumentat } val httpGet = new HttpGet(uriBuilder.build()) - httpGet.addHeader("Accept", mimeTypeApplicationTrig) + httpGet.addHeader("Accept", mimeTypeApplicationNQuads) val queryTimeoutMillis = settings.triplestoreQueryTimeout.toMillis.toInt * 10 val queryRequestConfig = RequestConfig.custom() @@ -922,14 +850,14 @@ class HttpTriplestoreConnector extends Actor with ActorLogging with Instrumentat } /** - * Uploads repository content from a TriG file. + * Uploads repository content from an N-Quads file. * - * @param inputFile a TriG file containing the content to be uploaded to the repository. + * @param inputFile an N-Quads file containing the content to be uploaded to the repository. */ private def uploadRepository(inputFile: File): Try[RepositoryUploadedResponse] = { val httpContext: HttpClientContext = makeHttpContext val httpPost: HttpPost = new HttpPost(repositoryUploadPath) - val fileEntity = new FileEntity(inputFile, ContentType.create(mimeTypeApplicationTrig, "UTF-8")) + val fileEntity = new FileEntity(inputFile, ContentType.create(mimeTypeApplicationNQuads, "UTF-8")) httpPost.setEntity(fileEntity) doHttpRequest( @@ -1024,10 +952,12 @@ class HttpTriplestoreConnector extends Actor with ActorLogging with Instrumentat maybeResponse.foreach(_.close) + // TODO: Can we throw a more user-friendly exception if the query timed out? + // TODO: Can we make Fuseki abandon the query if it takes too long? + triplestoreResponseTry.recover { case tre: TriplestoreResponseException => throw tre - // TODO: Can we throw a more user-friendly exception if the query timed out? - // TODO: Can we make Fuseki abandon the query if it takes too long? + case e: Exception => log.error(e, s"Failed to connect to triplestore") throw TriplestoreConnectionException(s"Failed to connect to triplestore", e, log) @@ -1037,6 +967,7 @@ class HttpTriplestoreConnector extends Actor with ActorLogging with Instrumentat def returnResponseAsString(response: CloseableHttpResponse): String = { Option(response.getEntity) match { case None => "" + case Some(responseEntity) => EntityUtils.toString(responseEntity) } @@ -1047,6 +978,7 @@ class HttpTriplestoreConnector extends Actor with ActorLogging with Instrumentat case None => log.error(s"Triplestore returned no content for graph $graphIri") throw TriplestoreResponseException(s"Triplestore returned no content for graph $graphIri") + case Some(responseEntity: HttpEntity) => NamedGraphDataResponse( turtle = EntityUtils.toString(responseEntity) @@ -1054,8 +986,8 @@ class HttpTriplestoreConnector extends Actor with ActorLogging with Instrumentat } } - def returnUploadResponse(response: CloseableHttpResponse): RepositoryUploadedResponse = { - RepositoryUploadedResponse() + def returnUploadResponse: CloseableHttpResponse => RepositoryUploadedResponse = { + _ => RepositoryUploadedResponse() } def returnInsertGraphDataResponse(graphName: String)(response: CloseableHttpResponse): InsertGraphDataContentResponse = { @@ -1063,59 +995,81 @@ class HttpTriplestoreConnector extends Actor with ActorLogging with Instrumentat case None => log.error(s"$graphName could not be inserted into Triplestore.") throw TriplestoreResponseException(s"$graphName could not be inserted into Triplestore.") + case Some(_) => InsertGraphDataContentResponse() } } + /** + * Represents a named graph IRI and the file format that the graph should be written in. + * + * @param graphIri the named graph IRI. + * @param quadFormat the file format. + */ + case class GraphIriAndFormat(graphIri: IRI, quadFormat: QuadFormat) + + /** + * Writes an HTTP response to a file. + * + * @param outputFile the output file. + * @param featureFactoryConfig the feature factory configuration. + * @param maybeGraphIriAndFormat a graph IRI and quad format for the output file. If defined, the response + * is parsed as Turtle and converted to the output format, with the graph IRI + * added to each statement. Otherwise, the response is written as-is to the + * output file. + * @param response the response to be read. + * @return a [[FileWrittenResponse]]. + */ def writeResponseFile(outputFile: File, featureFactoryConfig: FeatureFactoryConfig, - maybeGraphIri: Option[IRI] = None, - convertToTrig: Boolean = false)(response: CloseableHttpResponse): FileWrittenResponse = { + maybeGraphIriAndFormat: Option[GraphIriAndFormat] = None)(response: CloseableHttpResponse): FileWrittenResponse = { Option(response.getEntity) match { case Some(responseEntity: HttpEntity) => - // Stream the HTTP entity to the output file. - if (convertToTrig) { - // Stream the HTTP entity to the temporary .ttl file. - val turtleFile = new File(outputFile.getCanonicalPath + ".ttl") - Files.copy(responseEntity.getContent, Paths.get(turtleFile.getCanonicalPath)) + // Are we converting the response to a quad format? + maybeGraphIriAndFormat match { + case Some(GraphIriAndFormat(graphIri, quadFormat)) => + // Yes. Stream the HTTP entity to a temporary Turtle file. + val turtleFile = new File(outputFile.getCanonicalPath + ".ttl") + Files.copy(responseEntity.getContent, Paths.get(turtleFile.getCanonicalPath)) - // Convert the Turtle to Trig. + // Convert the Turtle to the output format. - var maybeBufferedInputStream: Option[BufferedInputStream] = None + val rdfFormatUtil: RdfFormatUtil = RdfFeatureFactory.getRdfFormatUtil(featureFactoryConfig) - val processFileTry: Try[Unit] = Try { - maybeBufferedInputStream = Some(new BufferedInputStream(new FileInputStream(turtleFile))) + val processFileTry: Try[Unit] = Try { + rdfFormatUtil.turtleToQuadsFile( + rdfSource = RdfInputStreamSource(new BufferedInputStream(new FileInputStream(turtleFile))), + graphIri = graphIri, + outputFile = outputFile, + outputFormat = quadFormat + ) + } - turtleToTrig( - rdfSource = RdfInputStreamSource(maybeBufferedInputStream.get), - graphIri = maybeGraphIri.get, - outputFile = outputFile, - featureFactoryConfig = featureFactoryConfig - ) - } + turtleFile.delete() - maybeBufferedInputStream.foreach(_.close()) - turtleFile.delete() + processFileTry match { + case Success(_) => () + case Failure(ex) => throw ex + } - processFileTry match { - case Success(_) => () - case Failure(ex) => throw ex - } - } else { - Files.copy(responseEntity.getContent, Paths.get(outputFile.getCanonicalPath)) + case None => + // No. Stream the HTTP entity directly to the output file. + Files.copy(responseEntity.getContent, Paths.get(outputFile.getCanonicalPath)) } FileWrittenResponse() case None => - if (maybeGraphIri.nonEmpty) { - log.error(s"Triplestore returned no content for graph ${maybeGraphIri.get}") - throw TriplestoreResponseException(s"Triplestore returned no content for graph ${maybeGraphIri.get}") - } else { - log.error(s"Triplestore returned no content for repository dump") - throw TriplestoreResponseException(s"Triplestore returned no content for for repository dump") + maybeGraphIriAndFormat match { + case Some(GraphIriAndFormat(graphIri, _)) => + log.error(s"Triplestore returned no content for graph $graphIri") + throw TriplestoreResponseException(s"Triplestore returned no content for graph $graphIri") + + case None => + log.error(s"Triplestore returned no content for repository dump") + throw TriplestoreResponseException(s"Triplestore returned no content for for repository dump") } } } diff --git a/webapi/src/main/scala/org/knora/webapi/store/triplestore/upgrade/RepositoryUpdater.scala b/webapi/src/main/scala/org/knora/webapi/store/triplestore/upgrade/RepositoryUpdater.scala index 032e7dad1e..eebac1b196 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/triplestore/upgrade/RepositoryUpdater.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/triplestore/upgrade/RepositoryUpdater.scala @@ -63,14 +63,12 @@ class RepositoryUpdater(system: ActorSystem, private val log: Logger = logger /** - * A map of version strings to plugins. + * A list of available plugins. */ - private val pluginsForVersionsMap: Map[String, PluginForKnoraBaseVersion] = RepositoryUpdatePlan.makePluginsForVersions( + private val plugins: Seq[PluginForKnoraBaseVersion] = RepositoryUpdatePlan.makePluginsForVersions( featureFactoryConfig = featureFactoryConfig, log = log - ).map { - knoraBaseVersion => knoraBaseVersion.versionString -> knoraBaseVersion - }.toMap + ) /** * Updates the repository, if necessary, to work with the current version of Knora. @@ -128,16 +126,24 @@ class RepositoryUpdater(system: ActorSystem, maybeRepositoryVersionString match { case Some(repositoryVersion) => // The repository has a version string. Get the plugins for all subsequent versions. - val pluginForRepositoryVersion: PluginForKnoraBaseVersion = pluginsForVersionsMap.getOrElse( + + // Make a map of version strings to plugins. + val versionsToPluginsMap: Map[String, PluginForKnoraBaseVersion] = plugins.map { + plugin => plugin.versionString -> plugin + }.toMap + + val pluginForRepositoryVersion: PluginForKnoraBaseVersion = versionsToPluginsMap.getOrElse( repositoryVersion, throw InconsistentRepositoryDataException(s"No such repository version $repositoryVersion") ) - pluginsForVersionsMap.values.filter(_.versionNumber > pluginForRepositoryVersion.versionNumber).toSeq + plugins.filter { + plugin => plugin.versionNumber > pluginForRepositoryVersion.versionNumber + } case None => // The repository has no version string. Include all updates. - pluginsForVersionsMap.values.toSeq + plugins } } @@ -165,8 +171,8 @@ class RepositoryUpdater(system: ActorSystem, } // The file to save the repository in. - val downloadedRepositoryFile = new File(downloadDir, "downloaded-repository.trig") - val transformedRepositoryFile = new File(downloadDir, "transformed-repository.trig") + val downloadedRepositoryFile = new File(downloadDir, "downloaded-repository.nq") + val transformedRepositoryFile = new File(downloadDir, "transformed-repository.nq") log.info("Downloading repository file...") for { @@ -209,7 +215,7 @@ class RepositoryUpdater(system: ActorSystem, pluginsForNeededUpdates: Seq[PluginForKnoraBaseVersion]): Unit = { // Parse the input file. log.info("Reading repository file...") - val model = rdfFormatUtil.fileToRdfModel(file = downloadedRepositoryFile, rdfFormat = TriG) + val model = rdfFormatUtil.fileToRdfModel(file = downloadedRepositoryFile, rdfFormat = NQuads) log.info(s"Read ${model.size} statements.") // Run the update plugins. @@ -227,7 +233,7 @@ class RepositoryUpdater(system: ActorSystem, rdfFormatUtil.rdfModelToFile( rdfModel = model, file = transformedRepositoryFile, - rdfFormat = TriG + rdfFormat = NQuads ) } diff --git a/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADMSpec.scala b/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADMSpec.scala index 8f6bdf1f91..a9385af748 100644 --- a/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADMSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADMSpec.scala @@ -16,15 +16,15 @@ package org.knora.webapi.messages.admin.responder.projectsmessages -import com.typesafe.config.ConfigFactory -import org.knora.webapi.exceptions.{BadRequestException, OntologyConstraintException} +import com.typesafe.config.{Config, ConfigFactory} import org.knora.webapi._ -import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 +import org.knora.webapi.exceptions.{BadRequestException, OntologyConstraintException} import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 import org.knora.webapi.sharedtestdata.SharedTestDataADM object ProjectsMessagesADMSpec { - val config = ConfigFactory.parseString( + val config: Config = ConfigFactory.parseString( """ akka.loglevel = "DEBUG" akka.stdout-loglevel = "DEBUG" @@ -32,25 +32,12 @@ object ProjectsMessagesADMSpec { } /** - * This spec is used to test subclasses of the [[ProjectsMessagesADM]] class. - */ + * This spec is used to test subclasses of the [[ProjectsResponderRequestADM]] trait. + */ class ProjectsMessagesADMSpec extends CoreSpec(ProjectsMessagesADMSpec.config) { private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - private val id = SharedTestDataADM.rootUser.id - private val email = SharedTestDataADM.rootUser.email - private val password = SharedTestDataADM.rootUser.password - private val token = SharedTestDataADM.rootUser.token - private val givenName = SharedTestDataADM.rootUser.givenName - private val familyName = SharedTestDataADM.rootUser.familyName - private val status = SharedTestDataADM.rootUser.status - private val lang = SharedTestDataADM.rootUser.lang - private val groups = SharedTestDataADM.rootUser.groups - private val projects = SharedTestDataADM.rootUser.projects - private val sessionId = SharedTestDataADM.rootUser.sessionId - private val permissions = SharedTestDataADM.rootUser.permissions - "The CreateProjectApiRequestADM case class" should { "return a 'BadRequest' when project description is not supplied" in { diff --git a/webapi/src/test/scala/org/knora/webapi/messages/util/rdf/JsonLDUtilSpec.scala b/webapi/src/test/scala/org/knora/webapi/messages/util/rdf/JsonLDUtilSpec.scala index bc23c5ad21..d863189d0b 100644 --- a/webapi/src/test/scala/org/knora/webapi/messages/util/rdf/JsonLDUtilSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/messages/util/rdf/JsonLDUtilSpec.scala @@ -359,5 +359,38 @@ abstract class JsonLDUtilSpec(featureToggle: FeatureToggle) extends CoreSpec { assert(flatJsonLD.body == expectedFlatJsonLD) } + + "correctly process input that results in an empty blank node" in { + // The JSON-LD parser ignores statements with invalid IRIs, and this can produce an empty + // blank node. + + val jsonLDWithInvalidProperties = + """{ + | "http://ns.dasch.swiss/repository#hasLicense":{ + | "type": "https://schema.org/URL", + | "value": "https://creativecommons.org/licenses/by/3.0" + | } + |}""".stripMargin + + // Parse the JSON-LD and check the parsed data structure. + + val jsonLDDocument = JsonLDUtil.parseJsonLD(jsonLDWithInvalidProperties) + + val expectedJsonLDDocument = JsonLDDocument( + body = JsonLDObject(Map("http://ns.dasch.swiss/repository#hasLicense" -> JsonLDObject(Map.empty))), + context = JsonLDObject(Map.empty) + ) + + assert(jsonLDDocument == expectedJsonLDDocument) + + // Convert it to an RdfModel and check the result. + val rdfModel = jsonLDDocument.toRdfModel(rdfModelFactory) + val expectedRdfModel = rdfFormatUtil.parseToRdfModel("[] [] .", Turtle) + assert(rdfModel == expectedRdfModel) + + // Convert back to JSON-LD and check that it's the same. + val jsonLDDocumentFromRdfModel = JsonLDUtil.fromRdfModel(rdfModel) + assert(jsonLDDocumentFromRdfModel == expectedJsonLDDocument) + } } } diff --git a/webapi/src/test/scala/org/knora/webapi/messages/util/rdf/RdfFormatUtilSpec.scala b/webapi/src/test/scala/org/knora/webapi/messages/util/rdf/RdfFormatUtilSpec.scala index 0f6b8cf01e..d69343cbed 100644 --- a/webapi/src/test/scala/org/knora/webapi/messages/util/rdf/RdfFormatUtilSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/messages/util/rdf/RdfFormatUtilSpec.scala @@ -20,6 +20,7 @@ package org.knora.webapi.util.rdf import java.io._ +import java.nio.file.Files import org.knora.webapi.feature.{FeatureFactoryConfig, FeatureToggle, KnoraSettingsFeatureFactoryConfig, TestFeatureFactoryConfig} import org.knora.webapi.messages.OntologyConstants @@ -83,11 +84,12 @@ abstract class RdfFormatUtilSpec(featureToggle: FeatureToggle) extends CoreSpec } } - private def checkModelForRdfTypeBook(rdfModel: RdfModel): Unit = { + private def checkModelForRdfTypeBook(rdfModel: RdfModel, context: Option[IRI] = None): Unit = { val statements: Set[Statement] = rdfModel.find( subj = Some(rdfNodeFactory.makeIriNode("http://rdfh.ch/0803/2a6221216701")), pred = Some(rdfNodeFactory.makeIriNode(OntologyConstants.Rdf.Type)), - obj = None + obj = None, + context = context ).toSet assert(statements.size == 1) @@ -153,6 +155,52 @@ abstract class RdfFormatUtilSpec(featureToggle: FeatureToggle) extends CoreSpec assert(rdfXmlOutputModel == inputModel) } + "parse RDF in TriG format" in { + val graphIri = "http://example.org/data#" + val inputTrig = FileUtil.readTextFile(new File("test_data/rdfFormatUtil/BookReiseInsHeiligeLand.trig")) + val inputModel: RdfModel = rdfFormatUtil.parseToRdfModel(rdfStr = inputTrig, rdfFormat = TriG) + checkModelForRdfTypeBook(rdfModel = inputModel, context = Some(graphIri)) + } + + "parse RDF in N-Quads format" in { + val graphIri = "http://example.org/data#" + val inputTrig = FileUtil.readTextFile(new File("test_data/rdfFormatUtil/BookReiseInsHeiligeLand.nq")) + val inputModel: RdfModel = rdfFormatUtil.parseToRdfModel(rdfStr = inputTrig, rdfFormat = NQuads) + checkModelForRdfTypeBook(rdfModel = inputModel, context = Some(graphIri)) + } + + "read Turtle, add a graph IRI to it, write it to a TriG file, and read back the TriG file" in { + val graphIri = "http://example.org/data#" + val rdfSource = RdfInputStreamSource(new BufferedInputStream(new FileInputStream(new File("test_data/resourcesR2RV2/BookReiseInsHeiligeLand.ttl")))) + val outputFile: File = Files.createTempFile("test", ".trig").toFile + + rdfFormatUtil.turtleToQuadsFile( + rdfSource = rdfSource, + graphIri = graphIri, + outputFile = outputFile, + outputFormat = TriG + ) + + val quadsModel: RdfModel = rdfFormatUtil.fileToRdfModel(file = outputFile, rdfFormat = TriG) + checkModelForRdfTypeBook(rdfModel = quadsModel, context = Some(graphIri)) + } + + "read Turtle, add a graph IRI to it, write it to an N-Quads file, and read back the N-Quads file" in { + val graphIri = "http://example.org/data#" + val rdfSource = RdfInputStreamSource(new BufferedInputStream(new FileInputStream(new File("test_data/resourcesR2RV2/BookReiseInsHeiligeLand.ttl")))) + val outputFile: File = Files.createTempFile("test", ".trig").toFile + + rdfFormatUtil.turtleToQuadsFile( + rdfSource = rdfSource, + graphIri = graphIri, + outputFile = outputFile, + outputFormat = NQuads + ) + + val quadsModel: RdfModel = rdfFormatUtil.fileToRdfModel(file = outputFile, rdfFormat = NQuads) + checkModelForRdfTypeBook(rdfModel = quadsModel, context = Some(graphIri)) + } + "parse RDF in JSON-LD format, producing a JsonLDDocument, then format it as JSON-LD again" in { val inputTurtle: String = FileUtil.readTextFile(new File("test_data/resourcesR2RV2/BookReiseInsHeiligeLand.jsonld")) val inputJsonLDDocument: JsonLDDocument = rdfFormatUtil.parseToJsonLDDocument(rdfStr = inputTurtle, rdfFormat = JsonLD) @@ -197,7 +245,6 @@ abstract class RdfFormatUtilSpec(featureToggle: FeatureToggle) extends CoreSpec rdfStreamProcessor = testStreamProcessor ) - inputStream.close() testStreamProcessor.check() } @@ -219,7 +266,7 @@ abstract class RdfFormatUtilSpec(featureToggle: FeatureToggle) extends CoreSpec rdfStreamProcessor = formattingStreamProcessor ) - fileInputStream.close() + byteArrayOutputStream.close() // Read back the ByteArrayOutputStream and check that it's correct. @@ -232,7 +279,6 @@ abstract class RdfFormatUtilSpec(featureToggle: FeatureToggle) extends CoreSpec rdfStreamProcessor = testStreamProcessor ) - byteArrayInputStream.close() testStreamProcessor.check() }