diff --git a/pom.xml b/pom.xml index c089a038..68242d54 100644 --- a/pom.xml +++ b/pom.xml @@ -91,6 +91,7 @@ 3.9.0 3.9.0 7.0.99 + 3.1.1-SNAPSHOT @@ -108,6 +109,12 @@ ${appbase.version} + + + com.epimorphics + lib + ${lib.version} + org.apache.jena diff --git a/src/main/java/com/epimorphics/registry/core/Registry.java b/src/main/java/com/epimorphics/registry/core/Registry.java index a59ffee1..50726d13 100644 --- a/src/main/java/com/epimorphics/registry/core/Registry.java +++ b/src/main/java/com/epimorphics/registry/core/Registry.java @@ -21,46 +21,15 @@ package com.epimorphics.registry.core; -import static com.epimorphics.registry.core.Status.LIFECYCLE_REGISTER; -import static javax.ws.rs.core.Response.Status.BAD_REQUEST; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response; - -import org.apache.jena.rdf.model.Model; -import org.apache.jena.rdf.model.ModelFactory; -import org.apache.jena.util.FileUtils; -import org.apache.jena.vocabulary.RDF; -import org.apache.shiro.SecurityUtils; -import org.apache.shiro.UnavailableSecurityManagerException; -import org.glassfish.jersey.uri.UriComponent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.epimorphics.appbase.core.App; -import com.epimorphics.appbase.core.ComponentBase; -import com.epimorphics.appbase.core.GenericConfig; import com.epimorphics.appbase.core.Shutdown; -import com.epimorphics.appbase.core.Startup; +import com.epimorphics.appbase.core.*; import com.epimorphics.appbase.templates.VelocityRender; import com.epimorphics.appbase.webapi.WebApiException; import com.epimorphics.rdfutil.ModelWrapper; import com.epimorphics.registry.core.Command.Operation; import com.epimorphics.registry.core.ForwardingRecord.Type; -import com.epimorphics.registry.message.LocalMessagingService; -import com.epimorphics.registry.message.Message; -import com.epimorphics.registry.message.MessagingService; -import com.epimorphics.registry.message.ProcessIfChanges; -import com.epimorphics.registry.message.RequestLogger; +import com.epimorphics.registry.language.LanguageManager; +import com.epimorphics.registry.message.*; import com.epimorphics.registry.security.UserInfo; import com.epimorphics.registry.security.UserStore; import com.epimorphics.registry.store.BackupService; @@ -72,6 +41,29 @@ import com.epimorphics.registry.webapi.facets.FacetService; import com.epimorphics.util.EpiException; import com.epimorphics.util.FileUtil; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.util.FileUtils; +import org.apache.jena.vocabulary.RDF; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.UnavailableSecurityManagerException; +import org.glassfish.jersey.uri.UriComponent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.epimorphics.registry.core.Status.LIFECYCLE_REGISTER; +import static javax.ws.rs.core.Response.Status.BAD_REQUEST; /** * This the primary configuration point for the Registry. @@ -125,6 +117,7 @@ public class Registry extends ComponentBase implements Startup, Shutdown { protected GenericConfig configExtensions; protected RequestLogger requestLogger; protected boolean cacheRegisters = false; + protected LanguageManager languageManager = new LanguageManager.Default(); public void setBaseUri(String uri) { baseURI = uri; @@ -224,6 +217,14 @@ public void setCacheRegisters(boolean cacheRegisters) { this.cacheRegisters = cacheRegisters; } + public void setLanguageManager(LanguageManager languageManager) { + this.languageManager = languageManager; + } + + public LanguageManager getLanguageManager() { + return languageManager; + } + @Override public void startup(App app) { super.startup(app); @@ -238,7 +239,14 @@ public void startup(App app) { registry = this; // Assumes singleton registry // Initialize the registry RDF store from the bootstrap registers if needed - Description root = store.getDescription(getBaseURI() + "/"); + Description root; + store.beginSafeRead(); + try { + root = store.getDescription(getBaseURI() + "/"); + } finally { + store.endSafeRead(); + } + if (root == null) { // Blank store, need to install a bootstrap root registers for(String bootSrc : bootFile.split("\\|")) { diff --git a/src/main/java/com/epimorphics/registry/language/Language.java b/src/main/java/com/epimorphics/registry/language/Language.java new file mode 100644 index 00000000..4e6b4055 --- /dev/null +++ b/src/main/java/com/epimorphics/registry/language/Language.java @@ -0,0 +1,29 @@ +package com.epimorphics.registry.language; + +/** + * Representation of a supported language. + */ +interface Language { + /** + * @return The ISO 639-1 language code. + */ + String getCode(); + + /** + * @return The user friendly label, in the corresponding language (if available). + */ + String getLabel(); + + class Base implements Language { + private final String code; + private final String label; + + Base(String code, String label) { + this.code = code; + this.label = label; + } + + @Override public String getCode() { return code; } + @Override public String getLabel() { return label; } + } +} diff --git a/src/main/java/com/epimorphics/registry/language/LanguageConfig.java b/src/main/java/com/epimorphics/registry/language/LanguageConfig.java new file mode 100644 index 00000000..0a8b52aa --- /dev/null +++ b/src/main/java/com/epimorphics/registry/language/LanguageConfig.java @@ -0,0 +1,13 @@ +package com.epimorphics.registry.language; + +import java.util.List; + +/** + * Maintains the state of the registry's supported languages. + */ +interface LanguageConfig { + /** + * @return The list of languages supported by the registry. + */ + List getLanguages(); +} diff --git a/src/main/java/com/epimorphics/registry/language/LanguageManager.java b/src/main/java/com/epimorphics/registry/language/LanguageManager.java new file mode 100644 index 00000000..7457ed33 --- /dev/null +++ b/src/main/java/com/epimorphics/registry/language/LanguageManager.java @@ -0,0 +1,32 @@ +package com.epimorphics.registry.language; + +import java.util.Collections; +import java.util.List; + +/** + * Multilingual registry support. + */ +public interface LanguageManager { + /** + * Determine whether the registry is configured to support multiple languages. + * @return True if and only if multiple languages are supported. + */ + Boolean isMultilingual(); + + /** + * @return All of the supported languages. + */ + List getLanguages(); + + /** + * Determines whether to use cookies to store users' language preference. + * @return True if and only if cookies should be used. Otherwise, false. + */ + Boolean getUseCookies(); + + class Default implements LanguageManager { + @Override public Boolean isMultilingual() { return false; } + @Override public List getLanguages() { return Collections.emptyList(); } + @Override public Boolean getUseCookies() { return false; } + } +} \ No newline at end of file diff --git a/src/main/java/com/epimorphics/registry/language/LanguageRegister.java b/src/main/java/com/epimorphics/registry/language/LanguageRegister.java new file mode 100644 index 00000000..e9cb21f1 --- /dev/null +++ b/src/main/java/com/epimorphics/registry/language/LanguageRegister.java @@ -0,0 +1,97 @@ +package com.epimorphics.registry.language; + +import com.epimorphics.appbase.core.App; +import com.epimorphics.appbase.core.Startup; +import com.epimorphics.registry.core.Description; +import com.epimorphics.registry.core.Register; +import com.epimorphics.registry.core.Registry; +import com.epimorphics.registry.message.MessagingService; +import com.epimorphics.registry.message.ProcessIfChanges; +import com.epimorphics.registry.store.RegisterEntryInfo; +import com.epimorphics.registry.store.StoreAPI; +import com.epimorphics.registry.vocab.RegistryVocab; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.Statement; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +public class LanguageRegister implements LanguageConfig, Startup { + private static final String REGISTER = "/system/language"; + + private Logger log = LoggerFactory.getLogger(LanguageRegister.class); + private List languages = new ArrayList<>(); + + @Override public void startup(App app) { + Registry reg = app.getA(Registry.class); + initLanguages(reg); + + String register = reg.getBaseURI() + REGISTER; + MessagingService msgSvc = reg.getMessagingService(); + MessagingService.Process onMonitorChange = new ProcessIfChanges(msg -> initLanguages(reg), register); + + msgSvc.processMessages(onMonitorChange); + } + + public synchronized void initLanguages(Registry reg) { + List nextLanguages = new ArrayList<>(); + + String uri = reg.getBaseURI() + REGISTER; + StoreAPI store = reg.getStore(); + store.beginSafeRead(); + + try { + Description desc = store.getDescription(uri); + if (desc instanceof Register) { + Register register = desc.asRegister(); + List members = register.getMembers(); + members.forEach(entry -> addLanguage(entry, store, nextLanguages)); + this.languages = nextLanguages; + } else { + log.warn("System register " + uri + " does not exist - unable to configure languages."); + } + } finally { + store.endSafeRead(); + } + } + + private void addLanguage(RegisterEntryInfo entry, StoreAPI store, List languages) { + Resource root = store.getDescription(entry.getEntityURI()).getRoot(); + if (root.hasProperty(RDF.type, RegistryVocab.Language)) { + Statement langStmt = root.getProperty(RegistryVocab.languageCode); + if (langStmt != null) { + String lang = langStmt.getObject().asLiteral().getLexicalForm(); + String label = getLabel(root, lang); + languages.add(new Language.Base(lang, label)); + log.info("Registered language: " + label + " (" + lang + ")"); + } else { + log.warn("Unable to add language entry " + entry.getItemURI() + ": Resource must specify a dbo:languageCode value."); + } + } else { + log.warn("Unable to add language entry " + entry.getItemURI() + ": Resource must have a rdf:type value of dbo:Language."); + } + + } + + private String getLabel(Resource root, String lang) { + Statement nativeLabel = root.getProperty(RDFS.label, lang); + if (nativeLabel != null) { + return nativeLabel.getObject().asLiteral().getLexicalForm(); + } + + Statement anyLabel = root.getProperty(RDFS.label); + if (anyLabel != null) { + return anyLabel.getObject().asLiteral().getLexicalForm(); + } + + return lang; + } + + public List getLanguages() { + return languages; + } +} diff --git a/src/main/java/com/epimorphics/registry/language/MultiLanguageManager.java b/src/main/java/com/epimorphics/registry/language/MultiLanguageManager.java new file mode 100644 index 00000000..4c7a0f38 --- /dev/null +++ b/src/main/java/com/epimorphics/registry/language/MultiLanguageManager.java @@ -0,0 +1,28 @@ +package com.epimorphics.registry.language; + +import java.util.List; + +public class MultiLanguageManager implements LanguageManager { + private Boolean useCookies = false; + private LanguageConfig config; + + public void setUseCookies(Boolean useCookies) { + this.useCookies = useCookies; + } + + public void setConfig(LanguageConfig config) { + this.config = config; + } + + public Boolean isMultilingual() { + return getLanguages().size() > 1; + } + + public List getLanguages() { + return config.getLanguages(); + } + + public Boolean getUseCookies() { + return useCookies; + } +} diff --git a/src/main/java/com/epimorphics/registry/vocab/RegistryVocab.java b/src/main/java/com/epimorphics/registry/vocab/RegistryVocab.java index f5b960d6..e07e6f5e 100644 --- a/src/main/java/com/epimorphics/registry/vocab/RegistryVocab.java +++ b/src/main/java/com/epimorphics/registry/vocab/RegistryVocab.java @@ -86,6 +86,8 @@ public class RegistryVocab { * of type is available.

*/ public static final ObjectProperty itemClass = m_model.createObjectProperty( "http://purl.org/linked-data/registry#itemClass" ); + + public static final ObjectProperty languageCode = m_model.createObjectProperty("http://dbpedia.org/ontology/languageCode"); /**

The manager of the register, may be a person (foaf:Person) or an organization * (org:Organization). Operates the register on behalf of the owner, makes day @@ -203,6 +205,8 @@ public class RegistryVocab { * which traverse the register hierarchy such as entity search will also be forwarded

*/ public static final OntClass FederatedRegister = m_model.createClass( "http://purl.org/linked-data/registry#FederatedRegister" ); + + public static final OntClass Language = m_model.createClass("http://dbpedia.org/ontology/Language"); /**

A registerable entity which simply forwards all requests to the delegation * target.

diff --git a/src/main/java/com/epimorphics/registry/webapi/RequestProcessor.java b/src/main/java/com/epimorphics/registry/webapi/RequestProcessor.java index 591f0db9..b9ee91ee 100644 --- a/src/main/java/com/epimorphics/registry/webapi/RequestProcessor.java +++ b/src/main/java/com/epimorphics/registry/webapi/RequestProcessor.java @@ -23,30 +23,27 @@ package com.epimorphics.registry.webapi; -import static com.epimorphics.webapi.marshalling.RDFXMLMarshaller.FULL_MIME_RDFXML; -import static com.epimorphics.webapi.marshalling.RDFXMLMarshaller.MIME_RDFXML; - -import java.io.InputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.List; - -import javax.servlet.ServletContext; -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.*; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.ResponseBuilder; -import javax.ws.rs.core.Response.Status; -import javax.ws.rs.core.StreamingOutput; -import javax.ws.rs.core.UriInfo; - +import com.epimorphics.appbase.core.AppConfig; +import com.epimorphics.appbase.templates.VelocityRender; +import com.epimorphics.appbase.webapi.BaseEndpoint; +import com.epimorphics.appbase.webapi.WebApiException; +import com.epimorphics.registry.commands.CommandUpdate; +import com.epimorphics.registry.core.*; +import com.epimorphics.registry.core.Command.Operation; +import com.epimorphics.registry.core.ForwardingRecord.Type; +import com.epimorphics.registry.csv.CSVPayloadRead; +import com.epimorphics.registry.csv.RDFCSVUtil; +import com.epimorphics.registry.language.LanguageManager; +import com.epimorphics.registry.security.UserInfo; +import com.epimorphics.registry.util.JSONLDSupport; +import com.epimorphics.registry.util.PATCH; +import com.epimorphics.registry.util.UiForm; import com.epimorphics.registry.vocab.RegistryVocab; -import org.apache.jena.rdf.model.*; +import com.epimorphics.util.NameUtils; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.rdf.model.Resource; import org.apache.jena.util.FileManager; import org.apache.jena.util.FileUtils; import org.apache.jena.vocabulary.RDF; @@ -61,25 +58,24 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.epimorphics.appbase.core.AppConfig; -import com.epimorphics.appbase.templates.VelocityRender; -import com.epimorphics.appbase.webapi.BaseEndpoint; -import com.epimorphics.appbase.webapi.WebApiException; -import com.epimorphics.registry.commands.CommandUpdate; -import com.epimorphics.registry.core.Command; -import com.epimorphics.registry.core.Command.Operation; -import com.epimorphics.registry.core.ForwardingRecord; -import com.epimorphics.registry.core.ForwardingRecord.Type; -import com.epimorphics.registry.core.ForwardingService; -import com.epimorphics.registry.core.MatchResult; -import com.epimorphics.registry.core.Registry; -import com.epimorphics.registry.csv.CSVPayloadRead; -import com.epimorphics.registry.csv.RDFCSVUtil; -import com.epimorphics.registry.security.UserInfo; -import com.epimorphics.registry.util.JSONLDSupport; -import com.epimorphics.registry.util.PATCH; -import com.epimorphics.registry.util.UiForm; -import com.epimorphics.util.NameUtils; +import javax.servlet.ServletContext; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.*; +import javax.ws.rs.core.*; +import javax.ws.rs.core.Response.ResponseBuilder; +import javax.ws.rs.core.Response.Status; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static com.epimorphics.webapi.marshalling.RDFXMLMarshaller.FULL_MIME_RDFXML; +import static com.epimorphics.webapi.marshalling.RDFXMLMarshaller.MIME_RDFXML; +import static javax.ws.rs.core.Cookie.DEFAULT_VERSION; +import static javax.ws.rs.core.NewCookie.DEFAULT_MAX_AGE; /** * Filter all requests as possible register API requests. @@ -96,6 +92,7 @@ public class RequestProcessor extends BaseEndpoint { public static final String VARY_HEADER = "Vary"; public static final String UI_PATH = "ui"; private static final String SYSTEM_QUERY = "system/query"; + private static final String LANGUAGE_COOKIE = "registry-pref-lang"; @GET @Produces("text/html") @@ -161,11 +158,45 @@ private Response readAsRDF(PassThroughResult ptr, String mime, String ext) { return builder.build(); } - public static Response render(String template, UriInfo uriInfo, ServletContext context, HttpServletRequest request, Object...params) { - String language = request.getLocale().getLanguage(); - if (language.isEmpty()) { - language = "en"; + private static String getRequestLanguage(LanguageManager languageManager, HttpServletRequest request) { + String param = request.getParameter("lang"); + if (param != null && !param.isEmpty()) { + return param; + } + + if (languageManager.getUseCookies()) { + Cookie cookie = Arrays.stream(request.getCookies()) + .filter(c -> c.getName().equals(LANGUAGE_COOKIE)) + .findFirst() + .orElse(null); + if (cookie != null) { + return cookie.getValue(); + } + } + + String header = request.getLocale().getLanguage(); + if (header != null && !header.isEmpty()) { + return header; + } + + return "en"; + } + + private static void setLanguageCookie(LanguageManager languageManager, ResponseBuilder response, HttpServletRequest request) { + if (languageManager.getUseCookies()) { + String param = request.getParameter("lang"); + if (param != null && !param.isEmpty()) { + NewCookie cookie = new NewCookie(LANGUAGE_COOKIE, param, "/", null, DEFAULT_VERSION, null, DEFAULT_MAX_AGE, null, false, false); + response.cookie(cookie); + } } + } + + public static Response render(String template, UriInfo uriInfo, ServletContext context, HttpServletRequest request, Object...params) { + Registry reg = Registry.get(); + LanguageManager languageManager = reg.getLanguageManager(); + + String language = getRequestLanguage(languageManager, request); Object[] fullParams = new Object[params.length + 10]; int i = 0; while (i < params.length) { @@ -173,7 +204,7 @@ public static Response render(String template, UriInfo uriInfo, ServletContext c i++; } fullParams[i++] = "registry"; - fullParams[i++] = Registry.get(); + fullParams[i++] = reg; fullParams[i++] = "subject"; fullParams[i++] = SecurityUtils.getSubject(); fullParams[i++] = "requestor"; @@ -190,6 +221,9 @@ public static Response render(String template, UriInfo uriInfo, ServletContext c if (SecurityUtils.getSubject().isAuthenticated()) { builder.header("Cache-control", "no-cache"); } + + setLanguageCookie(languageManager, builder, request); + return builder.entity(out).build(); } diff --git a/src/main/vocabs/registryVocab.ttl b/src/main/vocabs/registryVocab.ttl index aa3dd580..0048fb41 100644 --- a/src/main/vocabs/registryVocab.ttl +++ b/src/main/vocabs/registryVocab.ttl @@ -4,6 +4,7 @@ @prefix rdfs: . @prefix owl: . @prefix xsd: . +@prefix dbo: . @prefix dct: . @prefix dc: . @prefix foaf: . @@ -534,3 +535,18 @@ reg:sourceDataset a owl:ObjectProperty; rdfs:domain void:Linkset; rdfs:range void:Dataset; . + +# -- Internationalisation -------------------------------------------------- + +dbo:Language a owl:Class; + rdfs:label "Language"; + rdfs:comment "A language that is supported by a multilingual registry."; + . + +dbo:languageCode a owl:DatatypeProperty; + rdfs:label "Language code"; + rdfs:comment "The two-character ISO 639-1 language code."; + rdfs:domain dbo:Language; + rdfs:range xsd:string; + . +