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;
+ .
+