allow language system to be loaded from multiple files. closes #817 closes #876

pull/988/head
Justin Richer 2015-11-24 20:33:03 -05:00
parent e255fc1a10
commit 2496dc114c
5 changed files with 117 additions and 27 deletions

View File

@ -16,6 +16,7 @@
*******************************************************************************/ *******************************************************************************/
package org.mitre.openid.connect.config; package org.mitre.openid.connect.config;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
@ -25,6 +26,9 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanCreationException;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import com.google.common.collect.Lists;
import com.google.gson.Gson;
/** /**
@ -55,6 +59,8 @@ public class ConfigurationPropertiesBean {
private boolean forceHttps = false; // by default we just log a warning for HTTPS deployment private boolean forceHttps = false; // by default we just log a warning for HTTPS deployment
private Locale locale = Locale.ENGLISH; // we default to the english translation private Locale locale = Locale.ENGLISH; // we default to the english translation
private List<String> languageNamespaces = Lists.newArrayList("messages");
public boolean dualClient = false; public boolean dualClient = false;
@ -67,7 +73,7 @@ public class ConfigurationPropertiesBean {
* @throws HttpsUrlRequiredException * @throws HttpsUrlRequiredException
*/ */
@PostConstruct @PostConstruct
public void checkForHttps() { public void checkConfigConsistency() {
if (!StringUtils.startsWithIgnoreCase(issuer, "https")) { if (!StringUtils.startsWithIgnoreCase(issuer, "https")) {
if (this.forceHttps) { if (this.forceHttps) {
logger.error("Configured issuer url is not using https scheme. Server will be shut down!"); logger.error("Configured issuer url is not using https scheme. Server will be shut down!");
@ -77,6 +83,10 @@ public class ConfigurationPropertiesBean {
logger.warn("\n\n**\n** WARNING: Configured issuer url is not using https scheme.\n**\n\n"); logger.warn("\n\n**\n** WARNING: Configured issuer url is not using https scheme.\n**\n\n");
} }
} }
if (languageNamespaces == null || languageNamespaces.isEmpty()) {
logger.error("No configured language namespaces! Text rendering will fail!");
}
} }
/** /**
@ -171,7 +181,21 @@ public class ConfigurationPropertiesBean {
this.locale = locale; this.locale = locale;
} }
/** /**
* @return the languageNamespaces
*/
public List<String> getLanguageNamespaces() {
return languageNamespaces;
}
/**
* @param languageNamespaces the languageNamespaces to set
*/
public void setLanguageNamespaces(List<String> languageNamespaces) {
this.languageNamespaces = languageNamespaces;
}
/**
* @return true if dual client is configured, otherwise false * @return true if dual client is configured, otherwise false
*/ */
public boolean isDualClient() { public boolean isDualClient() {
@ -184,4 +208,19 @@ public class ConfigurationPropertiesBean {
public void setDualClient(boolean dualClient) { public void setDualClient(boolean dualClient) {
this.dualClient = dualClient; this.dualClient = dualClient;
} }
/**
* Get the list of namespaces as a JSON string
* @return
*/
public String getLanguageNamespacesString() {
return new Gson().toJson(getLanguageNamespaces());
}
/**
* Get the default namespace (first in the nonempty list)
*/
public String getDefaultLanguageNamespace() {
return getLanguageNamespaces().get(0);
}
} }

View File

@ -63,7 +63,7 @@ public class ConfigurationPropertiesBeanTest {
// leave as default, which is unset/false // leave as default, which is unset/false
try { try {
bean.setIssuer("http://localhost:8080/openid-connect-server/"); bean.setIssuer("http://localhost:8080/openid-connect-server/");
bean.checkForHttps(); bean.checkConfigConsistency();
} catch (BeanCreationException e) { } catch (BeanCreationException e) {
fail("Unexpected BeanCreationException for http issuer with default forceHttps, message:" + e.getMessage()); fail("Unexpected BeanCreationException for http issuer with default forceHttps, message:" + e.getMessage());
} }
@ -77,7 +77,7 @@ public class ConfigurationPropertiesBeanTest {
try { try {
bean.setIssuer("http://localhost:8080/openid-connect-server/"); bean.setIssuer("http://localhost:8080/openid-connect-server/");
bean.setForceHttps(false); bean.setForceHttps(false);
bean.checkForHttps(); bean.checkConfigConsistency();
} catch (BeanCreationException e) { } catch (BeanCreationException e) {
fail("Unexpected BeanCreationException for http issuer with forceHttps=false, message:" + e.getMessage()); fail("Unexpected BeanCreationException for http issuer with forceHttps=false, message:" + e.getMessage());
} }
@ -90,7 +90,7 @@ public class ConfigurationPropertiesBeanTest {
// set to true // set to true
bean.setIssuer("http://localhost:8080/openid-connect-server/"); bean.setIssuer("http://localhost:8080/openid-connect-server/");
bean.setForceHttps(true); bean.setForceHttps(true);
bean.checkForHttps(); bean.checkConfigConsistency();
} }
@Test @Test
@ -100,7 +100,7 @@ public class ConfigurationPropertiesBeanTest {
// leave as default, which is unset/false // leave as default, which is unset/false
try { try {
bean.setIssuer("https://localhost:8080/openid-connect-server/"); bean.setIssuer("https://localhost:8080/openid-connect-server/");
bean.checkForHttps(); bean.checkConfigConsistency();
} catch (BeanCreationException e) { } catch (BeanCreationException e) {
fail("Unexpected BeanCreationException for https issuer with default forceHttps, message:" + e.getMessage()); fail("Unexpected BeanCreationException for https issuer with default forceHttps, message:" + e.getMessage());
} }
@ -114,7 +114,7 @@ public class ConfigurationPropertiesBeanTest {
try { try {
bean.setIssuer("https://localhost:8080/openid-connect-server/"); bean.setIssuer("https://localhost:8080/openid-connect-server/");
bean.setForceHttps(false); bean.setForceHttps(false);
bean.checkForHttps(); bean.checkConfigConsistency();
} catch (BeanCreationException e) { } catch (BeanCreationException e) {
fail("Unexpected BeanCreationException for https issuer with forceHttps=false, message:" + e.getMessage()); fail("Unexpected BeanCreationException for https issuer with forceHttps=false, message:" + e.getMessage());
} }
@ -128,7 +128,7 @@ public class ConfigurationPropertiesBeanTest {
try { try {
bean.setIssuer("https://localhost:8080/openid-connect-server/"); bean.setIssuer("https://localhost:8080/openid-connect-server/");
bean.setForceHttps(true); bean.setForceHttps(true);
bean.checkForHttps(); bean.checkConfigConsistency();
} catch (BeanCreationException e) { } catch (BeanCreationException e) {
fail("Unexpected BeanCreationException for https issuer with forceHttps=true, message:" + e.getMessage()); fail("Unexpected BeanCreationException for https issuer with forceHttps=true, message:" + e.getMessage());
} }

View File

@ -48,7 +48,18 @@
<!-- This property sets the locale for server text --> <!-- This property sets the locale for server text -->
<!-- <property name="locale" value="sv" /> --> <!-- <property name="locale" value="sv" /> -->
<!-- This property sets the set of namespaces for language translation files. The default is "messages". These are checked in the order presented here. -->
<!--
<property name="languageNamespaces">
<list>
<value>foo</value>
<value>bar</value>
<value>messages</value>
</list>
</property>
-->
<!-- This property indicates if a dynamically registered client supports dual flows, such as client_credentials <!-- This property indicates if a dynamically registered client supports dual flows, such as client_credentials
at the same time with authorization_code or implicit --> at the same time with authorization_code or implicit -->
<!-- <property name="dualClient" value="true" /> --> <!-- <property name="dualClient" value="true" /> -->

View File

@ -40,7 +40,12 @@
$.i18n.init({ $.i18n.init({
fallbackLng: "en", fallbackLng: "en",
lng: "${config.locale}", lng: "${config.locale}",
resGetPath: "resources/js/locale/__lng__/messages.json" resGetPath: "resources/js/locale/__lng__/__ns__.json",
ns: {
namespaces: ${config.languageNamespacesString},
defaultNs: '${config.defaultLanguageNamespace}'
},
fallbackNS: ${config.languageNamespacesString}
}); });
moment.locale("${config.locale}"); moment.locale("${config.locale}");
// safely set the title of the application // safely set the title of the application

View File

@ -21,13 +21,16 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.support.AbstractMessageSource; import org.springframework.context.support.AbstractMessageSource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
@ -50,19 +53,22 @@ public class JsonMessageSource extends AbstractMessageSource {
private Locale fallbackLocale = new Locale("en"); // US English is the fallback language private Locale fallbackLocale = new Locale("en"); // US English is the fallback language
private Map<Locale, JsonObject> languageMaps = new HashMap<>(); private Map<Locale, List<JsonObject>> languageMaps = new HashMap<>();
@Autowired
private ConfigurationPropertiesBean config;
@Override @Override
protected MessageFormat resolveCode(String code, Locale locale) { protected MessageFormat resolveCode(String code, Locale locale) {
JsonObject lang = getLanguageMap(locale); List<JsonObject> langs = getLanguageMap(locale);
String value = getValue(code, lang); String value = getValue(code, langs);
if (value == null) { if (value == null) {
// if we haven't found anything, try the default locale // if we haven't found anything, try the default locale
lang = getLanguageMap(fallbackLocale); langs = getLanguageMap(fallbackLocale);
value = getValue(code, lang); value = getValue(code, langs);
} }
if (value == null) { if (value == null) {
@ -76,6 +82,31 @@ public class JsonMessageSource extends AbstractMessageSource {
} }
/** /**
* Get a value from the set of maps, taking the first match in order
* @param code
* @param langs
* @return
*/
private String getValue(String code, List<JsonObject> langs) {
if (langs == null || langs.isEmpty()) {
// no language maps, nothing to look up
return null;
}
for (JsonObject lang : langs) {
String value = getValue(code, lang);
if (value != null) {
// short circuit out of here if we find a match, otherwise keep going through the list
return value;
}
}
// if we didn't find anything return null
return null;
}
/**
* Get a value from a single map
* @param code * @param code
* @param locale * @param locale
* @param lang * @param lang
@ -126,20 +157,24 @@ public class JsonMessageSource extends AbstractMessageSource {
* @param locale * @param locale
* @return * @return
*/ */
private JsonObject getLanguageMap(Locale locale) { private List<JsonObject> getLanguageMap(Locale locale) {
if (!languageMaps.containsKey(locale)) { if (!languageMaps.containsKey(locale)) {
try { try {
String filename = locale.getLanguage() + File.separator + "messages.json"; List<JsonObject> set = new ArrayList<>();
for (String namespace : config.getLanguageNamespaces()) {
Resource r = getBaseDirectory().createRelative(filename); String filename = locale.getLanguage() + File.separator + namespace + ".json";
logger.info("No locale loaded, trying to load from " + r); Resource r = getBaseDirectory().createRelative(filename);
JsonParser parser = new JsonParser(); logger.info("No locale loaded, trying to load from " + r);
JsonObject obj = (JsonObject) parser.parse(new InputStreamReader(r.getInputStream(), "UTF-8"));
JsonParser parser = new JsonParser();
languageMaps.put(locale, obj); JsonObject obj = (JsonObject) parser.parse(new InputStreamReader(r.getInputStream(), "UTF-8"));
set.add(obj);
}
languageMaps.put(locale, set);
} catch (JsonIOException | JsonSyntaxException | IOException e) { } catch (JsonIOException | JsonSyntaxException | IOException e) {
logger.error("Unable to load locale", e); logger.error("Unable to load locale", e);
} }