From 9e3a3f7451695540093d50b46fac93710638f090 Mon Sep 17 00:00:00 2001 From: Georgy Litvinov Date: Fri, 25 Nov 2022 15:25:58 +0100 Subject: [PATCH] [i18b sprint] 3760 translations loading (#341) * renamed I18nBundle * added I18nBundle interface * Added translation provider * prototype of TranslationConverter * convert all properties * fixes * added caching * Removed obsolete code * Improved logging * fixed getting already existing label * Fix to get RDF Service for configuration models * fix translation request query * added INTERFACE_I18N_FIRSTTIME_BACKUP model * converter test added * formatting fixes * Translation provider tests added * cleanups, added cache test for translation provider * fix: get theme info from web app dao factory as sparql queries on both content and configuration models not supported --- .../vitro/webapp/config/RevisionInfoBean.java | 2 +- .../mannlib/vitro/webapp/i18n/I18n.java | 218 +----------- .../mannlib/vitro/webapp/i18n/I18nBundle.java | 133 +------ .../webapp/i18n/I18nContextListener.java | 30 ++ .../mannlib/vitro/webapp/i18n/I18nLogger.java | 21 +- .../vitro/webapp/i18n/I18nSemanticBundle.java | 28 ++ .../webapp/i18n/TranslationConverter.java | 330 ++++++++++++++++++ .../webapp/i18n/TranslationProvider.java | 221 ++++++++++++ .../webapp/i18n/VitroResourceBundle.java | 237 +------------ .../freemarker/I18nBundleTemplateModel.java | 10 +- .../i18n/freemarker/I18nMethodModel.java | 25 +- .../freemarker/I18nStringTemplateModel.java | 11 +- .../vitro/webapp/modelaccess/ModelNames.java | 7 + .../ConfigurationTripleSource.java | 23 +- .../setup/ConfigurationModelsSetup.java | 9 +- .../mannlib/vitro/webapp/i18n/I18nTest.java | 93 ----- .../webapp/i18n/TranslationConverterTest.java | 92 +++++ .../webapp/i18n/TranslationProviderTest.java | 161 +++++++++ .../mannlib/vitro/webapp/i18n/I18nStub.java | 9 +- .../modelInitContent.n3 | 12 + .../root/i18n/all.properties | 1 + .../root/i18n/all_en_CA.properties | 1 + .../root/i18n/vitro_all.properties | 1 + .../root/i18n/vivo_all_en_US.properties | 2 + .../i18n/customprefix_all_en_US.properties | 1 + .../root/themes/wilma/all.properties | 1 + .../root/themes/wilma/all_en_CA.properties | 1 + .../themes/wilma/vivo_all_en_US.properties | 1 + .../modelInitContent.n3 | 77 ++++ .../WEB-INF/resources/startup_listeners.txt | 1 + 30 files changed, 1041 insertions(+), 718 deletions(-) create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/I18nContextListener.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/I18nSemanticBundle.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverter.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/TranslationProvider.java delete mode 100644 api/src/test/java/edu/cornell/mannlib/vitro/webapp/i18n/I18nTest.java create mode 100644 api/src/test/java/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest.java create mode 100644 api/src/test/java/edu/cornell/mannlib/vitro/webapp/i18n/TranslationProviderTest.java create mode 100644 api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/modelInitContent.n3 create mode 100644 api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/i18n/all.properties create mode 100644 api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/i18n/all_en_CA.properties create mode 100644 api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/i18n/vitro_all.properties create mode 100644 api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/i18n/vivo_all_en_US.properties create mode 100644 api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/local/i18n/customprefix_all_en_US.properties create mode 100644 api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/themes/wilma/all.properties create mode 100644 api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/themes/wilma/all_en_CA.properties create mode 100644 api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/themes/wilma/vivo_all_en_US.properties create mode 100644 api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationProviderTest/modelInitContent.n3 diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/config/RevisionInfoBean.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/config/RevisionInfoBean.java index e1633798d..d01677d6a 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/config/RevisionInfoBean.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/config/RevisionInfoBean.java @@ -41,7 +41,7 @@ public class RevisionInfoBean { new Date(0), Collections.singleton(LevelRevisionInfo.DUMMY_LEVEL)); /** The bean is attached to the session by this name. */ - static final String ATTRIBUTE_NAME = RevisionInfoBean.class.getName(); + public static final String ATTRIBUTE_NAME = RevisionInfoBean.class.getName(); // ---------------------------------------------------------------------- // static methods diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java index 85f58495d..974e1d13d 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java @@ -2,16 +2,10 @@ package edu.cornell.mannlib.vitro.webapp.i18n; -import java.io.IOException; -import java.util.Comparator; -import java.util.LinkedList; +import java.util.Collections; import java.util.List; import java.util.Locale; -import java.util.MissingResourceException; import java.util.Objects; -import java.util.ResourceBundle; -import java.util.SortedSet; -import java.util.TreeSet; import java.util.concurrent.atomic.AtomicReference; import javax.servlet.ServletContext; @@ -39,8 +33,6 @@ import edu.cornell.mannlib.vitro.webapp.utils.developer.Key; public class I18n { private static final Log log = LogFactory.getLog(I18n.class); - public static final String DEFAULT_BUNDLE_NAME = "all"; - /** * If this attribute is present on the request, then the cache has already * been cleared. @@ -73,14 +65,6 @@ public class I18n { I18n.instance = new I18n(ctx); } - /** - * A convenience method to get a bundle and format the text. - */ - public static String text(String bundleName, HttpServletRequest req, - String key, Object... parameters) { - return bundle(bundleName, req).text(key, parameters); - } - /** * A convenience method to get the default bundle and format the text. */ @@ -89,25 +73,18 @@ public class I18n { return bundle(req).text(key, parameters); } - /** - * Get a request I18nBundle by this name. - */ - public static I18nBundle bundle(String bundleName, HttpServletRequest req) { - return instance.getBundle(bundleName, req); - } - /** * Get the default request I18nBundle. */ public static I18nBundle bundle(HttpServletRequest req) { - return instance.getBundle(DEFAULT_BUNDLE_NAME, req); + return instance.getBundle(req); } /** * Get the default context I18nBundle for preferred locales. */ public static I18nBundle bundle(List preferredLocales) { - return instance.getBundle(DEFAULT_BUNDLE_NAME, preferredLocales); + return instance.getBundle(preferredLocales); } // ---------------------------------------------------------------------- @@ -130,15 +107,11 @@ public class I18n { * * Declared 'protected' so it can be overridden in unit tests. */ - protected I18nBundle getBundle(String bundleName, HttpServletRequest req) { - log.debug("Getting request bundle '" + bundleName + "'"); - + protected I18nBundle getBundle(HttpServletRequest req) { checkDevelopmentMode(req); checkForChangeInThemeDirectory(req); - Locale locale = req.getLocale(); - - return getBundle(bundleName, locale); + return new I18nSemanticBundle(Collections.singletonList(locale)); } /** @@ -154,38 +127,11 @@ public class I18n { * * Declared 'protected' so it can be overridden in unit tests. */ - protected I18nBundle getBundle(String bundleName, List preferredLocales) { - log.debug("Getting context bundle '" + bundleName + "'"); - + protected I18nBundle getBundle( List preferredLocales) { checkDevelopmentMode(); checkForChangeInThemeDirectory(ctx); - Locale locale = SelectedLocale.getPreferredLocale(ctx, preferredLocales); - - return getBundle(bundleName, locale); - } - - /** - * Get an I18nBundle by this name, context, and locale. - */ - private I18nBundle getBundle(String bundleName, Locale locale) { - I18nLogger i18nLogger = new I18nLogger(); - try { - String dir = themeDirectory.get(); - ResourceBundle.Control control = new ThemeBasedControl(ctx, dir); - ResourceBundle rb = ResourceBundle.getBundle(bundleName, - locale, control); - - return new I18nBundle(bundleName, rb, i18nLogger); - } catch (MissingResourceException e) { - log.warn("Didn't find text bundle '" + bundleName + "'"); - - return I18nBundle.emptyBundle(bundleName, i18nLogger); - } catch (Exception e) { - log.error("Failed to create text bundle '" + bundleName + "'", e); - - return I18nBundle.emptyBundle(bundleName, i18nLogger); - } + return new I18nSemanticBundle(Collections.singletonList(locale)); } /** @@ -204,7 +150,7 @@ public class I18n { private void checkDevelopmentMode() { if (DeveloperSettings.getInstance().getBoolean(Key.I18N_DEFEAT_CACHE)) { log.debug("In development mode - clearing the cache."); - ResourceBundle.clearCache(); + clearCache(); } } @@ -241,158 +187,24 @@ public class I18n { if (!currentDir.equals(previousDir)) { log.debug("Theme directory changed from '" + previousDir + "' to '" + currentDir + "' - clearing the cache."); - ResourceBundle.clearCache(); + clearCache(); } } } + private void clearCache() { + TranslationProvider.getInstance().clearCache(); + } + /** Only clear the cache one time per request. */ private void clearCacheOnRequest(HttpServletRequest req) { if (req.getAttribute(ATTRIBUTE_CACHE_CLEARED) != null) { log.debug("Cache was already cleared on this request."); } else { - ResourceBundle.clearCache(); + clearCache(); log.debug("Cache cleared."); req.setAttribute(ATTRIBUTE_CACHE_CLEARED, Boolean.TRUE); } } - - // ---------------------------------------------------------------------- - // Control classes for instantiating ResourceBundles - // ---------------------------------------------------------------------- - - /** - * Instead of looking in the classpath, look in the theme i18n directory and - * the application i18n directory. - */ - static class ThemeBasedControl extends ResourceBundle.Control { - private static final String BUNDLE_DIRECTORY = "i18n/"; - private final ServletContext ctx; - private final String themeDirectory; - - public ThemeBasedControl(ServletContext ctx, String themeDirectory) { - this.ctx = ctx; - this.themeDirectory = themeDirectory; - } - - /** - * Don't look for classes to satisfy the request, just property files. - */ - @Override - public List getFormats(String baseName) { - return FORMAT_PROPERTIES; - } - - /** - * Don't look in the class path, look in the current servlet context, in - * the bundle directory under the theme directory and in the bundle - * directory under the application directory. - */ - @Override - public ResourceBundle newBundle(String baseName, Locale locale, - String format, ClassLoader loader, boolean reload) - throws IllegalAccessException, InstantiationException, - IOException { - checkArguments(baseName, locale, format); - - log.debug("Creating bundle for '" + baseName + "', " + locale - + ", '" + format + "', " + reload); - - String bundleName = toBundleName(baseName, locale); - if (bundleName == null) { - throw new NullPointerException("bundleName may not be null."); - } - - String themeI18nPath = "/" + themeDirectory + BUNDLE_DIRECTORY; - String appI18nPath = "/" + BUNDLE_DIRECTORY; - - log.debug("Paths are '" + themeI18nPath + "' and '" + appI18nPath - + "'"); - - return VitroResourceBundle.getBundle(bundleName, ctx, appI18nPath, - themeI18nPath, this); - } - - /** - * When creating the chain of acceptable Locales, include approximate - * matches before giving up and using the root Locale. - * - * Check the list of supported Locales to see if any have the same - * language but different region. If we find any, sort them and insert - * them into the usual result list, just before the root Locale. - */ - @Override - public List getCandidateLocales(String baseName, Locale locale) { - // Find the list of Locales that would normally be returned. - List usualList = super - .getCandidateLocales(baseName, locale); - - // If our "selectable locales" include no approximate matches that - // are not already in the list, we're done. - SortedSet approximateMatches = findApproximateMatches(locale); - approximateMatches.removeAll(usualList); - if (approximateMatches.isEmpty()) { - return usualList; - } - - // Otherwise, insert those approximate matches into the list just - // before the ROOT locale. - List mergedList = new LinkedList<>(usualList); - int rootLocaleHere = mergedList.indexOf(Locale.ROOT); - if (rootLocaleHere == -1) { - mergedList.addAll(approximateMatches); - } else { - mergedList.addAll(rootLocaleHere, approximateMatches); - } - return mergedList; - } - - private SortedSet findApproximateMatches(Locale locale) { - SortedSet set = new TreeSet<>(new LocaleComparator()); - - for (Locale l : SelectedLocale.getSelectableLocales(ctx)) { - if (locale.getLanguage().equals(l.getLanguage())) { - set.add(l); - } - } - - return set; - } - - /** - * The documentation for ResourceBundle.Control.newBundle() says I - * should throw these exceptions. - */ - private void checkArguments(String baseName, Locale locale, - String format) { - if (baseName == null) { - throw new NullPointerException("baseName may not be null."); - } - if (locale == null) { - throw new NullPointerException("locale may not be null."); - } - if (format == null) { - throw new NullPointerException("format may not be null."); - } - if (!FORMAT_DEFAULT.contains(format)) { - throw new IllegalArgumentException( - "format must be one of these: " + FORMAT_DEFAULT); - } - } - - } - - private static class LocaleComparator implements Comparator { - @Override - public int compare(Locale o1, Locale o2) { - int c = o1.getLanguage().compareTo(o2.getLanguage()); - if (c == 0) { - c = o1.getCountry().compareTo(o2.getCountry()); - if (c == 0) { - c = o1.getVariant().compareTo(o2.getVariant()); - } - } - return c; - } - } + } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/I18nBundle.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/I18nBundle.java index 6b27cc5a7..e50ce96be 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/I18nBundle.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/I18nBundle.java @@ -1,133 +1,10 @@ -/* $This file is distributed under the terms of the license in LICENSE$ */ - package edu.cornell.mannlib.vitro.webapp.i18n; -import java.text.MessageFormat; -import java.util.Collections; -import java.util.Enumeration; -import java.util.ResourceBundle; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import edu.cornell.mannlib.vitro.webapp.utils.developer.DeveloperSettings; -import edu.cornell.mannlib.vitro.webapp.utils.developer.Key; - -/** - * A wrapper for a ResourceBundle that will not throw an exception, no matter - * what string you request. - * - * If the ResourceBundle was not found, or if it doesn't contain the requested - * key, an error message string is returned, to help the developer diagnose the - * problem. - */ -public class I18nBundle { - private static final Log log = LogFactory.getLog(I18nBundle.class); - private static final String START_SEP = "\u25a4"; - private static final String END_SEP = "\u25a5"; - public static final String INT_SEP = "\u25a6"; - private static final String MESSAGE_BUNDLE_NOT_FOUND = "Text bundle ''{0}'' not found."; - private static final String MESSAGE_KEY_NOT_FOUND = "Text bundle ''{0}'' has no text for ''{1}''"; - - public static I18nBundle emptyBundle(String bundleName, - I18nLogger i18nLogger) { - return new I18nBundle(bundleName, i18nLogger); - } - - private final String bundleName; - private final ResourceBundle resources; - private final String notFoundMessage; - private final I18nLogger i18nLogger; - - private I18nBundle(String bundleName, I18nLogger i18nLogger) { - this(bundleName, new EmptyResourceBundle(), MESSAGE_BUNDLE_NOT_FOUND, - i18nLogger); - } - - public I18nBundle(String bundleName, ResourceBundle resources, - I18nLogger i18nLogger) { - this(bundleName, resources, MESSAGE_KEY_NOT_FOUND, i18nLogger); - } - - private I18nBundle(String bundleName, ResourceBundle resources, - String notFoundMessage, I18nLogger i18nLogger) { - if (bundleName == null) { - throw new IllegalArgumentException("bundleName may not be null"); - } - if (bundleName.isEmpty()) { - throw new IllegalArgumentException("bundleName may not be empty"); - } - if (resources == null) { - throw new NullPointerException("resources may not be null."); - } - if (notFoundMessage == null) { - throw new NullPointerException("notFoundMessage may not be null."); - } - this.bundleName = bundleName; - this.resources = resources; - this.notFoundMessage = notFoundMessage; - this.i18nLogger = i18nLogger; - } - - public String text(String key, Object... parameters) { - String textString; - if (resources.containsKey(key)) { - textString = resources.getString(key); - log.debug("In '" + bundleName + "', " + key + "='" + textString - + "')"); - } else { - String message = MessageFormat.format(notFoundMessage, bundleName, - key); - log.warn(message); - textString = "ERROR: " + message; - } - String message = formatString(textString, parameters); - - if (i18nLogger != null) { - i18nLogger.log(bundleName, key, parameters, textString, message); - } - if (isNeedExportInfo()) { - String separatedArgs = ""; - for (int i = 0; i < parameters.length; i++) { - separatedArgs += parameters[i] + INT_SEP; - } - - return START_SEP + key + INT_SEP + textString + INT_SEP + separatedArgs + message + END_SEP; - } else { - return message; - } - - } +public interface I18nBundle { - private static boolean isNeedExportInfo() { - return DeveloperSettings.getInstance().getBoolean(Key.I18N_ONLINE_TRANSLATION); - } - - private static String formatString(String textString, Object... parameters) { - if (parameters.length == 0) { - return textString; - } else { - return MessageFormat.format(textString, parameters); - } - } - - /** - * A resource bundle that contains no strings. - */ - public static class EmptyResourceBundle extends ResourceBundle { - @Override - public Enumeration getKeys() { - return Collections.enumeration(Collections. emptySet()); - } - - @Override - protected Object handleGetObject(String key) { - if (key == null) { - throw new NullPointerException("key may not be null."); - } - return null; - } - - } + public static final String START_SEP = "\u25a4"; + public static final String END_SEP = "\u25a5"; + public static final String INT_SEP = "\u25a6"; + public String text(String key, Object... parameters); } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/I18nContextListener.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/I18nContextListener.java new file mode 100644 index 000000000..701506e82 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/I18nContextListener.java @@ -0,0 +1,30 @@ +package edu.cornell.mannlib.vitro.webapp.i18n; + +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +public class I18nContextListener implements ServletContextListener{ + + @Override + public void contextInitialized(ServletContextEvent sce) { + initializeTranslationProvider(sce); + initializeTranslationConverter(sce); + } + + private void initializeTranslationConverter(ServletContextEvent sce) { + ServletContext ctx = sce.getServletContext(); + TranslationConverter.getInstance().initialize(ctx); + + } + + private void initializeTranslationProvider(ServletContextEvent sce) { + ServletContext ctx = sce.getServletContext(); + TranslationProvider.getInstance().initialize(ctx); + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + } + +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/I18nLogger.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/I18nLogger.java index b17f1933e..2aaab1398 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/I18nLogger.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/I18nLogger.java @@ -13,32 +13,29 @@ import edu.cornell.mannlib.vitro.webapp.utils.developer.Key; /** * If enabled in developer mode, write a message to the log each time someone * asks for a language string. - * - * The I18nBundle has a life span of one HTTP request, and so does this. */ public class I18nLogger { private static final Log log = LogFactory.getLog(I18nLogger.class); - - private final boolean isLogging; + private DeveloperSettings settings; public I18nLogger() { - DeveloperSettings settings = DeveloperSettings.getInstance(); - this.isLogging = settings.getBoolean(Key.I18N_LOG_STRINGS) - && log.isInfoEnabled(); + settings = DeveloperSettings.getInstance(); } - public void log(String bundleName, String key, Object[] parameters, - String rawText, String formattedText) { - if (isLogging) { + public void log(String key, Object[] parameters, String rawText, String formattedText) { + if (isI18nLoggingTurnedOn()) { String message = String.format( - "Retrieved from %s.%s with %s: '%s'", bundleName, key, + "Retrieved from %s with %s: '%s'", key, Arrays.toString(parameters), rawText); if (!rawText.equals(formattedText)) { message += String.format(" --> '%s'", formattedText); } - log.info(message); } } + + private boolean isI18nLoggingTurnedOn() { + return settings.getBoolean(Key.I18N_LOG_STRINGS) && log.isInfoEnabled(); + } } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/I18nSemanticBundle.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/I18nSemanticBundle.java new file mode 100644 index 000000000..fedff0a08 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/I18nSemanticBundle.java @@ -0,0 +1,28 @@ +package edu.cornell.mannlib.vitro.webapp.i18n; + +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +public class I18nSemanticBundle implements I18nBundle { + + private List preferredLocales = Collections.emptyList(); + + public I18nSemanticBundle(List preferredLocales){ + this.preferredLocales = convertToStrings(preferredLocales); + } + + private static List convertToStrings(List preferredLocales) { + return preferredLocales.stream().map(Locale::toLanguageTag).collect(Collectors.toList()); + } + + @Override + public String text(String key, Object... parameters) { + final TranslationProvider provider = TranslationProvider.getInstance(); + return provider.getTranslation(preferredLocales, key, parameters); + } + + + +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverter.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverter.java new file mode 100644 index 000000000..19a01d134 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverter.java @@ -0,0 +1,330 @@ +package edu.cornell.mannlib.vitro.webapp.i18n; + + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +import java.util.UUID; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.filefilter.DirectoryFileFilter; +import org.apache.commons.io.filefilter.RegexFileFilter; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jena.ontology.OntModel; +import org.apache.jena.ontology.OntModelSpec; +import org.apache.jena.query.ParameterizedSparqlString; +import org.apache.jena.query.Query; +import org.apache.jena.query.QueryExecution; +import org.apache.jena.query.QueryExecutionFactory; +import org.apache.jena.query.QueryFactory; +import org.apache.jena.query.QuerySolution; +import org.apache.jena.query.QuerySolutionMap; +import org.apache.jena.query.ResultSet; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.rdf.model.Literal; +import org.apache.jena.rdf.model.ResourceFactory; +import org.apache.jena.shared.Lock; + +import javax.servlet.ServletContext; + +import edu.cornell.mannlib.vitro.webapp.dao.jena.event.BulkUpdateEvent; +import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess; +import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames; +import edu.cornell.mannlib.vitro.webapp.rdfservice.ChangeSet; +import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService; +import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFServiceException; +import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.RDFServiceUtils; + +import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess.WhichService.CONFIGURATION; + + +public class TranslationConverter { + + protected OntModel memModel = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM); + protected ServletContext ctx; + private static final boolean BEGIN = true; + private static final boolean END = !BEGIN; + private static final int SUFFIX_LENGTH = ".properties".length(); + private static final Log log = LogFactory.getLog(TranslationConverter.class); + private static final TranslationConverter INSTANCE = new TranslationConverter(); + private static final String THEMES = "themes"; + private static final String ALL = "all"; + protected static final String APP_I18N_PATH = "/i18n/"; + protected static final String LOCAL_I18N_PATH = "/local/i18n/"; + protected static final String THEMES_PATH = "/themes/"; + private static final String TEMPLATE_BODY = "" + + "?uri .\n" + + "?uri .\n" + + "?uri ?application .\n" + + "?uri ?key .\n"; + private static final String TEMPLATE_LABEL = "" + + "?uri ?label .\n"; + private static final String TEMPLATE_THEME = "" + + "?uri ?theme .\n"; + + private static final String queryWithTheme(String langTag) { + return + "SELECT ?uri ?label WHERE {" + + TEMPLATE_BODY + + optionalLabel(langTag) + + TEMPLATE_THEME + + "}"; + } + + private static final String queryNoTheme(String langTag) { + return + "SELECT ?uri ?label WHERE {" + + TEMPLATE_BODY + + optionalLabel(langTag) + + "FILTER NOT EXISTS {" + + TEMPLATE_THEME + + "}" + + "}"; + } + + private static final String optionalLabel(String langTag) { + return + "OPTIONAL {" + + "?uri ?label .\n " + + "FILTER (LANG(?label)=\"" + langTag + "\")" + + "}"; + } + + public static TranslationConverter getInstance() { + return INSTANCE; + } + + public void initialize(ServletContext ctx) { + this.ctx = ctx; + OntModel tdbModel = ModelAccess.on(ctx).getOntModel(ModelNames.INTERFACE_I18N); + RDFService rdfService = ModelAccess.on(ctx).getRDFService(CONFIGURATION); + memModel.add(tdbModel); + convertAll(); + cleanTdbModel(tdbModel, rdfService); + updateTDBModel(rdfService); + } + + private void cleanTdbModel(OntModel storedModel, RDFService rdfService) { + ChangeSet cs = makeChangeSet(rdfService); + ByteArrayOutputStream removeOS = new ByteArrayOutputStream(); + storedModel.write(removeOS, "N3"); + InputStream removeIS = new ByteArrayInputStream(removeOS.toByteArray()); + cs.addRemoval(removeIS, RDFServiceUtils.getSerializationFormatFromJenaString("N3"), ModelNames.INTERFACE_I18N); + try { + rdfService.changeSetUpdate(cs); + } catch (RDFServiceException e) { + log.error(e,e); + } + } + + private void updateTDBModel(RDFService rdfService) { + ChangeSet cs = makeChangeSet(rdfService); + ByteArrayOutputStream addOS = new ByteArrayOutputStream(); + memModel.write(addOS, "N3"); + InputStream addIS = new ByteArrayInputStream(addOS.toByteArray()); + cs.addAddition(addIS, RDFServiceUtils.getSerializationFormatFromJenaString("N3"), ModelNames.INTERFACE_I18N); + try { + rdfService.changeSetUpdate(cs); + } catch (RDFServiceException e) { + log.error(e,e); + } + } + + public void convertAll() { + List i18nDirs = new LinkedList<>(Arrays.asList(APP_I18N_PATH, LOCAL_I18N_PATH, THEMES_PATH)); + List prefixes = VitroResourceBundle.getAppPrefixes(); + prefixes.add(""); + String prefixesRegex = "(" + StringUtils.join(prefixes, ALL + "|") + ALL + ")"; + log.debug("prefixesRegex " + prefixesRegex); + for (String dir : i18nDirs) { + File realDir = new File(ctx.getRealPath(dir)); + Collection files = FileUtils.listFiles(realDir, new RegexFileFilter(prefixesRegex + ".*\\.properties"), DirectoryFileFilter.DIRECTORY); + for (File file : files) { + convert(file); + } + } + } + + private void convert(File file) { + Properties props = new Properties(); + try (Reader reader = new InputStreamReader( new FileInputStream(file), "UTF-8")) { + props.load(reader); + } catch (Exception e) { + log.error(e,e); + } + if (props == null || props.isEmpty()) { + return; + } + log.info("Converting properties " + file.getAbsolutePath()); + String theme = getTheme(file); + String application = getApplication(file); + String language = getLanguage(file); + String langTag = getLanguageTag(language); + StringWriter additions = new StringWriter(); + StringWriter retractionsN3 = new StringWriter(); + for (Object key : props.keySet()) { + Object value = props.get(key); + QueryExecution queryExecution = getQueryExecution(key.toString(), theme, application, langTag); + ResultSet results = queryExecution.execSelect(); + String uri = null; + if (results.hasNext()) { + QuerySolution solution = results.nextSolution(); + uri = solution.get("uri").toString(); + String label = getLabel(solution); + if (labelAreadyExists(value, label)) { + continue; + } + if (!StringUtils.isBlank(label)) { + String retraction = fillOutLabelTemplate(uri, label, langTag); + retractionsN3.append(retraction); + } + } + String addition = fillOutTemplate(uri, key.toString(), value.toString(), theme, application, langTag); + additions.append(addition); + } + log.debug("Remove from model" + retractionsN3.toString()); + log.debug("Add to model" + additions.toString()); + OntModel addModel = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM); + OntModel removeModel = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM); + addModel.read(new StringReader(additions.toString()), null, "n3"); + removeModel.read(new StringReader(retractionsN3.toString()), null, "n3"); + memModel.enterCriticalSection(Lock.WRITE); + try { + memModel.remove(removeModel); + memModel.add(addModel); + } finally { + memModel.leaveCriticalSection(); + } + log.info("Conversion finished for properties " + file.getAbsolutePath()); + + } + + private String getLanguageTag(String language) { + return language.replaceAll("_","-"); + } + + private String getLabel(QuerySolution solution) { + final RDFNode label = solution.get("label"); + if (label == null) { + return ""; + } + return ((Literal)label).getLexicalForm(); + } + + private boolean labelAreadyExists(Object value, String label) { + return label.equals(value.toString()); + } + + private String fillOutTemplate(String uri, String key, String newLabel, String theme, String application, String langTag) { + if (StringUtils.isBlank(uri)) { + return fillOutFullTemplate(key, newLabel, theme, application, langTag); + } else { + return fillOutLabelTemplate(uri, newLabel, langTag); + } + } + + private String fillOutLabelTemplate(String uri, String label, String langTag) { + ParameterizedSparqlString pss = new ParameterizedSparqlString(); + pss.setCommandText(TEMPLATE_LABEL); + pss.setIri("uri", uri); + pss.setLiteral("label", label, langTag); + return pss.toString(); + } + + private String fillOutFullTemplate(String key, String label, String theme, String application, String langTag) { + String template = getBodyTemplate(theme); + ParameterizedSparqlString pss = new ParameterizedSparqlString(); + pss.setCommandText(template); + pss.setIri("uri", createUUID()); + pss.setLiteral("label", label, langTag); + pss.setLiteral("key", key); + pss.setLiteral("application", application); + if (!StringUtils.isBlank(theme)) { + pss.setLiteral("theme", theme); + } + return pss.toString(); + } + + private QueryExecution getQueryExecution(String key, String theme, String application, String langTag) { + Query query; + QuerySolutionMap bindings = new QuerySolutionMap(); + bindings.add("application", ResourceFactory.createStringLiteral(application)); + bindings.add("key", ResourceFactory.createStringLiteral(key)); + if (StringUtils.isBlank(theme)) { + query = QueryFactory.create(queryNoTheme(langTag)); + } else { + query = QueryFactory.create(queryWithTheme(langTag)); + bindings.add("theme", ResourceFactory.createStringLiteral(theme)); + } + QueryExecution qexec = QueryExecutionFactory.create(query, memModel, bindings); + return qexec; + } + + private String createUUID() { + return "urn:uuid:" + UUID.randomUUID(); + } + + private String getBodyTemplate(String theme) { + if (StringUtils.isBlank(theme)) { + return TEMPLATE_BODY + TEMPLATE_LABEL; + } + return TEMPLATE_BODY + TEMPLATE_LABEL + TEMPLATE_THEME; + } + + private String getLanguage(File file) { + String name = file.getName(); + if (!name.contains("_")) { + return "en_US"; + } + int startIndex; + if (name.contains("_all")) { + startIndex = name.indexOf("_all_") + 5; + } else { + startIndex = name.indexOf("_") + 1; + } + int endIndex = name.length() - SUFFIX_LENGTH; + + return name.substring(startIndex,endIndex); + } + + private String getApplication(File file) { + String name = file.getName(); + if (name.toLowerCase().contains("vivo")) { + return "VIVO"; + } + return "Vitro"; + } + + private String getTheme(File file) { + File parent = file.getParentFile(); + if (parent == null) { + return ""; + } + if (THEMES.equals(parent.getName())) { + return file.getName(); + } + return getTheme(parent); + } + + private ChangeSet makeChangeSet(RDFService rdfService) { + ChangeSet cs = rdfService.manufactureChangeSet(); + cs.addPreChangeEvent(new BulkUpdateEvent(null, BEGIN)); + cs.addPostChangeEvent(new BulkUpdateEvent(null, END)); + return cs; + } + +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/TranslationProvider.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/TranslationProvider.java new file mode 100644 index 000000000..2e015228d --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/TranslationProvider.java @@ -0,0 +1,221 @@ +package edu.cornell.mannlib.vitro.webapp.i18n; + +import java.text.MessageFormat; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.servlet.ServletContext; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jena.query.QuerySolution; +import org.apache.jena.rdf.model.Literal; + +import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess.WhichService.CONFIGURATION; +import edu.cornell.mannlib.vitro.webapp.config.RevisionInfoBean; +import edu.cornell.mannlib.vitro.webapp.config.RevisionInfoBean.LevelRevisionInfo; +import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory; +import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess; +import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService; +import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFServiceException; +import edu.cornell.mannlib.vitro.webapp.rdfservice.ResultSetConsumer; +import edu.cornell.mannlib.vitro.webapp.rdfservice.filter.LanguageFilteringRDFService; +import edu.cornell.mannlib.vitro.webapp.utils.developer.DeveloperSettings; +import edu.cornell.mannlib.vitro.webapp.utils.developer.Key; +import edu.cornell.mannlib.vitro.webapp.utils.sparqlrunner.QueryHolder; + +public class TranslationProvider { + + private static final String MESSAGE_KEY_NOT_FOUND = "ERROR: Translation not found ''{0}''"; + private static final TranslationProvider INSTANCE = new TranslationProvider(); + private static final Log log = LogFactory.getLog(TranslationProvider.class); + private static final I18nLogger i18nLogger = new I18nLogger(); + private static final String QUERY = "" + + "PREFIX : \n" + + "PREFIX rdfs: \n" + + "PREFIX vitro: \n" + + "PREFIX xsd: \n" + + "SELECT ?translation \n" + "WHERE {\n" + + " GRAPH {\n" + + " ?uri :hasKey ?key .\n" + + " ?uri rdfs:label ?translation .\n" + + " OPTIONAL { \n" + + " ?uri :hasTheme ?found_theme .\n" + + " }\n" + + " OPTIONAL { \n" + + " ?uri :hasApp ?found_application .\n" + + " }\n" + + " BIND(COALESCE(?found_theme, \"none\") as ?theme ) .\n" + + " FILTER(?theme = \"none\" || ?theme = ?current_theme) . " + + " BIND(COALESCE(?found_application, \"none\") as ?application ) .\n" + + " BIND(IF(?current_application = ?application && ?current_theme = ?theme, 3, " + + " IF(?current_theme = ?theme, 2, " + + " IF(?current_application = ?application, 1, 0)) ) AS ?order ) .\n" + + " }\n" + "} \n" + + "ORDER by DESC(?order)"; + + protected RDFService rdfService; + protected String application = "Vitro"; + private Map cache = new ConcurrentHashMap<>(); + private String theme = "vitro"; + private int prefixLen = "themes/".length(); + private int suffixLen = "/".length(); + private WebappDaoFactory wdf; + + public static TranslationProvider getInstance() { + return INSTANCE; + } + + public void initialize(ServletContext ctx) { + RevisionInfoBean info = (RevisionInfoBean) ctx.getAttribute(RevisionInfoBean.ATTRIBUTE_NAME); + List levelInfos = info.getLevelInfos(); + setApplication(levelInfos); + rdfService = ModelAccess.on(ctx).getRDFService(CONFIGURATION); + wdf = ModelAccess.on(ctx).getWebappDaoFactory(); + updateTheme(); + } + + private void updateTheme() { + final String themeDir = wdf.getApplicationDao().getApplicationBean().getThemeDir(); + final int length = themeDir.length(); + theme = themeDir.substring(prefixLen, length - suffixLen); + } + + public void setTheme(String theme) { + this.theme = theme; + } + + private void setApplication(List levelInfos) { + if (levelInfos.isEmpty()) { + return; + } + application = levelInfos.get(0).getName(); + } + + public String getTranslation(List preferredLocales, String key, Object[] parameters) { + TranslationKey tk = new TranslationKey(preferredLocales, key, parameters); + if (cache.containsKey(tk) && !needExportInfo()) { + log.debug("Returned value from cache for " + key); + return cache.get(tk); + } + String text = getText(preferredLocales, key); + String formattedText = formatString(text, parameters); + i18nLogger.log(key, parameters, text, formattedText); + if (needExportInfo()) { + return prepareExportInfo(key, parameters, text, formattedText); + } else { + cache.put(tk, formattedText); + log.debug("Added to cache " + key); + log.debug("Returned value from request for " + key); + return formattedText; + } + } + + private String prepareExportInfo(String key, Object[] parameters, String text, String message) { + String separatedArgs = ""; + for (int i = 0; i < parameters.length; i++) { + separatedArgs += parameters[i] + I18nBundle.INT_SEP; + } + log.debug("Returned value with export info for " + key ); + return I18nBundle.START_SEP + key + I18nBundle.INT_SEP + text + I18nBundle.INT_SEP + separatedArgs + + message + I18nBundle.END_SEP; + } + + private String getText(List preferredLocales, String key) { + String textString; + QueryHolder queryHolder = new QueryHolder(QUERY) + .bindToPlainLiteral("current_application", application) + .bindToPlainLiteral("key", key) + .bindToPlainLiteral("current_theme", theme) + .bindToPlainLiteral("locale", preferredLocales.get(0)); + + LanguageFilteringRDFService lfrs = new LanguageFilteringRDFService(rdfService, preferredLocales); + List list = new LinkedList<>(); + try { + lfrs.sparqlSelectQuery(queryHolder.getQueryString(), new ResultSetConsumer() { + @Override + protected void processQuerySolution(QuerySolution qs) { + Literal translation = qs.getLiteral("translation"); + if (translation != null) { + list.add(translation.getLexicalForm()); + } + } + }); + } catch (RDFServiceException e) { + log.error(e,e); + } + + if (list.isEmpty()) { + textString = notFound(key); + } else { + textString = list.get(0); + } + return textString; + } + + private static boolean needExportInfo() { + return DeveloperSettings.getInstance().getBoolean(Key.I18N_ONLINE_TRANSLATION); + } + + private static String formatString(String textString, Object... parameters) { + if (parameters.length == 0) { + return textString; + } else { + return MessageFormat.format(textString, parameters); + } + } + + private String notFound(String key) { + return MessageFormat.format(MESSAGE_KEY_NOT_FOUND, key); + } + + public void clearCache() { + if (wdf != null) { + updateTheme(); + } + cache.clear(); + log.info("Translation cache cleared"); + } + + private class TranslationKey { + + private List preferredLocales; + private String key; + private Object[] parameters; + + public TranslationKey(List preferredLocales, String key, Object[] parameters) { + this.preferredLocales = preferredLocales; + this.key = key; + this.parameters = parameters; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (! (obj instanceof TranslationKey)) { + return false; + } + TranslationKey other = (TranslationKey) obj; + return new EqualsBuilder() + .append(preferredLocales, other.preferredLocales) + .append(key, other.key) + .append(parameters, other.parameters) + .isEquals(); + } + + @Override + public int hashCode(){ + return new HashCodeBuilder() + .append(preferredLocales) + .append(key) + .append(parameters) + .toHashCode(); + } + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/VitroResourceBundle.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/VitroResourceBundle.java index 8a8b0053c..091c405a3 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/VitroResourceBundle.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/VitroResourceBundle.java @@ -2,61 +2,26 @@ package edu.cornell.mannlib.vitro.webapp.i18n; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.Reader; -import java.text.MessageFormat; import java.util.ArrayList; -import java.util.Enumeration; import java.util.List; -import java.util.Properties; -import java.util.ResourceBundle; - -import javax.servlet.ServletContext; - -import org.apache.commons.io.FileUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; /** - * Works like a PropertyResourceBundle with two exceptions: - * - * It looks for the file in both the i18n directory of the theme and in the i18n - * directory of the application. Properties found in the theme override those - * found in the application. - * - * It allows a property to take its contents from a file. File paths are - * relative to the i18n directory. Again, a file in the theme will override one - * in the application. - * - * If a property has a value (after overriding) of "@@file <filepath>", the - * bundle looks for the file relative to the i18n directory of the theme, then - * relative to the i18n directory of the application. If the file is not found - * in either location, a warning is written to the log and the property will - * contain an error message for displayed. - * - * Note that the filename is not manipulated for Locale, so the author of the - * properties files must do it explicitly. For example: - * - * In all.properties: account_email_html = @@file accountEmail.html - * - * In all_es.properties: account_email_html = @@file accountEmail_es.html + * If you use 3 tier architecture with custom prefix for properties files + * you can add it with {@link #addAppPrefix(String)} + * */ -public class VitroResourceBundle extends ResourceBundle { - private static final Log log = LogFactory.getLog(VitroResourceBundle.class); - - private static final String FILE_FLAG = "@@file "; - private static final String MESSAGE_FILE_NOT_FOUND = "File {1} not found for property {0}."; - +public class VitroResourceBundle { + private static final List appPrefixes = new ArrayList<>(); static { addAppPrefix("vitro"); } + public static List getAppPrefixes(){ + return appPrefixes; + } + public static void addAppPrefix(String prefix) { if (!prefix.endsWith("-") && !prefix.endsWith("_")) { prefix = prefix + "_"; @@ -67,188 +32,4 @@ public class VitroResourceBundle extends ResourceBundle { } } - // ---------------------------------------------------------------------- - // Factory method - // ---------------------------------------------------------------------- - - /** - * Returns the bundle for the for foo_ba_RR, providing that - * foo_ba_RR.properties exists in the I18n area of either the theme or the - * application. - * - * If the desired file doesn't exist in either location, return null. - * Usually, this does not indicate a problem but only that we were looking - * for too specific a bundle. For example, if the base name of the bundle is - * "all" and the locale is "en_US", we will likely return null on the search - * for all_en_US.properties, and all_en.properties, but will return a full - * bundle for all.properties. - * - * Of course, if all.properties doesn't exist either, then we have a - * problem, but that will be reported elsewhere. - * - * @return the populated bundle or null. - */ - public static VitroResourceBundle getBundle(String bundleName, - ServletContext ctx, String appI18nPath, String themeI18nPath, - Control control) { - try { - return new VitroResourceBundle(bundleName, ctx, appI18nPath, - themeI18nPath, control); - } catch (FileNotFoundException e) { - log.debug(e.getMessage()); - return null; - } catch (Exception e) { - log.warn(e, e); - return null; - } - } - - // ---------------------------------------------------------------------- - // The instance - // ---------------------------------------------------------------------- - - private final String bundleName; - private final ServletContext ctx; - private final String appI18nPath; - private final String themeI18nPath; - private final Control control; - private final Properties properties; - - private VitroResourceBundle(String bundleName, ServletContext ctx, - String appI18nPath, String themeI18nPath, Control control) - throws IOException { - this.bundleName = bundleName; - this.ctx = ctx; - this.appI18nPath = appI18nPath; - this.themeI18nPath = themeI18nPath; - this.control = control; - this.properties = loadProperties(); - loadReferencedFiles(); - } - - private Properties loadProperties() throws IOException { - String resourceName = control.toResourceName(bundleName, "properties"); - Properties props = null; - - File defaultsPath = locateFile(joinPath(appI18nPath, resourceName)); - File propertiesPath = locateFile(joinPath(themeI18nPath, resourceName)); - - props = loadProperties(props, defaultsPath); - if (appPrefixes != null && appPrefixes.size() > 0) { - for (String appPrefix : appPrefixes) { - props = loadProperties(props, locateFile(joinPath(appI18nPath, (appPrefix + resourceName)))); - } - } - props = loadProperties(props, propertiesPath); - if (props == null) { - throw new FileNotFoundException("Property file not found at '" + defaultsPath + "' or '" + propertiesPath + "'"); - } - props = loadProperties(props, locateFile(joinPath("/local/i18n/", resourceName))); - - return props; - } - - private Properties loadProperties(Properties defProps, File file) throws IOException { - if (file == null || !file.isFile()) { - return defProps; - } - - Properties props = null; - if (defProps != null) { - props = new Properties(defProps); - } else { - props = new Properties(); - } - - log.debug("Loading bundle '" + bundleName + "' defaults from '" + file + "'"); - FileInputStream stream = new FileInputStream(file); - Reader reader = new InputStreamReader(stream, "UTF-8"); - try { - props.load(reader); - } finally { - reader.close(); - } - - if (props.size() > 0) { - return props; - } - - return defProps; - } - - private void loadReferencedFiles() throws IOException { - for (String key : this.properties.stringPropertyNames()) { - String value = this.properties.getProperty(key); - if (value.startsWith(FILE_FLAG)) { - String filepath = value.substring(FILE_FLAG.length()).trim(); - loadReferencedFile(key, filepath); - } - } - } - - private void loadReferencedFile(String key, String filepath) - throws IOException { - String appFilePath = joinPath(appI18nPath, filepath); - String themeFilePath = joinPath(themeI18nPath, filepath); - File appFile = locateFile(appFilePath); - File themeFile = locateFile(themeFilePath); - - if (themeFile != null) { - this.properties.setProperty(key, - FileUtils.readFileToString(themeFile, "UTF-8")); - } else if (appFile != null) { - this.properties.setProperty(key, - FileUtils.readFileToString(appFile, "UTF-8")); - } else { - String message = MessageFormat.format(MESSAGE_FILE_NOT_FOUND, key, - themeFilePath, appFilePath); - this.properties.setProperty(key, message); - log.warn(message); - } - } - - private String joinPath(String root, String twig) { - if ((root.charAt(root.length() - 1) == File.separatorChar) - || (twig.charAt(0) == File.separatorChar)) { - return root + twig; - } else { - return root + File.separatorChar + twig; - } - } - - private File locateFile(String path) { - String realPath = ctx.getRealPath(path); - if (realPath == null) { - log.debug("No real path for '" + path + "'"); - return null; - } - - File f = new File(realPath); - if (!f.isFile()) { - log.debug("No file at '" + realPath + "'"); - return null; - } - if (!f.canRead()) { - log.error("Can't read the file at '" + realPath + "'"); - return null; - } - log.debug("Located file '" + path + "' at '" + realPath + "'"); - return f; - } - - @SuppressWarnings("unchecked") - @Override - public Enumeration getKeys() { - return (Enumeration) this.properties.propertyNames(); - } - - @Override - protected Object handleGetObject(String key) { - String value = this.properties.getProperty(key); - if (value == null) { - log.debug(bundleName + " has no value for '" + key + "'"); - } - return value; - } - } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/freemarker/I18nBundleTemplateModel.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/freemarker/I18nBundleTemplateModel.java index 59241ee04..133b35c91 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/freemarker/I18nBundleTemplateModel.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/freemarker/I18nBundleTemplateModel.java @@ -15,21 +15,17 @@ import freemarker.template.TemplateModelException; * wrapper around an I18nBundle. */ public class I18nBundleTemplateModel implements TemplateHashModel { - private static final Log log = LogFactory - .getLog(I18nBundleTemplateModel.class); + private static final Log log = LogFactory.getLog(I18nBundleTemplateModel.class); - private final String bundleName; private final I18nBundle textBundle; - public I18nBundleTemplateModel(String bundleName, I18nBundle textBundle) { - this.bundleName = bundleName; + public I18nBundleTemplateModel( I18nBundle textBundle) { this.textBundle = textBundle; } @Override public TemplateModel get(String key) throws TemplateModelException { - return new I18nStringTemplateModel(bundleName, key, - textBundle.text(key)); + return new I18nStringTemplateModel(key, textBundle.text(key)); } @Override diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/freemarker/I18nMethodModel.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/freemarker/I18nMethodModel.java index 7dead7163..3112c8325 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/freemarker/I18nMethodModel.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/freemarker/I18nMethodModel.java @@ -12,7 +12,7 @@ import org.apache.commons.logging.LogFactory; import edu.cornell.mannlib.vitro.webapp.i18n.I18n; import edu.cornell.mannlib.vitro.webapp.i18n.I18nBundle; import freemarker.core.Environment; -import freemarker.template.TemplateMethodModel; +import freemarker.template.TemplateMethodModelEx; import freemarker.template.TemplateModelException; /** @@ -21,30 +21,15 @@ import freemarker.template.TemplateModelException; * * If the bundle name is not provided, the default bundle is assumed. */ -public class I18nMethodModel implements TemplateMethodModel { +public class I18nMethodModel implements TemplateMethodModelEx { private static final Log log = LogFactory.getLog(I18nMethodModel.class); - @SuppressWarnings("rawtypes") @Override public Object exec(List args) throws TemplateModelException { - if (args.size() > 1) { - throw new TemplateModelException("Too many arguments: " - + "displayText method only requires a bundle name."); - } - Object arg = args.isEmpty() ? I18n.DEFAULT_BUNDLE_NAME : args.get(0); - if (!(arg instanceof String)) { - throw new IllegalArgumentException( - "Arguments to a TemplateMethodModel are supposed to be Strings!"); - } - - log.debug("Asking for this bundle: " + arg); - String bundleName = (String) arg; - Environment env = Environment.getCurrentEnvironment(); - HttpServletRequest request = (HttpServletRequest) env - .getCustomAttribute("request"); - I18nBundle tb = I18n.bundle(bundleName, request); - return new I18nBundleTemplateModel(bundleName, tb); + HttpServletRequest request = (HttpServletRequest) env.getCustomAttribute("request"); + I18nBundle tb = I18n.bundle(request); + return new I18nBundleTemplateModel(tb); } } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/freemarker/I18nStringTemplateModel.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/freemarker/I18nStringTemplateModel.java index 9aa8828fe..cb0730fed 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/freemarker/I18nStringTemplateModel.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/freemarker/I18nStringTemplateModel.java @@ -37,13 +37,10 @@ public class I18nStringTemplateModel implements TemplateMethodModelEx, private static final Log log = LogFactory .getLog(I18nStringTemplateModel.class); - private final String bundleName; private final String key; private final String textString; - public I18nStringTemplateModel(String bundleName, String key, - String textString) { - this.bundleName = bundleName; + public I18nStringTemplateModel( String key, String textString) { this.key = key; this.textString = textString; } @@ -56,8 +53,7 @@ public class I18nStringTemplateModel implements TemplateMethodModelEx, @SuppressWarnings({ "rawtypes", "unchecked" }) @Override public Object exec(List args) throws TemplateModelException { - log.debug("Formatting string '" + key + "' from bundle '" + bundleName - + "' with these arguments: " + args); + log.debug("Formatting string '" + key + "' with these arguments: " + args); if (args.isEmpty()) { return textString; @@ -74,8 +70,7 @@ public class I18nStringTemplateModel implements TemplateMethodModelEx, return MessageFormat.format(textString, unwrappedArgs); } } catch (Exception e) { - String message = "Can't format '" + key + "' from bundle '" - + bundleName + "', wrong argument types: " + args + String message = "Can't format '" + key + "', wrong argument types: " + args + " for message format'" + textString + "'"; log.warn(message); return message; diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/modelaccess/ModelNames.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/modelaccess/ModelNames.java index 10493a0c3..d3295234b 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/modelaccess/ModelNames.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/modelaccess/ModelNames.java @@ -34,6 +34,10 @@ public class ModelNames { public static final String DISPLAY_TBOX_FIRSTTIME_BACKUP = DISPLAY_TBOX + "FirsttimeBackup"; public static final String DISPLAY_DISPLAY = "http://vitro.mannlib.cornell.edu/default/vitro-kb-displayMetadata-displayModel"; public static final String DISPLAY_DISPLAY_FIRSTTIME_BACKUP = DISPLAY_DISPLAY + "FirsttimeBackup"; + public static final String INTERFACE_I18N = "http://vitro.mannlib.cornell.edu/default/interface-i18n"; + public static final String INTERFACE_I18N_FIRSTTIME_BACKUP = INTERFACE_I18N + "FirsttimeBackup"; + + /** * A map of the URIS, keyed by their short names, intended only for display @@ -64,6 +68,9 @@ public class ModelNames { map.put("DISPLAY_TBOX_FIRSTTIME_BACKUP", DISPLAY_TBOX_FIRSTTIME_BACKUP); map.put("DISPLAY_DISPLAY", DISPLAY_DISPLAY); map.put("DISPLAY_DISPLAY_FIRSTTIME_BACKUP", DISPLAY_DISPLAY_FIRSTTIME_BACKUP); + map.put("INTERFACE_I18N", INTERFACE_I18N); + map.put("INTERFACE_I18N_FIRSTTIME_BACKUP", INTERFACE_I18N_FIRSTTIME_BACKUP); + return Collections.unmodifiableMap(map); } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/modules/tripleSource/ConfigurationTripleSource.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/modules/tripleSource/ConfigurationTripleSource.java index fe766329b..06f141e7d 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/modules/tripleSource/ConfigurationTripleSource.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/modules/tripleSource/ConfigurationTripleSource.java @@ -13,6 +13,9 @@ import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.USER_ACCOU import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.DISPLAY_FIRSTTIME_BACKUP; import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.DISPLAY_TBOX_FIRSTTIME_BACKUP; import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.DISPLAY_DISPLAY_FIRSTTIME_BACKUP; +import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.INTERFACE_I18N ; +import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.INTERFACE_I18N_FIRSTTIME_BACKUP; + import org.apache.jena.rdf.model.ModelMaker; @@ -25,11 +28,21 @@ public abstract class ConfigurationTripleSource implements TripleSource { * A list of all Configuration models, in case the implementation wants to * add memory-mapping. */ - protected static final String[] CONFIGURATION_MODELS = { DISPLAY, - DISPLAY_TBOX, DISPLAY_DISPLAY, USER_ACCOUNTS, ABOX_ASSERTIONS_FIRSTTIME_BACKUP, - TBOX_ASSERTIONS_FIRSTTIME_BACKUP, APPLICATION_METADATA_FIRSTTIME_BACKUP, - USER_ACCOUNTS_FIRSTTIME_BACKUP, DISPLAY_FIRSTTIME_BACKUP, - DISPLAY_TBOX_FIRSTTIME_BACKUP, DISPLAY_DISPLAY_FIRSTTIME_BACKUP }; + protected static final String[] CONFIGURATION_MODELS = { + DISPLAY, + DISPLAY_TBOX, + DISPLAY_DISPLAY, + USER_ACCOUNTS, + ABOX_ASSERTIONS_FIRSTTIME_BACKUP, + TBOX_ASSERTIONS_FIRSTTIME_BACKUP, + APPLICATION_METADATA_FIRSTTIME_BACKUP, + USER_ACCOUNTS_FIRSTTIME_BACKUP, + DISPLAY_FIRSTTIME_BACKUP, + DISPLAY_TBOX_FIRSTTIME_BACKUP, + DISPLAY_DISPLAY_FIRSTTIME_BACKUP, + INTERFACE_I18N, + INTERFACE_I18N_FIRSTTIME_BACKUP, + }; /** * These decorators are added to a Configuration ModelMaker, regardless of diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/servlet/setup/ConfigurationModelsSetup.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/servlet/setup/ConfigurationModelsSetup.java index 75e660d7d..564b9645f 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/servlet/setup/ConfigurationModelsSetup.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/servlet/setup/ConfigurationModelsSetup.java @@ -6,6 +6,7 @@ import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.DISPLAY; import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.DISPLAY_DISPLAY; import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.DISPLAY_TBOX; import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.USER_ACCOUNTS; +import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.INTERFACE_I18N; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; @@ -13,17 +14,10 @@ import javax.servlet.ServletContextListener; import org.apache.jena.ontology.OntModel; import org.apache.jena.rdf.model.Model; -import org.apache.jena.rdf.model.Property; -import org.apache.jena.rdf.model.RDFNode; -import org.apache.jena.rdf.model.Resource; -import org.apache.jena.rdf.model.Statement; -import org.apache.jena.rdf.model.StmtIterator; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import java.io.StringWriter; -import java.util.ArrayList; -import java.util.List; import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess; import edu.cornell.mannlib.vitro.webapp.startup.StartupStatus; @@ -46,6 +40,7 @@ public class ConfigurationModelsSetup implements ServletContextListener { setupModel(ctx, DISPLAY_TBOX, "displayTbox"); setupModel(ctx, DISPLAY_DISPLAY, "displayDisplay"); setupModel(ctx, USER_ACCOUNTS, "auth"); + setupModel(ctx, INTERFACE_I18N, "interface-i18n"); ss.info(this, "Set up the display models and the user accounts model."); } catch (Exception e) { ss.fatal(this, e.getMessage(), e.getCause()); diff --git a/api/src/test/java/edu/cornell/mannlib/vitro/webapp/i18n/I18nTest.java b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/i18n/I18nTest.java deleted file mode 100644 index 8396f1beb..000000000 --- a/api/src/test/java/edu/cornell/mannlib/vitro/webapp/i18n/I18nTest.java +++ /dev/null @@ -1,93 +0,0 @@ -package edu.cornell.mannlib.vitro.webapp.i18n; - -import static org.junit.Assert.assertEquals; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -import org.junit.Before; -import org.junit.Test; - -import stubs.javax.servlet.ServletContextStub; -import edu.cornell.mannlib.vitro.testing.AbstractTestClass; -import edu.cornell.mannlib.vitro.webapp.i18n.selection.SelectedLocale; - -/* $This file is distributed under the terms of the license in LICENSE$ */ - -/** - * Test the I18N functionality. - * - * Start by checking the logic that finds approximate matches for - * language-specific property files. - */ -public class I18nTest extends AbstractTestClass { - private static final List SELECTABLE_LOCALES = locales("es_MX", - "en_US"); - - ServletContextStub ctx; - - @Before - public void setup() { - ctx = new ServletContextStub(); - } - - @Test - public void noMatchOnLanguageRegion() { - assertLocales("fr_CA", SELECTABLE_LOCALES, "fr_CA", "fr", ""); - } - - @Test - public void noMatchOnLanguage() { - assertLocales("fr", SELECTABLE_LOCALES, "fr", ""); - } - - @Test - public void noMatchOnRoot() { - assertLocales("", SELECTABLE_LOCALES, ""); - } - - @Test - public void matchOnLanguageRegion() { - assertLocales("es_ES", SELECTABLE_LOCALES, "es_ES", "es", "es_MX", ""); - } - - @Test - public void matchOnLanguage() { - assertLocales("es", SELECTABLE_LOCALES, "es", "es_MX", ""); - } - - // ---------------------------------------------------------------------- - // Helper methods - // ---------------------------------------------------------------------- - - private void assertLocales(String requested, List selectable, - String... expected) { - SelectedLocale.setSelectableLocales(ctx, selectable); - List expectedLocales = locales(expected); - - I18n.ThemeBasedControl control = new I18n.ThemeBasedControl(ctx, - "bogusThemeDirectory"); - List actualLocales = control.getCandidateLocales( - "bogusBaseName", locale(requested)); - - assertEquals("Expected locales", expectedLocales, actualLocales); - } - - private static List locales(String... strings) { - List locales = new ArrayList<>(); - for (String s : strings) { - locales.add(locale(s)); - } - return locales; - } - - private static Locale locale(String s) { - String[] parts = s.split("_"); - String language = (parts.length > 0) ? parts[0] : ""; - String country = (parts.length > 1) ? parts[1] : ""; - String variant = (parts.length > 2) ? parts[2] : ""; - return new Locale(language, country, variant); - } - -} diff --git a/api/src/test/java/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest.java b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest.java new file mode 100644 index 000000000..f080bdc0f --- /dev/null +++ b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest.java @@ -0,0 +1,92 @@ +package edu.cornell.mannlib.vitro.webapp.i18n; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; + +import org.apache.jena.graph.NodeFactory; +import org.apache.jena.ontology.OntModel; +import org.apache.jena.rdf.model.Selector; +import org.apache.jena.rdf.model.SimpleSelector; +import org.apache.jena.rdf.model.StmtIterator; +import org.apache.jena.rdf.model.impl.PropertyImpl; +import org.junit.Test; + +import stubs.javax.servlet.ServletContextStub; + +public class TranslationConverterTest { + + private static final String WILMA = "wilma"; + private static final String HAS_THEME = "http://vivoweb.org/ontology/core/properties/vocabulary#hasTheme"; + private static final String VITRO = "Vitro"; + private static final String VIVO = "VIVO"; + private static final String HAS_APP = "http://vivoweb.org/ontology/core/properties/vocabulary#hasApp"; + private static final String HAS_KEY = "http://vivoweb.org/ontology/core/properties/vocabulary#hasKey"; + private static final String ROOT_PATH = "src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root"; + private static final String INIT_N3_FILE = "src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/modelInitContent.n3"; + ServletContextStub ctx = new ServletContextStub(); + private OntModel model; + + @Test + public void testConversion() throws FileNotFoundException { + VitroResourceBundle.addAppPrefix("customprefix"); + VitroResourceBundle.addAppPrefix("vivo"); + TranslationConverter converter = TranslationConverter.getInstance(); + model = converter.memModel; + File n3 = new File(INIT_N3_FILE); + assertTrue(model.isEmpty()); + model.read(new FileReader(n3), null, "n3"); + assertFalse(model.isEmpty()); + File appI18n = new File(ROOT_PATH + TranslationConverter.APP_I18N_PATH); + File localI18n = new File(ROOT_PATH + TranslationConverter.LOCAL_I18N_PATH); + File themes = new File(ROOT_PATH + TranslationConverter.THEMES_PATH); + ctx.setRealPath(TranslationConverter.APP_I18N_PATH, appI18n.getAbsolutePath()); + ctx.setRealPath(TranslationConverter.LOCAL_I18N_PATH, localI18n.getAbsolutePath()); + ctx.setRealPath(TranslationConverter.THEMES_PATH, themes.getAbsolutePath()); + converter.ctx = ctx; + converter.convertAll(); + + assertEquals(2, getCount(HAS_KEY, "test_key_all_en_US")); + assertEquals(2, getCount(HAS_KEY, "test_key_all_en_CA")); + + assertEquals(2, getCount(HAS_KEY, "test_key_all")); + + assertEquals(1, getCount(HAS_KEY, "property_to_overwrite")); + + assertTrue(n3TranslationValueIsOverwrittenByProperty(model)); + + assertEquals(3, getCount(HAS_THEME, WILMA)); + assertEquals(6, getCount(HAS_APP, VITRO)); + assertEquals(3, getCount(HAS_APP, VIVO)); + // printResultModel(); + } + + private void printResultModel() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + model.write(baos, "N3"); + System.out.println(baos.toString()); + } + + private int getCount(String hasTheme, String wilma) { + Selector selector = new SimpleSelector(null, new PropertyImpl(hasTheme), wilma); + StmtIterator it = model.listStatements(selector); + int count = 0; + while (it.hasNext()) { + count++; + it.next(); + } + return count; + } + + private boolean n3TranslationValueIsOverwrittenByProperty(OntModel model) { + return model.getGraph().contains( + NodeFactory.createURI("urn:uuid:8c80dbf5-adda-41d5-a6fe-d5efde663600"), + NodeFactory.createURI("http://www.w3.org/2000/01/rdf-schema#label"), + NodeFactory.createLiteral("value from properties file","en-US")); + } +} diff --git a/api/src/test/java/edu/cornell/mannlib/vitro/webapp/i18n/TranslationProviderTest.java b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/i18n/TranslationProviderTest.java new file mode 100644 index 000000000..07aa07965 --- /dev/null +++ b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/i18n/TranslationProviderTest.java @@ -0,0 +1,161 @@ +package edu.cornell.mannlib.vitro.webapp.i18n; + +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.util.Collections; + +import org.apache.jena.query.Dataset; +import org.apache.jena.query.DatasetFactory; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.junit.Test; + +import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.jena.model.RDFServiceModel; + +public class TranslationProviderTest { + + private static final String VITRO = "Vitro"; + private static final String VIVO = "VIVO"; + private static final String ROOT = "src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationProviderTest/"; + private static final String TRANSLATIONS_N3_FILE = ROOT + "modelInitContent.n3"; + private static final String WILMA = "wilma"; + private static final String NEMO = "nemo"; + + private Model i18nModel; + private RDFServiceModel rdfService; + private TranslationProvider tp; + + public void init(String i18nFile, String themeName, String appName) throws FileNotFoundException { + i18nModel = ModelFactory.createDefaultModel(); + i18nModel.read(new FileReader(new File(i18nFile)), null, "n3"); + Dataset ds = DatasetFactory.createTxnMem(); + ds.addNamedModel("http://vitro.mannlib.cornell.edu/default/interface-i18n", i18nModel); + rdfService = new RDFServiceModel(ds); + tp = TranslationProvider.getInstance(); + tp.rdfService = rdfService; + tp.setTheme(themeName); + tp.application = appName; + tp.clearCache(); + } + + @Test + public void testNotExistingKey() throws FileNotFoundException { + init(TRANSLATIONS_N3_FILE, WILMA, VITRO); + Object array[] = {}; + String translation = tp.getTranslation(Collections.singletonList("en-US"), "non_existing_key", array); + assertEquals("ERROR: Translation not found 'non_existing_key'", translation); + } + + @Test + public void testVitroWilmaEnUS() throws FileNotFoundException { + init(TRANSLATIONS_N3_FILE, WILMA, VITRO); + Object array[] = {}; + String translation = tp.getTranslation(Collections.singletonList("en-US"), "testkey", array); + assertEquals("testkey Vitro wilma en-US", translation); + } + + @Test + public void testVitroWilmaDeDE() throws FileNotFoundException { + init(TRANSLATIONS_N3_FILE, WILMA, VITRO); + Object array[] = {}; + String translation = tp.getTranslation(Collections.singletonList("de-DE"), "testkey", array); + assertEquals("testkey Vitro wilma de-DE", translation); + } + + @Test + public void testVIVOWilmaEnUS() throws FileNotFoundException { + init(TRANSLATIONS_N3_FILE, WILMA, VIVO); + Object array[] = {}; + String translation = tp.getTranslation(Collections.singletonList("en-US"), "testkey", array); + assertEquals("testkey VIVO wilma en-US", translation); + } + + @Test + public void testVIVOWilmaDeDE() throws FileNotFoundException { + init(TRANSLATIONS_N3_FILE, WILMA, VIVO); + Object array[] = {}; + String translation = tp.getTranslation(Collections.singletonList("de-DE"), "testkey", array); + assertEquals("testkey VIVO wilma de-DE", translation); + } + + @Test + public void testThemeFallbackVitroNemoEnUS() throws FileNotFoundException { + init(TRANSLATIONS_N3_FILE, NEMO, VITRO); + Object array[] = {}; + String translation = tp.getTranslation(Collections.singletonList("en-US"), "testkey", array); + assertEquals("testkey Vitro no theme en-US", translation); + } + + @Test + public void testThemeFallbackVitroNemoDeDE() throws FileNotFoundException { + init(TRANSLATIONS_N3_FILE, NEMO, VITRO); + Object array[] = {}; + String translation = tp.getTranslation(Collections.singletonList("de-DE"), "testkey", array); + assertEquals("testkey Vitro no theme de-DE", translation); + } + + @Test + public void testThemeFallbackVIVONemoEnUS() throws FileNotFoundException { + init(TRANSLATIONS_N3_FILE, NEMO, VIVO); + Object array[] = {}; + String translation = tp.getTranslation(Collections.singletonList("en-US"), "testkey", array); + assertEquals("testkey VIVO no theme en-US", translation); + } + + @Test + public void testThemeFallbackVIVONemoDeDE() throws FileNotFoundException { + init(TRANSLATIONS_N3_FILE, NEMO, VIVO); + Object array[] = {}; + String translation = tp.getTranslation(Collections.singletonList("de-DE"), "testkey", array); + assertEquals("testkey VIVO no theme de-DE", translation); + } + + @Test + public void testAppFallbackVIVONemoEnUS() throws FileNotFoundException { + init(TRANSLATIONS_N3_FILE, WILMA, VIVO); + Object array[] = {}; + String translation = tp.getTranslation(Collections.singletonList("en-US"), "testkey_app_fallback", array); + assertEquals("testkey_app_fallback Vitro wilma en-US", translation); + } + + @Test + public void testAppFallbackVIVONemoDeDE() throws FileNotFoundException { + init(TRANSLATIONS_N3_FILE, WILMA, VIVO); + Object array[] = {}; + String translation = tp.getTranslation(Collections.singletonList("de-DE"), "testkey_app_fallback", array); + assertEquals("testkey_app_fallback Vitro wilma de-DE", translation); + } + + @Test + public void testAppAndThemeFallbackVIVONemoEnUS() throws FileNotFoundException { + init(TRANSLATIONS_N3_FILE, NEMO, VIVO); + Object array[] = {}; + String translation = tp.getTranslation(Collections.singletonList("en-US"), "testkey_app_fallback", array); + assertEquals("testkey_app_fallback Vitro no theme en-US", translation); + } + + @Test + public void testAppAndThemeFallbackVIVONemoDeDE() throws FileNotFoundException { + init(TRANSLATIONS_N3_FILE, NEMO, VIVO); + Object array[] = {}; + String translation = tp.getTranslation(Collections.singletonList("de-DE"), "testkey_app_fallback", array); + assertEquals("testkey_app_fallback Vitro no theme de-DE", translation); + } + + @Test + public void testCache() throws FileNotFoundException { + init(TRANSLATIONS_N3_FILE, WILMA, VITRO); + Object array[] = {}; + String translation = tp.getTranslation(Collections.singletonList("en-US"), "testkey", array); + assertEquals("testkey Vitro wilma en-US", translation); + tp.application = VIVO; + translation = tp.getTranslation(Collections.singletonList("en-US"), "testkey", array); + assertEquals("testkey Vitro wilma en-US", translation); + tp.clearCache(); + translation = tp.getTranslation(Collections.singletonList("en-US"), "testkey", array); + assertEquals("testkey VIVO wilma en-US", translation); + } +} diff --git a/api/src/test/java/stubs/edu/cornell/mannlib/vitro/webapp/i18n/I18nStub.java b/api/src/test/java/stubs/edu/cornell/mannlib/vitro/webapp/i18n/I18nStub.java index 372eedc69..1f40be38e 100644 --- a/api/src/test/java/stubs/edu/cornell/mannlib/vitro/webapp/i18n/I18nStub.java +++ b/api/src/test/java/stubs/edu/cornell/mannlib/vitro/webapp/i18n/I18nStub.java @@ -51,14 +51,11 @@ public class I18nStub extends I18n { } @Override - protected I18nBundle getBundle(String bundleName, HttpServletRequest req) { - return new I18nBundleStub(bundleName); + protected I18nBundle getBundle( HttpServletRequest req) { + return new I18nBundleStub(); } - private class I18nBundleStub extends I18nBundle { - public I18nBundleStub(String bundleName) { - super(bundleName, new DummyResourceBundle(), null); - } + private class I18nBundleStub implements I18nBundle { @Override public String text(String key, Object... parameters) { diff --git a/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/modelInitContent.n3 b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/modelInitContent.n3 new file mode 100644 index 000000000..aa626e48e --- /dev/null +++ b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/modelInitContent.n3 @@ -0,0 +1,12 @@ +@prefix owl: . +@prefix rdf: . +@prefix xsd: . +@prefix rdfs: . + + + a owl:NamedIndividual , ; + rdfs:label "value from n3 file"@en-US ; + + "VIVO" ; + + "property_to_overwrite" . diff --git a/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/i18n/all.properties b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/i18n/all.properties new file mode 100644 index 000000000..1316bed35 --- /dev/null +++ b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/i18n/all.properties @@ -0,0 +1 @@ +test_key_all = test value all diff --git a/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/i18n/all_en_CA.properties b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/i18n/all_en_CA.properties new file mode 100644 index 000000000..88b0f1776 --- /dev/null +++ b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/i18n/all_en_CA.properties @@ -0,0 +1 @@ +test_key_all_en_CA = test value all_en_CA diff --git a/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/i18n/vitro_all.properties b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/i18n/vitro_all.properties new file mode 100644 index 000000000..452eea67b --- /dev/null +++ b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/i18n/vitro_all.properties @@ -0,0 +1 @@ +test_key = test value vitro_all diff --git a/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/i18n/vivo_all_en_US.properties b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/i18n/vivo_all_en_US.properties new file mode 100644 index 000000000..7ee60b3f9 --- /dev/null +++ b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/i18n/vivo_all_en_US.properties @@ -0,0 +1,2 @@ +test_key = test value vivo_all_en_US +property_to_overwrite = value from properties file diff --git a/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/local/i18n/customprefix_all_en_US.properties b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/local/i18n/customprefix_all_en_US.properties new file mode 100644 index 000000000..daed1b1ba --- /dev/null +++ b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/local/i18n/customprefix_all_en_US.properties @@ -0,0 +1 @@ +test_key_all_en_US = test value customprefix_all_en_US diff --git a/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/themes/wilma/all.properties b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/themes/wilma/all.properties new file mode 100644 index 000000000..0ae565e86 --- /dev/null +++ b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/themes/wilma/all.properties @@ -0,0 +1 @@ +test_key_all = test value all wilma diff --git a/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/themes/wilma/all_en_CA.properties b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/themes/wilma/all_en_CA.properties new file mode 100644 index 000000000..f6d73cdae --- /dev/null +++ b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/themes/wilma/all_en_CA.properties @@ -0,0 +1 @@ +test_key_all_en_CA = test value all_en_CA wilma diff --git a/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/themes/wilma/vivo_all_en_US.properties b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/themes/wilma/vivo_all_en_US.properties new file mode 100644 index 000000000..3e5dc414e --- /dev/null +++ b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root/themes/wilma/vivo_all_en_US.properties @@ -0,0 +1 @@ +test_key_all_en_US = test value vivo_all_en_US wilma diff --git a/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationProviderTest/modelInitContent.n3 b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationProviderTest/modelInitContent.n3 new file mode 100644 index 000000000..590177fe6 --- /dev/null +++ b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationProviderTest/modelInitContent.n3 @@ -0,0 +1,77 @@ +@prefix owl: . +@prefix rdf: . +@prefix xsd: . +@prefix rdfs: . + + + a owl:NamedIndividual , ; + rdfs:label "testkey VIVO no theme en-US"@en-US ; + rdfs:label "testkey VIVO no theme de-DE"@de-DE ; + + "VIVO" ; + + "testkey" . + + + a owl:NamedIndividual , ; + rdfs:label "testkey Vitro no theme en-US"@en-US ; + rdfs:label "testkey Vitro no theme de-DE"@de-DE ; + + "Vitro" ; + + "testkey" . + + + a owl:NamedIndividual , ; + rdfs:label "testkey VIVO wilma en-US"@en-US ; + rdfs:label "testkey VIVO wilma de-DE"@de-DE ; + + "VIVO" ; + + "wilma" ; + + "testkey" . + + + a owl:NamedIndividual , ; + rdfs:label "testkey Vitro wilma en-US"@en-US ; + rdfs:label "testkey Vitro wilma de-DE"@de-DE ; + + "Vitro" ; + + "wilma" ; + + "testkey" . + + + a owl:NamedIndividual , ; + rdfs:label "testkey Vitro vitro en-US"@en-US ; + rdfs:label "testkey Vitro vitro de-DE"@de-DE ; + + "Vitro" ; + + "vitro" ; + + "testkey" . + + + a owl:NamedIndividual , ; + rdfs:label "testkey_app_fallback Vitro wilma en-US"@en-US ; + rdfs:label "testkey_app_fallback Vitro wilma de-DE"@de-DE ; + + "Vitro" ; + + "wilma" ; + + "testkey_app_fallback" . + + + a owl:NamedIndividual , ; + rdfs:label "testkey_app_fallback Vitro no theme en-US"@en-US ; + rdfs:label "testkey_app_fallback Vitro no theme de-DE"@de-DE ; + + "Vitro" ; + + "testkey_app_fallback" . + + diff --git a/webapp/src/main/webapp/WEB-INF/resources/startup_listeners.txt b/webapp/src/main/webapp/WEB-INF/resources/startup_listeners.txt index 40d34f39c..029f91882 100644 --- a/webapp/src/main/webapp/WEB-INF/resources/startup_listeners.txt +++ b/webapp/src/main/webapp/WEB-INF/resources/startup_listeners.txt @@ -52,6 +52,7 @@ edu.ucsf.vitro.opensocial.OpenSocialSmokeTests # For multiple language support edu.cornell.mannlib.vitro.webapp.i18n.selection.LocaleSelectionSetup +edu.cornell.mannlib.vitro.webapp.i18n.I18nContextListener # The search indexer uses a "public" permission, so the PropertyRestrictionPolicyHelper # and the PermissionRegistry must already be set up.