zcommon/src/main/java/org/zkoss/util/resource/impl/LabelLoaderImpl.java
/* LabelLoader.java
Purpose:
Description:
History:
Mon Jun 12 13:05:05 2006, Created by tomyeh
Copyright (C) 2006 Potix Corporation. All Rights Reserved.
{{IS_RIGHT
}}IS_RIGHT
*/
package org.zkoss.util.resource.impl;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zkoss.lang.Library;
import org.zkoss.lang.SystemException;
import org.zkoss.util.FilterMap;
import org.zkoss.util.Locales;
import org.zkoss.util.Maps;
import org.zkoss.util.WaitLock;
import org.zkoss.util.resource.ClassLocator;
import org.zkoss.util.resource.LabelLocator;
import org.zkoss.util.resource.LabelLocator2;
import org.zkoss.xel.Expression;
import org.zkoss.xel.ExpressionFactory;
import org.zkoss.xel.Expressions;
import org.zkoss.xel.VariableResolver;
import org.zkoss.xel.VariableResolverX;
import org.zkoss.xel.util.SimpleXelContext;
/**
* The label loader (implementation only).
*
* Used to implement {@link org.zkoss.util.resource.Labels}.
*
* <p>Notice that the encoding of the Locale dependent file (*.properties)
* is assumed to be UTF-8. If it is not the case, please refer to
* <a href="http://books.zkoss.org/wiki/ZK_Configuration_Reference/zk.xml/The_library-property_Element/Library_Properties#org.zkoss.util.label.web.charset">ZK Configuration Reference</a>
* for more information.
* <p> Specify the library property of <code>org.zkoss.util.resource.LabelLoader.class</code>
* in zk.xml to provide a customized label loader for debugging purpose. (since 7.0.1)
* @author tomyeh
*/
public class LabelLoaderImpl implements LabelLoader {
private static final Logger log = LoggerFactory.getLogger(LabelLoaderImpl.class);
/** A map of (Locale l, Map(String key, ExValue label)).
* We use two maps to speed up the access of labels.
* _labels allows concurrent access without synchronization.
* _syncLabels requires synchronization and used for update.
*/
private Map<Locale, Map<String, ExValue>> _labels = Collections.emptyMap();
/** A map of (Locale 1, Map<String key1, Map<String key2...> or ExValue label>)
* It is used by variable resolver and allows ${labels.key1.key2}.
* _segLabels allows concurrent access without synchronization.
* See also {@link #getSegmentedLabels}.
*/
private Map<Locale, Map<String, Object>> _segLabels = Collections.emptyMap();
/** Map<Locale, Map<String, String>>.
*/
private final Map<Locale, Object> _syncLabels = new HashMap<Locale, Object>(8);
/** A set of LabelLocator or LabelLocator2. */
private final Set<Object> _locators = new LinkedHashSet<Object>(4); //order is important
/** The XEL context. */
private final SimpleXelContext _xelc;
private String _jarcharset, _warcharset;
private final ExpressionFactory _expf;
private final FilterMap.Filter _fmfilter;
public LabelLoaderImpl() {
_fmfilter = new FilterMap.Filter() {
public Object filter(Object key, Object value) {
Object o = value instanceof ExValue ?
((ExValue)value).getValue(): value;
if (o == null) {
// it's possible like missing a label.
o = handleMissingLabel(key);
}
return o;
}
};
_expf = Expressions.newExpressionFactory();
_xelc = new SimpleXelContext(new Resolver(), null);
}
/**
* Handles the missing label. The default action is logging a message.
*
* @param key the specified key
* @return the fallback value, or null if no fallback value available
* @since 8.6.0
*/
protected Object handleMissingLabel(Object key) {
log.debug("The key of [{}] is not found in labels!", key);
return null;
}
/** Returns the label of the specified key for the current locale,
* or null if not found.
* @see #getSegmentedLabels
*/
public String getLabel(String key) {
return getLabel(Locales.getCurrent(), key);
}
/** Returns the label of the specified key for the specified locale,
* or null if not found.
* @since 5.0.7
*/
public String getLabel(Locale locale, String key) {
Map<String, ExValue> map = _labels.get(locale);
if (map == null)
map = loadLabels(locale);
final ExValue exVal = map.get(key);
return exVal != null ? exVal.getValue(): null;
}
/** Returns a map of segmented labels for the current locale (never null).
* Unlike {@link #getLabel}, if a key of the label contains dot, it will
* be split into multiple keys and then grouped into map. It is so-called
* segmented.
* <p>For example, the following property file will parsed into a couple of maps,
* and <code>getSegmentedLabels()</code> returns a map containing
* a single entry. The entry's key is <code>"a"</code> and the value
* is another map with two entries <code>"b"</code> and <code>"c"</code>.
* And, the value for <code>"b"</code> is another two-entries map (containing
* <code>"c"</code> and <code>"d"</code>).
* <pre><code>
* a.b.c=1
* a.b.d=2
* a.e=3</pre></code>
* <p>This method is designed to make labels easier to be accessed in
* EL expressions.
* <p>On the other hand, {@link #getLabel} does not split them, and
* you could access them by, say, <code>getLabel("a.b.d")</code>.
* @since 5.0.7
*/
public Map<String, Object> getSegmentedLabels() {
return getSegmentedLabels(Locales.getCurrent());
}
/** Returns a map of segmented labels for the specified locale (never null).
* Refer to {@link #getSegmentedLabels()} for details.
* @since 5.0.7
*/
public Map<String, Object> getSegmentedLabels(Locale locale) {
final Map<String, Object> map = _segLabels.get(locale);
if (map != null)
return map;
loadLabels(locale);
return _segLabels.get(locale);
}
/** Sets the variable resolver, which is used if an EL expression
* is specified.
*
* @since 3.0.0
*/
public VariableResolver setVariableResolver(VariableResolver resolv) {
final Resolver resolver = (Resolver)_xelc.getVariableResolver();
final VariableResolver old = resolver.custom;
resolver.custom = resolv;
return old;
}
/** Registers a locator which is used to load the Locale-dependent labels
* from other resource, such as servlet contexts.
*/
public void register(LabelLocator locator) {
register0(locator);
}
/** Registers a locator which is used to load the Locale-dependent labels
* from other resource, such as database.
* @since 5.0.5
*/
public void register(LabelLocator2 locator) {
register0(locator);
}
private void register0(Object locator) {
if (locator == null)
throw new NullPointerException("locator");
synchronized (_locators) {
if (!_locators.add(locator))
log.warn("Replace the old one, because it is replicated: "+locator);
}
reset(); //Labels might be loaded before, so...
}
/** Resets all cached labels and next call to {@link #getLabel}
* will cause re-loading the Locale-dependent labels.
*/
public void reset() {
synchronized (_syncLabels) {
_syncLabels.clear();
_segLabels = Collections.emptyMap();
_labels = Collections.emptyMap();
}
}
//-- private utilities --//
/** Returns Map(String key, ExValue label) of the specified locale.
*/
@SuppressWarnings("unchecked")
private final Map<String, ExValue> loadLabels(Locale locale) {
WaitLock lock = null;
for (;;) {
final Object o;
synchronized (_syncLabels) {
o = _syncLabels.get(locale);
if (o == null)
_syncLabels.put(locale, lock = new WaitLock()); //lock it
}
if (o instanceof Map)
return (Map)o;
if (o == null)
break; //go to load the page
//wait because some one is creating the servlet
if (!((WaitLock)o).waitUntilUnlock(5*60*1000))
log.warn("Take too long to wait loading labels: "+locale
+"\nTry to load again automatically...");
} //for(;;)
if (_jarcharset == null)
_jarcharset = Library.getProperty("org.zkoss.util.label.classpath.charset", "UTF-8");
if (_warcharset == null) {
_warcharset = Library.getProperty("org.zkoss.util.label.web.charset", null);
if (_warcharset == null)
_warcharset = Library.getProperty("org.zkoss.util.label.WEB-INF.charset", "UTF-8"); //backward compatible
}
try {
//get the class name
if (locale != null)
log.info("Loading labels for "+locale);
Map labels = new HashMap(512);
//1. load from modules
final ClassLocator locator = new ClassLocator();
for (Enumeration en = locator.getResources(
locale == null ? "metainfo/zk-label.properties":
"metainfo/zk-label_" + locale + ".properties");
en.hasMoreElements();) {
final URL url = (URL)en.nextElement();
load(labels, url, _jarcharset);
}
//2. load from extra resource
final List locators;
synchronized (_locators) {
locators = new LinkedList(_locators);
}
for (Iterator it = locators.iterator(); it.hasNext();) {
Object o = it.next();
if (o instanceof LabelLocator) {
final URL url = ((LabelLocator)o).locate(locale);
if (url != null)
load(labels, url, _warcharset);
} else {
final LabelLocator2 loc = (LabelLocator2)o;
final InputStream is = loc.locate(locale);
if (is != null) {
final String cs = loc.getCharset();
load(labels, is, cs != null ? cs: _warcharset);
}
}
}
//Convert values to ExValue
toExValue(labels);
//merge with labels from 'super' locale
if (locale != null) {
final String lang = locale.getLanguage();
final String cnty = locale.getCountry();
final String var = locale.getVariant();
final Map superlabels = loadLabels(
var != null && var.length() > 0 ? new Locale(lang, cnty):
cnty != null && cnty.length() > 0 ? new Locale(lang, ""): null);
if (labels.isEmpty()) {
labels = superlabels.isEmpty() ?
Collections.EMPTY_MAP: superlabels;
} else if (!superlabels.isEmpty()) {
Map combined = new HashMap(superlabels);
combined.putAll(labels);
labels = combined;
}
}
//add to map
synchronized (_syncLabels) {
_syncLabels.put(locale, labels);
cloneLables();
}
return labels;
} catch (Throwable ex) {
synchronized (_syncLabels) {
_syncLabels.remove(locale);
cloneLables();
}
throw SystemException.Aide.wrap(ex);
} finally {
lock.unlock(); //unlock (always unlock to avoid deadlock)
}
}
@SuppressWarnings("unchecked")
private void toExValue(Map labels) {
if (!labels.isEmpty())
for (Iterator it = labels.entrySet().iterator(); it.hasNext();) {
final Map.Entry me = (Map.Entry) it.next();
String value = expendValue(labels, (String) me.getValue());
me.setValue(new ExValue(value));
}
}
//expend ${} EL in labels recursively
private String expendValue(Map labels, String value) {
if (labels != null && value != null) {
int offset = 0;
while (offset < value.length()) {
int start = value.indexOf("${", offset);
if (start != -1) {
int end = value.indexOf("}", start);
if (end != -1) {
String exp = value.substring(start, end + 1);
String expStr = exp.substring(2, exp.length() - 1);
if (expStr.endsWith(".$"))
expStr = expStr.substring(0, expStr.length() - 2);
Object expend = labels.get(expStr);
String expended = "";
if (expend instanceof String)
expended = expendValue(labels, (String) expend);
else if (expend instanceof ExValue)
expended = expendValue(labels, ((ExValue) expend).getValue());
value = value.substring(0, start) + expended + value.substring(start + exp.length());
offset = start + expended.length();
} else {
break;
}
} else {
break;
}
}
}
return value;
}
//Copy _syncLabels to _labels. It must be called in synchronized(_syncLabels)
@SuppressWarnings("unchecked")
private void cloneLables() {
final Map labels = new HashMap(),
segLabels = new HashMap();
for (Iterator it = _syncLabels.entrySet().iterator(); it.hasNext();) {
final Map.Entry me = (Map.Entry)it.next();
final Object value = me.getValue();
if (value instanceof Map) {
final Object key = me.getKey();
labels.put(key, value);
segLabels.put(key, segment((Map)value));
}
}
_labels = labels;
_segLabels = segLabels;
}
@SuppressWarnings("unchecked")
private Map segment(Map map) {
for (Iterator it = map.keySet().iterator(); it.hasNext();) {
final String key = (String)it.next();
if (key.indexOf(".") >= 0)
return segmentInner(new HashMap(map)); //clone since we'll modify it
}
return new FilterMap(map, _fmfilter); //no special key
}
@SuppressWarnings("unchecked")
private FilterMap segmentInner(Map map) {
final Map segFound = new HashMap();
for (Iterator it = map.entrySet().iterator(); it.hasNext();) {
final Map.Entry me = (Map.Entry)it.next();
final String key = (String)me.getKey();
final Object val = me.getValue();
final int index = key.indexOf('.');
if (index >= 0) {
it.remove(); //remove it
final String newkey = key.substring(0, index);
Map vals = (Map)segFound.get(newkey);
if (vals == null)
segFound.put(newkey, vals = new HashMap());
vals.put(key.substring(index + 1), val);
}
}
for (Iterator it = segFound.entrySet().iterator(); it.hasNext();) {
final Map.Entry me = (Map.Entry)it.next();
final FilterMap seged;
Object o = map.put(me.getKey(), seged = segmentInner((Map)me.getValue()));
if (o != null && !(o instanceof Map)/*just in case*/) {
final Map m = seged.getOrigin();
o = m.put("$", o);
if (o != null)
m.put("$", o); //restore
}
}
return new FilterMap(map, _fmfilter);
}
/** Loads all labels from the specified URL. */
private static final void load(Map<String, String> labels, URL url, String charset)
throws IOException {
log.info("Opening "+url); //don't use MCommon since Messages might call getLabel
load(labels, url.openStream(), charset);
}
/** Loads all labels from the specified URL. */
private static final void load(Map<String, String> labels, InputStream is, String charset)
throws IOException {
final Map<String, String> news = new HashMap<String, String>();
try {
Maps.load(news, is, charset);
} finally {
try {is.close();} catch (Throwable ex) {}
}
for (Map.Entry<String, String> me: news.entrySet()) {
labels.put(me.getKey(), me.getValue());
}
}
private class ExValue {
private Expression _expr;
private String _val;
public ExValue(String val) {
int j;
if ((j = val.indexOf("${")) >= 0 && val.indexOf('}', j + 2) >= 0) {
try {
_expr = _expf.parseExpression(_xelc, val, String.class);
return;
} catch (Throwable ex) {
log.error("Illegal expression: "+val, ex);
}
}
_expr = null;
_val = val;
}
public String getValue() {
return _expr != null ? (String)_expr.evaluate(_xelc): _val;
}
}
private class Resolver implements VariableResolver {
private VariableResolver custom;
public Object resolveVariable(String name) {
if (custom != null) {
final Object o = custom instanceof VariableResolverX ?
((VariableResolverX)custom).resolveVariable(null, null, name):
custom.resolveVariable(name);
if (o != null)
return o;
}
return getSegmentedLabels().get(name);
}
}
}