blob: 9b245c811bbcacbd8684c4afad6ffceb1be85ec4 [file] [log] [blame]
/**
* 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 v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* 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;
}
}
}