001: /*
002:
003: This software is OSI Certified Open Source Software.
004: OSI Certified is a certification mark of the Open Source Initiative.
005:
006: The license (Mozilla version 1.0) can be read at the MMBase site.
007: See http://www.MMBase.org/license
008:
009: */
010: package org.mmbase.util;
011:
012: import java.util.*;
013: import org.mmbase.util.logging.*;
014: import org.mmbase.util.xml.DocumentReader;
015: import org.w3c.dom.*;
016:
017: /**
018: *<p>
019: * A String which is localized. There are two mechanisms to find and provide translations: They can
020: * explicitely be set with {@link #set} (e.g. during parsing an XML), or a resource-bundle can be
021: * associated with {@link #setBundle}, which will be used to find translations based on the key of
022: * this object.
023: *</p>
024: *<p>
025: * The 'set' mechanism can also be driven by {@link #fillFromXml}, which provides a sensible way to fill the LocalizedString with
026: * setting from a sub element of XMLs.
027: *</p>
028: *<p>
029: * The idea is that objects of this type can be used in stead of normal String objects, for error
030: * messages, descriptions and other texts which need localization (e.g. because they are exposed to
031: * end-users).
032: *</p>
033: *
034: * @author Michiel Meeuwissen
035: * @version $Id: LocalizedString.java,v 1.31 2007/05/23 13:19:59 michiel Exp $
036: * @since MMBase-1.8
037: */
038: public class LocalizedString implements java.io.Serializable, Cloneable {
039:
040: private static final Logger log = Logging
041: .getLoggerInstance(LocalizedString.class);
042:
043: private static final long serialVersionUID = 1L;
044:
045: private static Locale defaultLocale = null; // means 'system default' and 'unset'.
046:
047: /**
048: * Sets a default locale for this JVM or web-app. When not using it, the locale is the system
049: * default. Several web-apps do run in one JVM however and it is very imaginable that you want a
050: * different default for the Locale.
051: *
052: * So, this function can be called only once. Calling it the second time will not do
053: * anything. It returns the already set default locale then, which should probably prompt you to log an error
054: * or throw an exception or so. Otherwise it returns <code>null</code> indicating that the
055: * default locale is now what you just set.
056: */
057: public static Locale setDefault(Locale locale) {
058: if (defaultLocale != null)
059: return defaultLocale;
060: defaultLocale = locale;
061: return null;
062: }
063:
064: /**
065: * Returns the default locale if set, or otherwise the system default ({@link java.util.Locale#getDefault}).
066: */
067: public static Locale getDefault() {
068: return defaultLocale != null ? defaultLocale : Locale
069: .getDefault();
070: }
071:
072: /**
073: * Converts a collection of localized strings to a collection of normal strings.
074: * @param col Collection of LocalizedString objects
075: * @param locale Locale to be used for the call to {@link #get(Locale)} which obviously is needed
076: */
077: public static Collection<String> toStrings(
078: Collection<LocalizedString> col, Locale locale) {
079: Collection<String> res = new ArrayList<String>();
080: for (LocalizedString s : col) {
081: res.add(s.get(locale));
082: }
083: return res;
084: }
085:
086: private String key;
087:
088: private Map<Locale, String> values = null;
089: private String bundle = null;
090:
091: // just for the contract of Serializable
092: protected LocalizedString() {
093: }
094:
095: /**
096: * @param k The key of this String, if k == <code>null</code> then the first set will define it.
097: */
098: public LocalizedString(String k) {
099: key = k;
100: }
101:
102: /**
103: * Gets the key to use as a default and/or for obtaining a value from the bundle
104: */
105: public String getKey() {
106: return key;
107: }
108:
109: /**
110: * Sets the key to use as a default and/or for obtaining a value from the bundle
111: */
112: public void setKey(String key) {
113: this .key = key;
114: }
115:
116: /**
117: * Gets the value for a certain locale. If no match is found, it falls back to the key.
118: */
119: public String get(Locale locale) {
120: if (locale == null) {
121: locale = defaultLocale == null ? Locale.getDefault()
122: : defaultLocale;
123: }
124: if (values != null) {
125: String result = values.get(locale);
126:
127: if (result != null)
128: return result;
129:
130: String variant = locale.getVariant();
131: String country = locale.getCountry();
132: String language = locale.getLanguage();
133:
134: if (!"".equals(variant)) {
135: result = values.get(new Locale(language, country));
136: if (result != null)
137: return result;
138: }
139:
140: if (!"".equals(country)) {
141: result = values.get(new Locale(language));
142: if (result != null)
143: return result;
144: }
145:
146: // Some LocalizedString instances may have a default value stored with the key 'null'
147: // instead of the locale from MMBase. This is the case for values stored while the
148: // MMBase module was not yet active.
149: // This code 'fixes' that reference.
150: // It's not nice, but as a proper fix likely requires a total rewrite of Module.java and
151: // MMBase.java, this will have to do for the moment.
152: if (locale.equals(defaultLocale)) {
153: result = values.get(null);
154: if (result != null) {
155: values.put(locale, result);
156: return result;
157: }
158: }
159: }
160:
161: if (bundle != null) {
162: try {
163: return ResourceBundle.getBundle(bundle, locale)
164: .getString(key);
165: } catch (MissingResourceException mre) {
166: // fall back to key.
167: if (log.isDebugEnabled()) {
168: log.debug("Cannot get resource from bundle: "
169: + bundle + ", key: " + key);
170: }
171: }
172: }
173:
174: return key;
175: }
176:
177: /**
178: * Sets the value for a certain locale. If the value for a more general locale is still unset,
179: * it will also set that (so, it sets also nl when setting nl_BE if nl still is unset).
180: */
181: public void set(final String value, Locale locale) {
182: if (key == null)
183: key = value;
184:
185: if (values == null) {
186: values = new HashMap<Locale, String>();
187: }
188:
189: if (locale == null) {
190: locale = defaultLocale;
191: }
192:
193: values.put(locale, value);
194:
195: if (locale != null) {
196: String variant = locale.getVariant();
197: String country = locale.getCountry();
198: String language = locale.getLanguage();
199: if (!"".equals(variant)) {
200: Locale loc = new Locale(language, country);
201: if (values.get(loc) == null) {
202: values.put(loc, value);
203: }
204: }
205: if (!"".equals(country)) {
206: Locale loc = new Locale(language);
207: if (values.get(loc) == null) {
208: values.put(loc, value);
209: }
210: }
211: }
212: }
213:
214: /**
215: * Returns a Map representation of the localisation setting represented by this
216: * LocalizedString. It is an unmodifiable mapping: Locale -> localized value.
217: */
218: public Map<Locale, String> asMap() {
219: if (values == null)
220: return Collections.emptyMap();
221: return Collections.unmodifiableMap(values);
222: }
223:
224: /**
225: * A resource-bundle with given name can be associated to this LocalizedString. If no
226: * translations were explicitely added, it can be used to look up the translation in the bundle,
227: * using the key.
228: */
229:
230: public void setBundle(String b) {
231: bundle = b;
232: }
233:
234: /**
235: * {@inheritDoc}
236: *
237: * For LocalizedString this returns the String for the default Locale (see {@link #getDefault}).
238: */
239: public String toString() {
240: return get((Locale) null);
241: }
242:
243: /**
244: * This utility takes care of reading the xml:lang attribute from an element
245: * @param element a DOM element
246: * @return A {@link java.util.Locale} object, or <code>null</code> if the element did not have,
247: * or had an empty, xml:lang attribute
248: */
249: public static Locale getLocale(Element element) {
250: return getLocale(element.getAttribute("xml:lang"));
251: }
252:
253: /**
254: * @since MMBase-1.8.1
255: */
256: public static Locale getLocale(String xmlLang) {
257: Locale loc = null;
258: if (xmlLang != null && (!xmlLang.equals(""))) {
259:
260: String[] split = xmlLang.split("[-_]", 3);
261: if (split.length == 1) {
262: loc = new Locale(split[0]);
263: } else if (split.length == 2) {
264: loc = new Locale(split[0], split[1]);
265: } else {
266: loc = new Locale(split[0], split[1], split[2]);
267: }
268: }
269: return loc;
270: }
271:
272: /**
273: * Degrades a Locale object to a more general Locale. Principally this means that first the
274: * 'variant' will be dropped and then the country. As an extra the 'variant' is also degraded
275: * progressively. This is done by taking away parts (from the end) which are separated by
276: * underscore characters. Also, after degrading the country, also locales are tried with no
277: * country, but with a variant only.
278: * So e.g. nl_BE_a_b is degraded to nl_BE_a, then nl_BE, then nl__a_b, then nl__a, then nl.
279: *
280: * @param locale The locale to be degraded
281: * @param originalLocale The original locale (used to find back the original variant after
282: * dropping the country)
283: * @return A degraded Locale of <code>null</code> if the locale could not be degraded any further.
284: *
285: * @since MMBase-1.8.5
286: */
287: public static Locale degrade(Locale locale, Locale originalLocale) {
288: String language = locale.getLanguage();
289: String country = locale.getCountry();
290: String variant = locale.getVariant();
291: if (variant != null && !"".equals(variant)) {
292: String[] var = variant.split("_");
293: if (var.length > 1) {
294: StringBuilder v = new StringBuilder(var[0]);
295: for (int i = 1; i < var.length - 1; i++) {
296: v.append('_');
297: v.append(var[i]);
298: }
299: return new Locale(language, country, v.toString());
300: } else {
301: return new Locale(language, country);
302: }
303: }
304: if (!"".equals(country)) {
305: String originalVariant = originalLocale.getVariant();
306: if (originalVariant != null && !"".equals(originalVariant)) {
307: return new Locale(language, "", originalVariant);
308: } else {
309: return new Locale(language);
310: }
311: }
312: // cannot be degraded any more.
313: return null;
314: }
315:
316: /**
317: * This utility determines the value of an xml:lang attribute. So, given a {@link java.util.Locale}
318: * it produces a String.
319: * @param locale A java locale
320: * @return A string that can be used as the value for an XML xml:lang attribute.
321: * @since MMBase-1.8.1
322: */
323: public static String getXmlLang(Locale locale) {
324: if (locale == null)
325: return null;
326: StringBuffer lang = new StringBuffer(locale.getLanguage());
327: String country = locale.getCountry();
328: if (country.length() > 0) {
329: lang.append("-").append(country);
330: String variant = locale.getVariant();
331: if (variant != null && variant.length() > 0) {
332: lang.append("-").append(variant);
333: }
334: }
335: return lang.toString();
336: }
337:
338: /**
339: * This utility takes care of setting the xml:lang attribute on an element.
340: * @param element Element on which the xml:lang attribute is going to be set
341: * @param locale Java's Locale object
342: * @since MMBase-1.8.1
343: */
344: public static void setXmlLang(Element element, Locale locale) {
345: String xmlLang = getXmlLang(locale);
346: if (xmlLang != null) {
347: element.setAttribute("xml:lang", xmlLang);
348: }
349: }
350:
351: /**
352: * Given a certain tagname, and a DOM parent element, it configures this LocalizedString, using
353: * subtags with this tagname with 'xml:lang' attributes. This boils down to repeative calls to {@link #set(String, Locale)}.
354: */
355:
356: public void fillFromXml(final String tagName, final Element element) {
357: if (element == null)
358: return;
359: NodeList childNodes = element.getChildNodes();
360: for (int k = 0; k < childNodes.getLength(); k++) {
361: if (childNodes.item(k) instanceof Element) {
362: Element childElement = (Element) childNodes.item(k);
363: if (tagName.equals(childElement.getLocalName())) {
364: Locale locale = getLocale(childElement);
365: String description = DocumentReader
366: .getNodeTextValue(childElement);
367: set(description, locale);
368: }
369: }
370: }
371: }
372:
373: /**
374: * Writes this LocalizedString object back to an XML, i.e. it searches for and creates
375: * sub-elements (identified by xml:lang attributes) of a certain given parent element, and sets
376: * the node-text-value of those elements corresponding to the locale.
377: * @param tagName Tag-name of the to be used sub-elements
378: * @param ns Namespace of the to be created sub-elements, or <code>null</code>
379: * @param element The parent element which must contain the localized elements.
380: * @param path A comma separated list of names of tags which must skipped, before appending
381: * childs. See {@link org.mmbase.util.xml.DocumentReader#appendChild(Element, Element, String)}.
382: *
383: * @since MMBase-1.8.1
384: */
385: public void toXml(final String tagName, final String ns,
386: final Element element, final String path) {
387: if (values != null) { // if no explicit values, nothing can be done
388:
389: // what if there are corresponding elements already:
390: org.w3c.dom.NodeList nl = element
391: .getElementsByTagName(tagName);
392: for (Map.Entry<Locale, String> entry : values.entrySet()) {
393: Locale loc = entry.getKey();
394: String value = entry.getValue();
395: String xmlLang = getXmlLang(loc);
396: // look if such an element is available
397: Element child = null;
398: for (int j = 0; j < nl.getLength(); j++) {
399: Element cand = (Element) nl.item(j);
400: if (cand.getAttribute("xml:lang").equals(xmlLang)) {
401: child = cand;
402: break;
403: }
404: }
405: if (child == null) {
406: if (ns != null) {
407: child = element.getOwnerDocument()
408: .createElementNS(ns, tagName);
409: } else {
410: child = element.getOwnerDocument()
411: .createElement(tagName);
412: }
413: DocumentReader.appendChild(element, child, path);
414: setXmlLang(child, loc);
415: }
416: DocumentReader.setNodeTextValue(child, value);
417: }
418: }
419: }
420:
421: public Object clone() {
422: try {
423: LocalizedString clone = (LocalizedString) super .clone();
424: if (values != null) {
425: clone.values = (Map) ((HashMap) values).clone();
426: }
427: return clone;
428: } catch (CloneNotSupportedException cnse) {
429: // should not happen
430: log.error("Cannot clone this LocalizedString");
431: throw new RuntimeException(
432: "Cannot clone this LocalizedString", cnse);
433: }
434: }
435:
436: public boolean equals(Object o) {
437: if (o instanceof LocalizedString) {
438: LocalizedString os = (LocalizedString) o;
439: return key.equals(os.key)
440: && (values == null ? os.values == null : values
441: .equals(os.values))
442: && (bundle == null ? os.bundle == null : bundle
443: .equals(os.bundle));
444: } else {
445: return false;
446: }
447: }
448:
449: public int hashCode() {
450: int result = 0;
451: result = HashCodeUtil.hashCode(result, key);
452: result = HashCodeUtil.hashCode(result, values);
453: result = HashCodeUtil.hashCode(result, bundle);
454: return result;
455: }
456:
457: }
|