| /** |
| * Copyright (c) 2011, 2015 - Lunifera GmbH (Gross Enzersdorf, Austria), Loetz GmbH&Co.KG (69115 Heidelberg, Germany) |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Florian Pirchner - Initial implementation |
| */ |
| package org.eclipse.osbp.runtime.common.i18n; |
| |
| import java.io.IOException; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.net.URL; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import org.apache.commons.collections.map.LRUMap; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| // TODO: Auto-generated Javadoc |
| /** |
| * A registry that may be used to collect and access I18n translations based on |
| * URLs. The target of URLs need to be property files with the name pattern |
| * {file}_{language}-{country} (myI18n_de-DE). If no language and country are |
| * specified, the default locale is used. |
| */ |
| public class I18nRegistry { |
| |
| /** The Constant LOGGER. */ |
| private static final Logger LOGGER = LoggerFactory |
| .getLogger(I18nRegistry.class); |
| |
| /** The fast access. */ |
| private final LRUMap fastAccess = new LRUMap(1000); |
| |
| /** The cache. */ |
| private final Map<Locale, List<ResourceDescription>> cache = new HashMap<Locale, List<ResourceDescription>>(); |
| |
| /** |
| * Instantiates a new i18n registry. |
| */ |
| public I18nRegistry() { |
| |
| } |
| |
| /** |
| * Tries to find the translation for the given locale and key. |
| * |
| * @param locale |
| * the locale |
| * @param key |
| * the key |
| * @return the string |
| */ |
| public String findTranslation(Locale locale, String key) { |
| String result = tryFastAccess(locale, key); |
| if (result != null) { |
| return result; |
| } |
| |
| AccessPath accessPath = computeBestMatchAccessPath(locale, key); |
| Translation translation = accessPath.getTranslation(); |
| if (translation == null) { |
| return ""; |
| } |
| |
| return putFastAccess(translation.getLocale(), translation.getI18nKey(), |
| translation.getI18nValue()); |
| } |
| |
| /** |
| * Try to find the translation in the fast access map. |
| * |
| * @param locale |
| * the locale |
| * @param key |
| * the key |
| * @return the string |
| */ |
| private String tryFastAccess(Locale locale, String key) { |
| List<Locale> computedLocales = computeLocales(locale); |
| for (Locale temp : computedLocales) { |
| String fastAccessKey = createFastAccessKey(temp, key); |
| String result = (String) fastAccess.get(fastAccessKey); |
| if (result != null) { |
| return result; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Try to find the translation in the fast access map. |
| * |
| * @param locale |
| * the locale |
| * @param key |
| * the key |
| * @param translation |
| * the translation |
| * @return the string |
| */ |
| private String putFastAccess(Locale locale, String key, String translation) { |
| fastAccess.put(createFastAccessKey(locale, key), translation); |
| return translation; |
| } |
| |
| /** |
| * Creates the key for the fast access map access. |
| * |
| * @param temp |
| * the temp |
| * @param key |
| * the key |
| * @return the string |
| */ |
| private String createFastAccessKey(Locale temp, String key) { |
| return temp.toLanguageTag() + ":" + key; |
| } |
| |
| /** |
| * Computes the access url. |
| * <p> |
| * Following order will be used: |
| * <ol> |
| * <li>Use given locale</li> |
| * <li>Create more specific locale and repeat until default locale is |
| * reached.</li> |
| * </ul> </li> |
| * </ol> |
| * |
| * @param locale |
| * the locale |
| * @param key |
| * the key |
| * @return the access path |
| */ |
| private AccessPath computeAccessPath(Locale locale, String key) { |
| |
| String temp = key; |
| Matcher valueMatcher = null; |
| if (temp != null && !"".equals(temp)) { |
| temp = Pattern.quote(temp); |
| Pattern valuePattern = Pattern.compile(temp); |
| valueMatcher = valuePattern.matcher(""); |
| valueMatcher.reset(); |
| } |
| |
| AccessPath url = new AccessPath(); |
| int prio = 0; |
| |
| List<Locale> computedLocales = computeLocales(locale); |
| |
| for (Locale computedLocale : computedLocales) { |
| Accessor accessor = new Accessor(computedLocale, ++prio, temp, |
| valueMatcher); |
| url.addAccessor(accessor); |
| } |
| |
| return url; |
| } |
| |
| /** |
| * Computes the access url to find the best matching element. See |
| * {@link #computeAccessPath(IProject, Locale, String, String)}. |
| * |
| * @param locale |
| * the locale |
| * @param key |
| * the key |
| * @return the access path |
| */ |
| private AccessPath computeBestMatchAccessPath(Locale locale, String key) { |
| return computeAccessPath(locale, key); |
| } |
| |
| /** |
| * Computes all locales that should be added to AccessPath. |
| * |
| * @param locale |
| * the locale |
| * @return the list |
| */ |
| private List<Locale> computeLocales(Locale locale) { |
| List<Locale> locales = new LinkedList<Locale>(); |
| |
| // Add first locale |
| locales.add(locale); |
| |
| Locale temp = locale; |
| while (true) { |
| String tag = temp.toLanguageTag(); |
| String[] segments = tag.split("-"); |
| if (segments.length > 1) { |
| StringBuilder builder = new StringBuilder(); |
| for (int i = 0; i < segments.length - 1; i++) { |
| if (builder.length() != 0) { |
| builder.append("-"); |
| } |
| builder.append(segments[i]); |
| } |
| Locale moreGeneral = Locale.forLanguageTag(builder.toString()); |
| locales.add(moreGeneral); |
| temp = moreGeneral; |
| } else { |
| break; |
| } |
| } |
| |
| locales.add(new Locale("")); |
| |
| return locales; |
| } |
| |
| /** |
| * Checks if is valid. |
| * |
| * @param result |
| * the result |
| * @return true, if is valid |
| */ |
| private boolean isValid(String result) { |
| return result != null && !"".equals(result); |
| } |
| |
| /** |
| * Add a new resource to the cache. |
| * |
| * @param url |
| * the url |
| */ |
| public void addResource(URL url) { |
| |
| Properties properties = new Properties(); |
| try { |
| properties.load(url.openStream()); |
| } catch (IOException e) { |
| LOGGER.error("{}", e); |
| return; |
| } |
| |
| Locale locale = getLocale(url); |
| if (locale == null) { |
| LOGGER.error("Could not determine locale for " + url.toString()); |
| return; |
| } |
| |
| ResourceDescription description = new ResourceDescription(locale, url, |
| properties); |
| addResource(description); |
| } |
| |
| /** |
| * Gets the locale. |
| * |
| * @param url |
| * the url |
| * @return the locale |
| */ |
| private Locale getLocale(URL url) { |
| String urlString = url.toString(); |
| String file = urlString.substring(urlString.lastIndexOf("/") + 1); |
| |
| String[] tokens = file.replace(".properties", "").split("_"); |
| StringBuilder builder = new StringBuilder(); |
| if (tokens.length >= 2) { |
| for (int i = 1; i < tokens.length; i++) { |
| if (builder.length() > 0) { |
| builder.append("-"); |
| } |
| builder.append(tokens[i]); |
| } |
| } |
| Locale locale = Locale.forLanguageTag(builder.toString()); |
| return locale; |
| } |
| |
| /** |
| * Add a new resource to the cache. |
| * |
| * @param description |
| * the description |
| */ |
| public void addResource(ResourceDescription description) { |
| synchronized (cache) { |
| if (!cache.containsKey(description.getLocale())) { |
| cache.put(description.getLocale(), |
| new ArrayList<ResourceDescription>()); |
| } |
| |
| List<ResourceDescription> descs = cache |
| .get(description.getLocale()); |
| if (!descs.contains(description)) { |
| descs.add(description); |
| } |
| } |
| |
| // clear the fast access cache |
| fastAccess.clear(); |
| } |
| |
| /** |
| * Gets the resource descriptions. |
| * |
| * @param locale |
| * the locale |
| * @return the resource descriptions |
| */ |
| private List<ResourceDescription> getResourceDescriptions(Locale locale) { |
| if (cache.containsKey(locale)) { |
| return new ArrayList<I18nRegistry.ResourceDescription>( |
| cache.get(locale)); |
| } else { |
| return Collections.emptyList(); |
| } |
| } |
| |
| /** |
| * Removes the given translations. |
| * |
| * @param locale |
| * the locale |
| * @param url |
| * the url |
| */ |
| public void removeResource(Locale locale, URL url) { |
| if (!cache.containsKey(locale)) { |
| return; |
| } |
| |
| // clear the fast access cache |
| fastAccess.clear(); |
| |
| // remove the resource from the builder |
| synchronized (cache) { |
| List<ResourceDescription> descs = cache.get(locale); |
| for (Iterator<ResourceDescription> iterator = descs.iterator(); iterator |
| .hasNext();) { |
| ResourceDescription desc = iterator.next(); |
| try { |
| // avoid lookups -> |
| // http://michaelscharf.blogspot.co.at/2006/11/javaneturlequals-and-hashcode-make.html |
| URI uri = url.toURI(); |
| URI descURI = desc.getURI(); |
| if (descURI != null && descURI.equals(uri)) { |
| iterator.remove(); |
| } |
| } catch (URISyntaxException e) { |
| LOGGER.error("{}", e); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Removes the translations with the given URL. |
| * |
| * @param url |
| * the url |
| */ |
| public void removeResource(URL url) { |
| Locale locale = getLocale(url); |
| if (locale == null) { |
| LOGGER.error("Could not determine locale for " + url.toString()); |
| return; |
| } |
| removeResource(locale, url); |
| |
| } |
| |
| /** |
| * Defines how the registry should be searched. For instance the ordering of |
| * locales... |
| */ |
| private static class AccessPath { |
| |
| /** The accessors. */ |
| private List<Accessor> accessors = new LinkedList<I18nRegistry.Accessor>(); |
| |
| /** |
| * Instantiates a new access path. |
| */ |
| public AccessPath() { |
| } |
| |
| /** |
| * Adds the accessor. |
| * |
| * @param accessor |
| * the accessor |
| */ |
| public void addAccessor(Accessor accessor) { |
| accessors.add(accessor); |
| } |
| |
| /** |
| * Returns the translation. The key matches the entire key for an i18n |
| * record. |
| * |
| * @return the translation |
| */ |
| public Translation getTranslation() { |
| for (Accessor accessor : accessors) { |
| Translation result = accessor.getTranslation(); |
| if (result != null) { |
| return result; |
| } |
| } |
| |
| return null; |
| } |
| } |
| |
| /** |
| * This class will access the registry. |
| */ |
| private class Accessor { |
| |
| /** The locale. */ |
| private final Locale locale; |
| |
| /** The matcher. */ |
| private final Matcher matcher; |
| |
| /** The key. */ |
| @SuppressWarnings("unused") |
| private final String key; |
| |
| /** The prio. */ |
| private final int prio; |
| |
| /** |
| * Instantiates a new accessor. |
| * |
| * @param locale |
| * the locale |
| * @param prio |
| * the prio |
| * @param key |
| * the key |
| * @param matcher |
| * the matcher |
| */ |
| public Accessor(Locale locale, int prio, String key, Matcher matcher) { |
| super(); |
| this.locale = locale; |
| this.key = key; |
| this.matcher = matcher; |
| this.prio = prio; |
| } |
| |
| /** |
| * Returns the first translation found. The matcher must match the given |
| * key. |
| * |
| * @return the translation |
| */ |
| public Translation getTranslation() { |
| List<ResourceDescription> descs = getResourceDescriptions(locale); |
| for (ResourceDescription desc : descs) { |
| for (Map.Entry<Object, Object> entry : desc.getProperties() |
| .entrySet()) { |
| if (matcher.reset((String) entry.getKey()).matches()) { |
| if (isValid((String) entry.getValue())) { |
| return new Translation((String) entry.getKey(), |
| (String) entry.getValue(), desc, prio); |
| } |
| } |
| } |
| } |
| return null; |
| } |
| } |
| |
| /** |
| * A resource is a file containing all translations for a file. |
| */ |
| public static class ResourceDescription { |
| |
| /** The locale. */ |
| private final Locale locale; |
| |
| /** The url. */ |
| private final URL url; |
| |
| /** The properties. */ |
| private final Properties properties; |
| |
| /** |
| * Instantiates a new resource description. |
| * |
| * @param locale |
| * the locale |
| * @param url |
| * the url |
| * @param properties |
| * the properties |
| */ |
| public ResourceDescription(Locale locale, URL url, Properties properties) { |
| this.locale = locale; |
| this.url = url; |
| this.properties = properties; |
| } |
| |
| /** |
| * Gets the locale. |
| * |
| * @return the locale |
| */ |
| public Locale getLocale() { |
| return locale; |
| } |
| |
| /** |
| * Gets the url. |
| * |
| * @return the url |
| */ |
| public URL getURL() { |
| return url; |
| } |
| |
| /** |
| * Gets the uri. |
| * |
| * @return the uri |
| * @throws URISyntaxException |
| * the URI syntax exception |
| */ |
| public URI getURI() throws URISyntaxException { |
| return url != null ? url.toURI() : null; |
| } |
| |
| /** |
| * Gets the properties. |
| * |
| * @return the properties |
| */ |
| public Properties getProperties() { |
| return properties; |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see java.lang.Object#hashCode() |
| */ |
| @Override |
| public int hashCode() { |
| final int prime = 31; |
| int result = 1; |
| try { |
| // try to use URI for performance issues |
| URI uri = getURI(); |
| result = prime * result + ((uri == null) ? 0 : uri.hashCode()); |
| } catch (URISyntaxException e) { |
| // otherwise fall back to url |
| result = prime * result + ((url == null) ? 0 : url.hashCode()); |
| } |
| |
| return result; |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see java.lang.Object#equals(java.lang.Object) |
| */ |
| @Override |
| public boolean equals(Object obj) { |
| if (this == obj) |
| return true; |
| if (obj == null) |
| return false; |
| if (getClass() != obj.getClass()) |
| return false; |
| |
| ResourceDescription other = (ResourceDescription) obj; |
| try { |
| // try to use URI for performance issues |
| URI uri = getURI(); |
| URI otherUri = other.getURI(); |
| if (uri == null) { |
| if (otherUri != null) |
| return false; |
| } else if (!uri.equals(otherUri)) |
| return false; |
| } catch (URISyntaxException e) { |
| // otherwise fall back to url |
| if (url == null) { |
| if (other.url != null) |
| return false; |
| } else if (!url.equals(other.url)) |
| return false; |
| } |
| return true; |
| } |
| } |
| |
| /** |
| * The Class Translation. |
| */ |
| public static class Translation { |
| |
| /** The i18n key. */ |
| private final String i18nKey; |
| |
| /** The i18n value. */ |
| private final String i18nValue; |
| |
| /** The resource description. */ |
| private final ResourceDescription resourceDescription; |
| |
| /** The priority. */ |
| @SuppressWarnings("unused") |
| private final int priority; |
| |
| /** |
| * Instantiates a new translation. |
| * |
| * @param i18nKey |
| * the i18n key |
| * @param i18nValue |
| * the i18n value |
| * @param resourceDescription |
| * the resource description |
| * @param priority |
| * the priority |
| */ |
| public Translation(String i18nKey, String i18nValue, |
| ResourceDescription resourceDescription, int priority) { |
| super(); |
| this.i18nKey = i18nKey; |
| this.i18nValue = i18nValue; |
| this.resourceDescription = resourceDescription; |
| this.priority = priority; |
| } |
| |
| /** |
| * Returns the i18nKey. |
| * |
| * @return the i18n key |
| */ |
| public String getI18nKey() { |
| return i18nKey; |
| } |
| |
| /** |
| * Returns the i18nValue. |
| * |
| * @return the i18n value |
| */ |
| public String getI18nValue() { |
| return i18nValue; |
| } |
| |
| /** |
| * Returns the locale where key and value had been found. |
| * |
| * @return the locale |
| */ |
| public Locale getLocale() { |
| return resourceDescription.getLocale(); |
| } |
| |
| /** |
| * Returns the resource description, where the i18n entry was contained. |
| * |
| * @return the resource description |
| */ |
| public ResourceDescription getResourceDescription() { |
| return resourceDescription; |
| } |
| |
| } |
| } |