diff --git a/src/main/java/cz/cvut/kbss/termit/config/ServiceConfig.java b/src/main/java/cz/cvut/kbss/termit/config/ServiceConfig.java index 246059711..270d7f910 100644 --- a/src/main/java/cz/cvut/kbss/termit/config/ServiceConfig.java +++ b/src/main/java/cz/cvut/kbss/termit/config/ServiceConfig.java @@ -14,14 +14,20 @@ import com.fasterxml.jackson.databind.ObjectMapper; import cz.cvut.kbss.termit.aspect.ChangeTrackingAspect; import cz.cvut.kbss.termit.aspect.VocabularyContentModificationAspect; +import cz.cvut.kbss.termit.exception.ResourceNotFoundException; import org.apache.hc.client5.http.classic.HttpClient; import org.apache.hc.client5.http.impl.DefaultRedirectStrategy; import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.tika.utils.StringUtils; import org.aspectj.lang.Aspects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.converter.ResourceHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; @@ -37,6 +43,8 @@ @Configuration public class ServiceConfig { + private static final Logger LOG = LoggerFactory.getLogger(ServiceConfig.class); + @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); @@ -71,12 +79,27 @@ public LocalValidatorFactoryBean validatorFactoryBean() { } @Bean("termTypesLanguage") - public ClassPathResource termTypesLanguageFile() { + public Resource termTypesLanguageFile(cz.cvut.kbss.termit.util.Configuration config) { + if (!StringUtils.isBlank(config.getLanguage().getTypes().getSource())) { + return createFileSystemResource(config.getLanguage().getTypes().getSource(), "types"); + } return new ClassPathResource("languages/types.ttl"); } + private Resource createFileSystemResource(String path, String type) { + final FileSystemResource source = new FileSystemResource(path); + if (!source.exists()) { + throw new ResourceNotFoundException(type + " language file '" + path + "' not found."); + } + LOG.info("Will load term {} from '{}'.", type, path); + return source; + } + @Bean("termStatesLanguage") - public ClassPathResource termStatesLanguageFile() { + public Resource termStatesLanguageFile(cz.cvut.kbss.termit.util.Configuration config) { + if (!StringUtils.isBlank(config.getLanguage().getStates().getSource())) { + return createFileSystemResource(config.getLanguage().getStates().getSource(), "states"); + } return new ClassPathResource("languages/states.ttl"); } diff --git a/src/main/java/cz/cvut/kbss/termit/service/export/SKOSVocabularyExporter.java b/src/main/java/cz/cvut/kbss/termit/service/export/SKOSVocabularyExporter.java index d95502437..0d60afa8b 100644 --- a/src/main/java/cz/cvut/kbss/termit/service/export/SKOSVocabularyExporter.java +++ b/src/main/java/cz/cvut/kbss/termit/service/export/SKOSVocabularyExporter.java @@ -7,6 +7,7 @@ import cz.cvut.kbss.termit.util.TypeAwareResource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/cz/cvut/kbss/termit/service/language/TermStateLanguageService.java b/src/main/java/cz/cvut/kbss/termit/service/language/TermStateLanguageService.java index 3bd1fa53b..2b1742825 100644 --- a/src/main/java/cz/cvut/kbss/termit/service/language/TermStateLanguageService.java +++ b/src/main/java/cz/cvut/kbss/termit/service/language/TermStateLanguageService.java @@ -5,18 +5,17 @@ import cz.cvut.kbss.termit.util.Utils; import cz.cvut.kbss.termit.util.Vocabulary; import org.eclipse.rdf4j.model.Model; -import org.eclipse.rdf4j.model.Resource; import org.eclipse.rdf4j.model.ValueFactory; import org.eclipse.rdf4j.model.impl.SimpleValueFactory; import org.eclipse.rdf4j.model.vocabulary.RDF; -import org.eclipse.rdf4j.model.vocabulary.RDFS; +import org.eclipse.rdf4j.model.vocabulary.SKOS; import org.eclipse.rdf4j.rio.RDFFormat; import org.eclipse.rdf4j.rio.RDFParseException; import org.eclipse.rdf4j.rio.Rio; import org.eclipse.rdf4j.rio.UnsupportedRDFormatException; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; import java.io.IOException; @@ -33,11 +32,11 @@ @Service public class TermStateLanguageService { - private final ClassPathResource termStatesLanguageTtl; + private final Resource termStatesLanguageTtl; private List cache; - public TermStateLanguageService(@Qualifier("termStatesLanguage") ClassPathResource termStatesLanguageTtl) { + public TermStateLanguageService(@Qualifier("termStatesLanguage") Resource termStatesLanguageTtl) { this.termStatesLanguageTtl = termStatesLanguageTtl; } @@ -60,18 +59,19 @@ private List loadTermStates() { final Model model = Rio.parse(termStatesLanguageTtl.getInputStream(), RDFFormat.TURTLE); return model.filter(null, RDF.TYPE, vf.createIRI(Vocabulary.s_c_stav_pojmu)) .stream().map(s -> { - final Resource state = s.getSubject(); + final org.eclipse.rdf4j.model.Resource state = s.getSubject(); final RdfsResource res = new RdfsResource(); res.setUri(URI.create(state.stringValue())); final Model statements = model.filter(state, null, null); res.setTypes(statements.filter(state, RDF.TYPE, null).stream() .map(ts -> ts.getObject().stringValue()).collect(Collectors.toSet())); - res.setLabel(Utils.resolveTranslations(state, RDFS.LABEL, statements)); - res.setComment(Utils.resolveTranslations(state, RDFS.COMMENT, statements)); + res.setLabel(Utils.resolveTranslations(state, SKOS.PREF_LABEL, statements)); + res.setComment(Utils.resolveTranslations(state, SKOS.SCOPE_NOTE, statements)); return res; }).collect(Collectors.toList()); } catch (IOException | RDFParseException | UnsupportedRDFormatException e) { - throw new LanguageRetrievalException("Unable to load term states language from file " + termStatesLanguageTtl.getPath(), e); + throw new LanguageRetrievalException( + "Unable to load term states language from file " + termStatesLanguageTtl.getFilename(), e); } } diff --git a/src/main/java/cz/cvut/kbss/termit/service/language/UfoTermTypesService.java b/src/main/java/cz/cvut/kbss/termit/service/language/UfoTermTypesService.java index 35b411605..8e9fa29fb 100644 --- a/src/main/java/cz/cvut/kbss/termit/service/language/UfoTermTypesService.java +++ b/src/main/java/cz/cvut/kbss/termit/service/language/UfoTermTypesService.java @@ -29,12 +29,13 @@ import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; import java.io.IOException; import java.net.URI; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; /** @@ -45,12 +46,12 @@ @Service public class UfoTermTypesService { - private final ClassPathResource languageTtlUrl; + private final Resource languageTtlUrl; private List cache; @Autowired - public UfoTermTypesService(@Qualifier("termTypesLanguage") ClassPathResource languageTtlUrl) { + public UfoTermTypesService(@Qualifier("termTypesLanguage") Resource languageTtlUrl) { this.languageTtlUrl = languageTtlUrl; } @@ -81,16 +82,21 @@ private List loadTermTypes() { final org.eclipse.rdf4j.model.Resource type = s.getSubject(); final Term term = new Term(URI.create(type.stringValue())); final Model statements = model.filter(type, null, null); - term.setLabel(Utils.resolveTranslations(type, org.eclipse.rdf4j.model.vocabulary.RDFS.LABEL, statements)); - term.setDescription(Utils.resolveTranslations(type, org.eclipse.rdf4j.model.vocabulary.RDFS.COMMENT, statements)); - term.setSubTerms(statements.filter(type, SKOS.NARROWER, null).stream() + term.setLabel(Utils.resolveTranslations(type, SKOS.PREF_LABEL, statements)); + term.setDescription(Utils.resolveTranslations(type, SKOS.SCOPE_NOTE, statements)); + final Set subTerms = statements.filter(type, SKOS.NARROWER, null).stream() .filter(st -> st.getObject().isIRI()) - .map(st -> new TermInfo(URI.create(st.getObject().stringValue()))) - .collect(Collectors.toSet())); + .map(st -> URI.create(st.getObject().stringValue())) + .collect(Collectors.toSet()); + model.filter(null, SKOS.BROADER, type).stream() + .filter(st -> st.getSubject().isIRI()) + .map(st -> URI.create(st.getSubject().stringValue())) + .forEach(subTerms::add); + term.setSubTerms(subTerms.stream().map(TermInfo::new).collect(Collectors.toSet())); return term; }).collect(Collectors.toList()); } catch (IOException | RDFParseException | UnsupportedRDFormatException e) { - throw new LanguageRetrievalException("Unable to load term types from file " + languageTtlUrl.getPath(), e); + throw new LanguageRetrievalException("Unable to load term types from file " + languageTtlUrl.getFilename(), e); } } } diff --git a/src/main/java/cz/cvut/kbss/termit/util/Configuration.java b/src/main/java/cz/cvut/kbss/termit/util/Configuration.java index c38490b97..264b36b02 100644 --- a/src/main/java/cz/cvut/kbss/termit/util/Configuration.java +++ b/src/main/java/cz/cvut/kbss/termit/util/Configuration.java @@ -66,6 +66,7 @@ public class Configuration { private ACL acl = new ACL(); private Mail mail = new Mail(); private Security security = new Security(); + private Language language = new Language(); public String getUrl() { return url; @@ -219,6 +220,15 @@ public void setSecurity(Security security) { this.security = security; } + public Language getLanguage() { + return language; + } + + public void setLanguage(Language language) { + this.language = language; + } + + public static class Persistence { /** * OntoDriver class for the repository. @@ -763,7 +773,7 @@ public enum ProviderType { /** * Claim in the authentication token provided by the OIDC service containing roles mapped to TermIt user roles. - * + *

* Supports nested objects via dot notation. */ private String roleClaim = "realm_access.roles"; @@ -784,4 +794,51 @@ public void setRoleClaim(String roleClaim) { this.roleClaim = roleClaim; } } + + public static class Language { + + /** + * Path to a file containing definition of the language of types terms can be classified with. + *

+ * The file must be in Turtle format. The term definitions must use SKOS terminology for attributes (prefLabel, + * scopeNote and broader/narrower). + */ + private LanguageSource types = new LanguageSource(); + + /** + * Path to a file containing definition of the language of states terms can be in with. The file must be in + * Turtle format. The term definitions must use SKOS terminology for attributes (prefLabel, scopeNote and + * broader/narrower). + */ + private LanguageSource states = new LanguageSource(); + + public LanguageSource getTypes() { + return types; + } + + public void setTypes(LanguageSource types) { + this.types = types; + } + + public LanguageSource getStates() { + return states; + } + + public void setStates(LanguageSource states) { + this.states = states; + } + + public static class LanguageSource { + + private String source; + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + } + } } diff --git a/src/main/resources/languages/states.ttl b/src/main/resources/languages/states.ttl index 421692ea3..78e20f347 100644 --- a/src/main/resources/languages/states.ttl +++ b/src/main/resources/languages/states.ttl @@ -5,13 +5,13 @@ @prefix termit-pojem: . termit-pojem:navrhovaný-pojem a pdp:stav-pojmu , termit-pojem:úvodní-stav-pojmu ; - rdfs:label "Navrhovaný pojem"@cs , "Proposed term"@en ; - rdfs:comment "Pojem, který ještě nebyl schválen k publikaci."@cs , "A proposed term that has not been confirmed for publication, yet."@en . + skos:prefLabel "Navrhovaný pojem"@cs , "Proposed term"@en ; + skos:scopeNote "Pojem, který ještě nebyl schválen k publikaci."@cs , "A proposed term that has not been confirmed for publication, yet."@en . termit-pojem:publikovaný-pojem a pdp:stav-pojmu ; - rdfs:label "Publikovaný pojem"@cs , "Published term"@en ; - rdfs:comment "Pojem, který byl publikován a používá se."@cs , "A published term that is in use."@en . + skos:prefLabel "Publikovaný pojem"@cs , "Published term"@en ; + skos:scopeNote "Pojem, který byl publikován a používá se."@cs , "A published term that is in use."@en . termit-pojem:zrušený-pojem a pdp:stav-pojmu , termit-pojem:koncový-stav-pojmu ; - rdfs:label "Zrušený pojem"@cs , "Cancelled term"@en ; - rdfs:comment "Pojem, který byl zrušen a již by se neměl používat."@cs , "A term that has been cancelled and should not be used anymore."@en . + skos:prefLabel "Zrušený pojem"@cs , "Cancelled term"@en ; + skos:scopeNote "Pojem, který byl zrušen a již by se neměl používat."@cs , "A term that has been cancelled and should not be used anymore."@en . diff --git a/src/main/resources/languages/types.ttl b/src/main/resources/languages/types.ttl index 3edb18f67..c8b3f9184 100644 --- a/src/main/resources/languages/types.ttl +++ b/src/main/resources/languages/types.ttl @@ -1,10 +1,9 @@ @prefix rdf: . -@prefix rdfs: . @prefix skos: . @prefix ufo: . ufo:individual a skos:Concept ; - rdfs:label "Individuál"@cs, + skos:prefLabel "Individuál"@cs, "Individual"@en ; skos:narrower ufo:event, ufo:intrinsic-trope, @@ -12,7 +11,7 @@ ufo:individual a skos:Concept ; ufo:relator . ufo:type a skos:Concept ; - rdfs:label "Typ"@cs, + skos:prefLabel "Typ"@cs, "Type"@en ; skos:narrower ufo:event-type, ufo:intrinsic-trope-type, @@ -20,35 +19,35 @@ ufo:type a skos:Concept ; ufo:relator-type . ufo:event a skos:Concept ; - rdfs:label "Událost"@cs, + skos:prefLabel "Událost"@cs, "Event"@en ; - rdfs:comment "An event, perdurant in the ontological sense. Events do not change its properties over time."@en . + skos:scopeNote "An event, perdurant in the ontological sense. Events do not change its properties over time."@en . ufo:event-type a skos:Concept ; - rdfs:label "Typ události"@cs, + skos:prefLabel "Typ události"@cs, "Event Type"@en . ufo:intrinsic-trope a skos:Concept ; - rdfs:label "Vlastnost"@cs, + skos:prefLabel "Vlastnost"@cs, "Aspect"@en . ufo:intrinsic-trope-type a skos:Concept ; - rdfs:label "Typ vlastnosti"@cs, + skos:prefLabel "Typ vlastnosti"@cs, "Aspect Type"@en . ufo:object a skos:Concept ; - rdfs:label "Objekt"@cs, + skos:prefLabel "Objekt"@cs, "Object"@en ; - rdfs:comment "Object is any identifiable endurant entity existence of which is not directly dependent on an existence of another entity."@en . + skos:scopeNote "Object is any identifiable endurant entity existence of which is not directly dependent on an existence of another entity."@en . ufo:object-type a skos:Concept ; - rdfs:label "Typ objektu"@cs, + skos:prefLabel "Typ objektu"@cs, "Object Type"@en . ufo:relator a skos:Concept ; - rdfs:label "Vztah"@cs, + skos:prefLabel "Vztah"@cs, "Relator"@en . ufo:relator-type a skos:Concept ; - rdfs:label "Typ vztahu"@cs, + skos:prefLabel "Typ vztahu"@cs, "Relation"@en . diff --git a/src/test/java/cz/cvut/kbss/termit/service/language/UfoTermTypesServiceTest.java b/src/test/java/cz/cvut/kbss/termit/service/language/UfoTermTypesServiceTest.java index e89177695..add62bbd9 100644 --- a/src/test/java/cz/cvut/kbss/termit/service/language/UfoTermTypesServiceTest.java +++ b/src/test/java/cz/cvut/kbss/termit/service/language/UfoTermTypesServiceTest.java @@ -1,6 +1,7 @@ package cz.cvut.kbss.termit.service.language; import cz.cvut.kbss.termit.model.Term; +import cz.cvut.kbss.termit.util.Vocabulary; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -11,6 +12,7 @@ import java.io.IOException; import java.net.URL; import java.util.List; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.when; @@ -31,4 +33,28 @@ void getTypesForBasicLanguage() throws IOException { List result = sut.getTypes(); assertEquals(10, result.size()); } + + @Test + void getTypesSupportsTopDownTermHierarchy() throws Exception { + final URL url = ClassLoader.getSystemResource("languages/types.ttl"); + when(languageTtlUrl.getInputStream()).thenReturn(url.openStream()); + List result = sut.getTypes(); + final Optional individual = result.stream() + .filter(t -> t.getUri().toString().equals(Vocabulary.s_c_individual)) + .findAny(); + assertTrue(individual.isPresent()); + assertFalse(individual.get().getSubTerms().isEmpty()); + } + + @Test + void getTypesSupportsBottomUpTermHierarchy() throws Exception { + final URL url = ClassLoader.getSystemResource("languages/bottomUpTypes.ttl"); + when(languageTtlUrl.getInputStream()).thenReturn(url.openStream()); + List result = sut.getTypes(); + final Optional individual = result.stream() + .filter(t -> t.getUri().toString().equals(Vocabulary.s_c_individual)) + .findAny(); + assertTrue(individual.isPresent()); + assertFalse(individual.get().getSubTerms().isEmpty()); + } } diff --git a/src/test/resources/languages/bottomUpTypes.ttl b/src/test/resources/languages/bottomUpTypes.ttl new file mode 100644 index 000000000..694d248b9 --- /dev/null +++ b/src/test/resources/languages/bottomUpTypes.ttl @@ -0,0 +1,53 @@ +@prefix rdf: . +@prefix skos: . +@prefix ufo: . + +ufo:individual a skos:Concept ; + skos:prefLabel "Individuál"@cs, + "Individual"@en . + +ufo:type a skos:Concept ; + skos:prefLabel "Typ"@cs, + "Type"@en . + +ufo:event a skos:Concept ; + skos:prefLabel "Událost"@cs, + "Event"@en ; + skos:scopeNote "An event, perdurant in the ontological sense. Events do not change its properties over time."@en ; + skos:broader ufo:individual . + +ufo:event-type a skos:Concept ; + skos:prefLabel "Typ události"@cs, + "Event Type"@en ; + skos:broader ufo:type . + +ufo:intrinsic-trope a skos:Concept ; + skos:prefLabel "Vlastnost"@cs, + "Aspect"@en ; + skos:broader ufo:individual . + +ufo:intrinsic-trope-type a skos:Concept ; + skos:prefLabel "Typ vlastnosti"@cs, + "Aspect Type"@en ; + skos:broader ufo:type . + +ufo:object a skos:Concept ; + skos:prefLabel "Objekt"@cs, + "Object"@en ; + skos:scopeNote "Object is any identifiable endurant entity existence of which is not directly dependent on an existence of another entity."@en ; + skos:broader ufo:individual . + +ufo:object-type a skos:Concept ; + skos:prefLabel "Typ objektu"@cs, + "Object Type"@en ; + skos:broader ufo:type . + +ufo:relator a skos:Concept ; + skos:prefLabel "Vztah"@cs, + "Relator"@en ; + skos:broader ufo:individual . + +ufo:relator-type a skos:Concept ; + skos:prefLabel "Typ vztahu"@cs, + "Relation"@en ; + skos:broader ufo:type .