diff --git a/services/src/main/java/org/fao/geonet/api/registries/vocabularies/KeywordsApi.java b/services/src/main/java/org/fao/geonet/api/registries/vocabularies/KeywordsApi.java index 956d616f55b..200860f4a4c 100644 --- a/services/src/main/java/org/fao/geonet/api/registries/vocabularies/KeywordsApi.java +++ b/services/src/main/java/org/fao/geonet/api/registries/vocabularies/KeywordsApi.java @@ -49,6 +49,7 @@ import org.fao.geonet.api.tools.i18n.LanguageUtils; import org.fao.geonet.constants.Geonet; import org.fao.geonet.domain.ISODate; +import org.fao.geonet.exceptions.BadParameterEx; import org.fao.geonet.kernel.*; import org.fao.geonet.kernel.search.KeywordsSearcher; import org.fao.geonet.kernel.search.keyword.*; @@ -139,6 +140,8 @@ public class KeywordsApi { @Autowired ThesaurusManager thesaurusManager; + List allowedExtensions = Arrays.asList("rdf", "owl", "xml"); + /** * Search keywords. * @@ -570,8 +573,8 @@ private Object getKeyword( String key = ((Map.Entry) entry).getKey().toString(); String value = ((Map.Entry) entry).getValue().toString(); Element conv = new Element("conversion"); - conv.setAttribute("from",key.toString()); - conv.setAttribute("to",value.toString().replace("#","")); + conv.setAttribute("from", key); + conv.setAttribute("to", value.replace("#","")); langConversion.addContent(conv); } @@ -610,9 +613,7 @@ private Object getKeyword( root.addContent(gui); root.addContent(nodeUrl); root.addContent(nodeId); - final Element transform = Xml.transform(root, convertXsl); - - return transform; + return Xml.transform(root, convertXsl); } } @@ -632,7 +633,8 @@ private Object getKeyword( value = "/{thesaurus:.+}", method = RequestMethod.GET, produces = { - MediaType.TEXT_XML_VALUE + MediaType.TEXT_XML_VALUE, + MediaType.APPLICATION_XML_VALUE }) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Thesaurus in SKOS format.", @@ -808,13 +810,14 @@ public String uploadThesaurus( String extension = FilenameUtils.getExtension(fname); - if (extension.equalsIgnoreCase("rdf") || - extension.equalsIgnoreCase("xml")) { + if (allowedExtensions.contains(extension.toLowerCase())) { Log.debug(Geonet.THESAURUS, "Uploading thesaurus: " + fname); // Rename .xml to .rdf for all thesaurus fname = fname.replace(extension, "rdf"); - uploadThesaurus(rdfFile, stylesheet, context, fname, type.toString(), dir); + uploadThesaurus(rdfFile, + getStylesheetForExtension(stylesheet, extension), + context, fname, type.toString(), dir); } else { Log.debug(Geonet.THESAURUS, "Incorrect extension for thesaurus named: " + fname); throw new Exception("Incorrect extension for thesaurus named: " @@ -827,13 +830,13 @@ public String uploadThesaurus( return String.format("Thesaurus '%s' loaded in %d sec.", fname, duration); } finally { - if (tempDir != null) { - FileUtils.deleteQuietly(tempDir); - } + FileUtils.deleteQuietly(tempDir); } } - + private static String getStylesheetForExtension(String stylesheet, String extension) { + return extension.equals("owl") ? "owl-to-skos" : stylesheet; + } /** @@ -935,7 +938,7 @@ public void importCsvAsThesaurus( } long fsize; - if (csvFile != null && Files.exists(csvFile)) { + if (Files.exists(csvFile)) { fsize = Files.size(csvFile); } else { throw new MissingServletRequestParameterException("CSV file doesn't exist", "file"); @@ -978,9 +981,7 @@ public void importCsvAsThesaurus( response.getOutputStream().write(Xml.getString(element).getBytes()); } } finally { - if (tempDir != null) { - FileUtils.deleteQuietly(tempDir); - } + FileUtils.deleteQuietly(tempDir); } } @@ -1049,7 +1050,7 @@ public Element convertCsvToSkos(Path csvFile, extractRelated(key, thesaurusNamespaceUrl, csvParser, csvRecord, conceptLinkSeparator, conceptBroaderIdColumn, broaderLinks); - if (broaderLinks.get(key) == null || broaderLinks.get(key).size() == 0) { + if (broaderLinks.get(key) == null || broaderLinks.get(key).isEmpty()) { topConcepts.add(key); } extractRelated(key, thesaurusNamespaceUrl, csvParser, csvRecord, @@ -1070,7 +1071,7 @@ public Element convertCsvToSkos(Path csvFile, } Element scheme = buildConceptScheme(csvFile, thesaurusTitle, thesaurusNamespaceUrl); - if(broaderLinks.size() > 0 && topConcepts.size() > 0) { + if(broaderLinks.size() > 0 && !topConcepts.isEmpty()) { topConcepts.forEach(t -> { Element topConcept = new Element("hasTopConcept", SKOS_NAMESPACE); topConcept.setAttribute("resource", t, RDF_NAMESPACE); @@ -1281,13 +1282,14 @@ public String uploadThesaurusFromUrl( String extension = FilenameUtils.getExtension(fname); - if (extension.equalsIgnoreCase("rdf") || - extension.equalsIgnoreCase("xml")) { + if (allowedExtensions.contains(extension.toLowerCase())) { Log.debug(Geonet.THESAURUS, "Uploading thesaurus: " + fname); // Rename .xml to .rdf for all thesaurus fname = fname.replace(extension, "rdf"); - uploadThesaurus(rdfFile, stylesheet, context, fname, type.toString(), dir); + uploadThesaurus(rdfFile, + getStylesheetForExtension(stylesheet, extension), + context, fname, type.toString(), dir); } else { Log.debug(Geonet.THESAURUS, "Incorrect extension for thesaurus named: " + fname); throw new MissingServletRequestParameterException("Incorrect extension for thesaurus", fname); @@ -1423,7 +1425,7 @@ private Path extractSKOSFromRegistry(String registryUrl, REGISTRY_TYPE registryT * @throws IOException Signals that an I/O exception has occurred. * @throws MalformedURLException the malformed URL exception */ - private Path getXMLContentFromUrl(String url, ServiceContext context) throws URISyntaxException, IOException, MalformedURLException { + private Path getXMLContentFromUrl(String url, ServiceContext context) throws URISyntaxException, IOException { Path rdfFile; URI uri = new URI(url); rdfFile = Files.createTempFile("thesaurus", ".rdf"); @@ -1451,17 +1453,21 @@ private void uploadThesaurus(Path rdfFile, String style, ServiceContext context, String fname, String type, String dir) throws Exception { - Path stylePath = context.getAppPath().resolve(Geonet.Path.STYLESHEETS); - Element tsXml; Element xml = Xml.loadFile(rdfFile); xml.detach(); if (!"_none_".equals(style)) { FilePathChecker.verify(style); - - tsXml = Xml.transform(xml, stylePath.resolve(style)); - tsXml.detach(); + Path xsltPath = dataDirectory.getWebappDir().resolve(String.format( + "xslt/services/thesaurus/%s.xsl", style)); + if (Files.exists(xsltPath)) { + tsXml = Xml.transform(xml, xsltPath); + tsXml.detach(); + } else { + throw new BadParameterEx(String.format( + "XSL transformation '%s' not found. Only conversion provided in xslt/services/thesaurus can be used.", style)); + } } else { tsXml = xml; } diff --git a/services/src/test/java/org/fao/geonet/api/registries/vocabularies/KeywordsApiTest.java b/services/src/test/java/org/fao/geonet/api/registries/vocabularies/KeywordsApiTest.java index 68cc20c7e6e..01871cb85a0 100644 --- a/services/src/test/java/org/fao/geonet/api/registries/vocabularies/KeywordsApiTest.java +++ b/services/src/test/java/org/fao/geonet/api/registries/vocabularies/KeywordsApiTest.java @@ -32,16 +32,24 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; import org.springframework.mock.web.MockMultipartFile; import org.springframework.mock.web.MockMultipartHttpServletRequest; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; import javax.servlet.http.HttpSession; import java.util.List; import static org.fao.geonet.csw.common.Csw.NAMESPACE_DC; +import static org.fao.geonet.csw.common.Csw.NAMESPACE_DCT; import static org.fao.geonet.kernel.rdf.Selectors.RDF_NAMESPACE; import static org.fao.geonet.kernel.rdf.Selectors.SKOS_NAMESPACE; import static org.junit.Assert.assertEquals; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * export CATALOG=http://localhost:8080/geonetwork @@ -61,6 +69,8 @@ public class KeywordsApiTest extends AbstractServiceIntegrationTest { @Autowired private SpringLocalServiceInvoker invoker; + @Autowired + private WebApplicationContext wac; @Test public void testConvertCsvToSkos() throws Exception { @@ -199,4 +209,46 @@ public void testConvertCsvToSkosDefaultTitleAndNamespace() throws Exception { assertEquals( "taxref.csv", scheme.getChildText("title", NAMESPACE_DC)); } + + + @Test + public void testImportOntologyToSkos() throws Exception { + createServiceContext(); + User user = new User().setId(USER_ID); + HttpSession session = loginAs(user); + MockHttpSession mockHttpSession = loginAsAdmin(); + + MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest(session.getServletContext()); + request.setRequestURI("/srv/api/registries/vocabularies"); + MockMultipartFile file = new MockMultipartFile( + "file", + "mobility-theme.owl", + null, + getClass().getClassLoader().getResourceAsStream("mobility-theme.owl")); + request.addFile(file); + request.setSession(session); + request.setParameter("type", "external"); + request.setParameter("dir", "theme"); + MockHttpServletResponse response = new MockHttpServletResponse(); + invoker.invoke(request, response); + assertEquals(200, response.getStatus()); + + + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + MvcResult result = mockMvc.perform(get("/srv/api/registries/vocabularies/external.theme.mobility-theme") + .accept("application/xml") + .session(mockHttpSession)) + .andExpect(status().isOk()) + .andReturn(); + + Element thesaurus = Xml.loadString(result.getResponse().getContentAsString(), false); + Element scheme = (Element) thesaurus.getChildren("ConceptScheme", SKOS_NAMESPACE).get(0); + assertEquals( + "https://w3id.org/mobilitydcat-ap/mobility-theme", scheme.getAttributeValue("about", RDF_NAMESPACE)); + assertEquals( + "Mobility Theme", scheme.getChildText("title", NAMESPACE_DCT)); + + List concepts = thesaurus.getChildren("Concept", SKOS_NAMESPACE); + assertEquals(123, concepts.size()); + } } diff --git a/services/src/test/resources/mobility-theme.owl b/services/src/test/resources/mobility-theme.owl new file mode 100644 index 00000000000..2c661d26f2e --- /dev/null +++ b/services/src/test/resources/mobility-theme.owl @@ -0,0 +1,1979 @@ + + + + + Mario Scrocca (Cefriel) + Peter Lubrich (BASt) + Controlled vocabulary for the classification of the theme for a mobility dataset. + + NAPCORE SubWG4.4 + Mobility Theme + Published Controlled Vocabulary + 1.0.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Accesibility information for vehicles + + + + + + + + + + + + + Accidents and incidents + + + + + + + + + + + + + Address identifiers + + + + + + + + + + + + Air and space travel + + + + + + + + + + + + + Applicable road user charges and payment methods + + + + + + + + + + + + + Availability of charging points for electric vehicles + + + + + + + + + + + + + Availability of delivery areas + + + + + + + + + + + + + Availability of filling stations + + + + + + + + + + + + + Basic commercial conditions + + + + + + + + + + + + + Basic common standard fares + + + + + + + + + + + + + Bike-hiring Availability + + + + + + + + + + + + + Bike-hiring Stations + + + + + + + + + + + + + Bike-parking locations + + + + + + + + + + + + + Bike sharing Availability + + + + + + + + + + + + + Bike-sharing Locations and stations + + + + + + + + + + + + + Bridge access conditions + + + + + + + + + + + + + Bridge closures and access conditions + + + + + + + + + + + + + Car-hiring Availability + + + + + + + + + + + + + Car parking availability + + + + + + + + + + + + + Car parking locations and conditions + + + + + + + + + + + + + Car-sharing Availability + + + + + + + + + + + + + Car-sharing Locations and stations + + + + + + + + + + + + + Car-sharing Stations + + + + + + + + + + + + + Common fare products + + + + + + + + + + + + + Connection links + + + + + + + + + + + + + Current travel times + + + + + + + + + + + + + + + Cycle network data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Data content category + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Data content sub-category + + + + + + + + + + + + + + Direction of travel on reversible lanes + + + + + + + + + + + + + Disruptions, delays, cancellations + + + + + + + + + + + + + Dynamic overtaking bans on heavy goods vehicles + + + + + + + + + + + + + Dynamic speed limits + + + + + + + + + + + + + + + + + + + + + Dynamic traffic signs and regulations + + + + + + + + + + + + + E-scooter-sharing Availability + + + + + + + + + + + + + E-scooter-sharing Locations and stations + + + + + + + + + + + + + + + Environmental standards for vehicles + + + + + + + + + + + + + Expected delays + + + + + + + + + + + + + Fares + + + + + + + + + + + + + + + + Filling and charging stations + + + + + + + + + + + + + + + Freight and logistics + + + + + + + + + + + + + Freight delivery regulations + + + + + + + + + + + + + + + + + General information for trip-planning + + + + + + + + + + + + + Geometry + + + + + + + + + + + + + Gradients + + + + + + + + + + + + + Hours of operation + + + + + + + + + + + + + Identification of tolled roads + + + + + + + + + + + + + Junctions + + + + + + + + + + + + + Lane closures and access conditions + + + + + + + + + + + + + Location and conditions of charging points + + + + + + + + + + + + + Location and conditions of filling stations + + + + + + + + + + + + + Location and length of queues + + + + + + + + + + + + + Location of delivery areas + + + + + + + + + + + + + Location of tolling stations + + + + + + + + + + + + + Locations and stations + + + + + + + + + + + + + Long-term road works + + + + + + + + + + + + + Network closures/diversions + + + + + + + + + + + + + Network detailed attributes + + + + + + + + + + + + + Network geometry and lane character + + + + + + + + + + + + + Network topology and routes/lines + + + + + + + + + + + + + Number of lanes + + + + + + + + + + + + + Operational Calendar + + + + + + + + + + + + Other + + + + + + + + + + + + + Other access restrictions and traffic regulations + + + + + + + + + + + + + Other static traffic signs + + + + + + + + + + + + + Other temporary traffic management measures or plans + + + + + + + + + + + + + Other traffic regulations + + + + + + + + + + + + + Parameters needed to calculate costs + + + + + + + + + + + + + Parameters needed to calculate environmental factors + + + + + + + + + + + + + Park and Ride stops + + + + + + + + + + + + + + + + + + + + Parking, service and rest area information + + + + + + + + + + + + + Passenger classes + + + + + + + + + + + + + Payment methods + + + + + + + + + + + + + Payment methods for tolls + + + + + + + + + + + + + Pedestrian accessibility facilities + + + + + + + + + + + + + + Pedestrian network data + + + + + + + + + + + + + Pedestrian network geometry + + + + + + + + + + + + + Permanent access restrictions + + + + + + + + + + + + + Planned interchanges between scheduled services + + + + + + + + + + + + + Points of interest + + + + + + + + + + + + + Poor road conditions + + + + + + + + + + + + + Predicted travel times + + + + + + + + + + + + + Provider data + + + + + + + + + + + + + + + + + + + Public transport non-scheduled transport + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Public transport scheduled transport + + + + + + + + + + + + + Purchase information + + + + + + + + + + + + + Real-time estimated departure and arrival times + + + + + + + + + + + + + + + + + + + + Real-time traffic data + + + + + + + + + + + + + Reservation and purchase options + + + + + + + + + + + + + Road classification + + + + + + + + + + + + + Road closures and access conditions + + + + + + + + + + + + + + + Road events and conditions + + + + + + + + + + + + + Road weather conditions + + + + + + + + + + + + + Road width + + + + + + + + + + + + + + Road work information + + + + + + + + + + + + + Service and rest area availability + + + + + + + + + + + + + Service and rest area locations and conditions + + + + + + + + + + + + + Service areas and service times + + + + + + + + + + + + + + + + + + + + + + + + Sharing and Hiring Services + + + + + + + + + + + + + Short-term road works + + + + + + + + + + + + + Special Fare Products + + + + + + + + + + + + + Speed + + + + + + + + + + + + + Speed limits + + + + + + + + + + + + + + + + + + Static road network data + + + + + + + + + + + + + + + + + + + Static traffic signs and regulations + + + + + + + + + + + + + Stop facilities accessibility and paths within facility + + + + + + + + + + + + + Stop facilities geometry and map layout + + + + + + + + + + + + + Stop facilities location and features + + + + + + + + + + + + + Stop facilities status of features + + + + + + + + + + + + + Timetables static + + + + + + + + + + + + + + + + Toll information + + + + + + + + + + + + + Topographic places + + + + + + + + + + + + + Traffic circulation plans + + + + + + + + + + + + + Traffic data at border crossings to third countries + + + + + + + + + + + + + Traffic volume + + + + + + + + + + + + + Transport operators + + + + + + + + + + + + + Truck parking availability + + + + + + + + + + + + + Truck parking locations and conditions + + + + + + + + + + + + + Tunnel access conditions + + + + + + + + + + + + + Tunnel closures and access conditions + + + + + + + + + + + + + Vehicle details + + + + + + + + + + + + + Waiting time at border crossings to non-EU Member States + + + + + + + + + + + + Waterways and water bodies + + + + + + + + diff --git a/web/src/main/webapp/xslt/services/thesaurus/owl-to-skos.xsl b/web/src/main/webapp/xslt/services/thesaurus/owl-to-skos.xsl new file mode 100644 index 00000000000..449ee6d70d5 --- /dev/null +++ b/web/src/main/webapp/xslt/services/thesaurus/owl-to-skos.xsl @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +