001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.i18n;
029
030import org.opencms.main.CmsLog;
031import org.opencms.util.CmsDateUtil;
032import org.opencms.util.CmsStringUtil;
033
034import java.text.DateFormat;
035import java.text.MessageFormat;
036import java.util.Date;
037import java.util.Locale;
038import java.util.MissingResourceException;
039import java.util.ResourceBundle;
040import java.util.function.Function;
041
042import org.apache.commons.logging.Log;
043
044/**
045 * Reads localized resource Strings from a <code>java.util.ResourceBundle</code>
046 * and provides convenience methods to access the Strings from a template.<p>
047 *
048 * This class is frequently used from JSP templates. Because of that, throwing of
049 * exceptions related to the access of the resource bundle are suppressed
050 * so that a template always execute. The class provides an {@link #isInitialized()} method
051 * that can be checked to see if the instance was properly initialized.<p>
052 *
053 * @since 6.0.0
054 */
055public class CmsMessages {
056
057    /** The suffix of a "short" localized key name. */
058    public static final String KEY_SHORT_SUFFIX = ".short";
059
060    /** Prefix / Suffix for unknown keys. */
061    public static final String UNKNOWN_KEY_EXTENSION = "???";
062
063    /** The log object for this class. */
064    private static final Log LOG = CmsLog.getLog(CmsMessages.class);
065
066    /** The resource bundle base name this object was initialized with. */
067    private String m_bundleName;
068
069    /** The locale to use for looking up the messages from the bundle. */
070    private Locale m_locale;
071
072    /** The resource bundle this message object was initialized with. */
073    private ResourceBundle m_resourceBundle;
074
075    /**
076     * Constructor for the messages with an initialized <code>java.util.Locale</code>.
077     *
078     * @param bundleName the base ResourceBundle name
079     * @param locale the m_locale to use, eg. "de", "en" etc.
080     */
081    public CmsMessages(String bundleName, Locale locale) {
082
083        try {
084            m_locale = locale;
085            m_bundleName = bundleName;
086            m_resourceBundle = CmsResourceBundleLoader.getBundle(bundleName, m_locale);
087        } catch (MissingResourceException e) {
088            m_resourceBundle = null;
089        } catch (Exception e) {
090            m_resourceBundle = null;
091            LOG.error("Error creating messages for bundle " + bundleName + " for locale " + m_locale);
092        }
093    }
094
095    /**
096     * Constructor for the messages with a language string.<p>
097     *
098     * The <code>language</code> is a 2 letter language ISO code, e.g. <code>"EN"</code>.<p>
099     *
100     * The Locale for the messages will be created like this:<br>
101     * <code>new Locale(language, "", "")</code>.<p>
102     *
103     * @param bundleName the base ResourceBundle name
104     * @param language ISO language indentificator for the m_locale of the bundle
105     */
106    public CmsMessages(String bundleName, String language) {
107
108        this(bundleName, language, "", "");
109    }
110
111    /**
112     * Constructor for the messages with language and country code strings.<p>
113     *
114     * The <code>language</code> is a 2 letter language ISO code, e.g. <code>"EN"</code>.
115     * The <code>country</code> is a 2 letter country ISO code, e.g. <code>"us"</code>.<p>
116     *
117     * The Locale for the messages will be created like this:<br>
118     * <code>new Locale(language, country, "")</code>.
119     *
120     * @param bundleName the base ResourceBundle name
121     * @param language ISO language indentificator for the m_locale of the bundle
122     * @param country ISO 2 letter country code for the m_locale of the bundle
123     */
124    public CmsMessages(String bundleName, String language, String country) {
125
126        this(bundleName, language, country, "");
127    }
128
129    /**
130     * Constructor for the messages with language, country code and variant strings.<p>
131     *
132     * The <code>language</code> is a 2 letter language ISO code, e.g. <code>"EN"</code>.
133     * The <code>country</code> is a 2 letter country ISO code, e.g. <code>"us"</code>.
134     * The <code>variant</code> is a vendor or browser-specific code, e.g. <code>"POSIX"</code>.<p>
135     *
136     * The Locale for the messages will be created like this:<br>
137     * <code>new Locale(language, country, variant)</code>.
138     *
139     * @param bundleName the base ResourceBundle name
140     * @param language language indentificator for the m_locale of the bundle
141     * @param country 2 letter country code for the m_locale of the bundle
142     * @param variant a vendor or browser-specific variant code
143     */
144    public CmsMessages(String bundleName, String language, String country, String variant) {
145
146        this(bundleName, new Locale(language, country, variant));
147    }
148
149    /**
150     * Empty constructor for subclassing.<p>
151     */
152    protected CmsMessages() {
153
154        // empty constructor for subclassing
155    }
156
157    /**
158     * Formats an unknown key.<p>
159     *
160     * @param keyName the key to format
161     * @return the formatted unknown key
162     *
163     * @see #isUnknownKey(String)
164     */
165    public static String formatUnknownKey(String keyName) {
166
167        StringBuffer buf = new StringBuffer(64);
168        buf.append(UNKNOWN_KEY_EXTENSION);
169        buf.append(" ");
170        buf.append(keyName);
171        buf.append(" ");
172        buf.append(UNKNOWN_KEY_EXTENSION);
173        return buf.toString();
174    }
175
176    /**
177     * Returns <code>true</code> if the provided value matches the scheme
178     * <code>"??? " + keyName + " ???"</code>, that is the value appears to be an unknown key.<p>
179     *
180     * Also returns <code>true</code> if the given value is <code>null</code>.<p>
181     *
182     * @param value the value to check
183     * @return true if the value is matches the scheme for unknown keys
184     *
185     * @see #formatUnknownKey(String)
186     */
187    public static boolean isUnknownKey(String value) {
188
189        return (value == null) || (value.startsWith(UNKNOWN_KEY_EXTENSION));
190    }
191
192    /**
193     * @see java.lang.Object#equals(java.lang.Object)
194     */
195    @Override
196    public boolean equals(Object obj) {
197
198        if (obj == this) {
199            return true;
200        }
201        if (obj instanceof CmsMultiMessages) {
202            return false;
203        }
204        if (obj instanceof CmsMessages) {
205            CmsMessages other = (CmsMessages)obj;
206            return other.getBundleName().equals(m_bundleName) && other.getLocale().equals(m_locale);
207        }
208        return false;
209    }
210
211    /**
212     * Returns a formated date String from a Date value,
213     * the format being {@link DateFormat#SHORT} and the locale
214     * based on this instance.<p>
215     *
216     * @param date the Date object to format as String
217     * @return the formatted date
218     */
219    public String getDate(Date date) {
220
221        return CmsDateUtil.getDate(date, DateFormat.SHORT, m_locale);
222    }
223
224    /**
225     * Returns a formated date String from a Date value,
226     * the formatting based on the provided option and the locale
227     * based on this instance.<p>
228     *
229     * @param date the Date object to format as String
230     * @param format the format to use, see {@link CmsMessages} for possible values
231     * @return the formatted date
232     */
233    public String getDate(Date date, int format) {
234
235        return CmsDateUtil.getDate(date, format, m_locale);
236    }
237
238    /**
239     * Returns a formated date String from a timestamp value,
240     * the format being {@link DateFormat#SHORT} and the locale
241     * based on this instance.<p>
242     *
243     * @param time the time value to format as date
244     * @return the formatted date
245     */
246    public String getDate(long time) {
247
248        return CmsDateUtil.getDate(new Date(time), DateFormat.SHORT, m_locale);
249    }
250
251    /**
252     * Returns a formated date and time String from a Date value,
253     * the format being {@link DateFormat#SHORT} and the locale
254     * based on this instance.<p>
255     *
256     * @param date the Date object to format as String
257     * @return the formatted date and time
258     */
259    public String getDateTime(Date date) {
260
261        return CmsDateUtil.getDateTime(date, DateFormat.SHORT, m_locale);
262    }
263
264    /**
265     * Returns a formated date and time String from a Date value,
266     * the formatting based on the provided option and the locale
267     * based on this instance.<p>
268     *
269     * @param date the Date object to format as String
270     * @param format the format to use, see {@link CmsMessages} for possible values
271     * @return the formatted date and time
272     */
273    public String getDateTime(Date date, int format) {
274
275        return CmsDateUtil.getDateTime(date, format, m_locale);
276    }
277
278    /**
279     * Returns a formated date and time String from a timestamp value,
280     * the format being {@link DateFormat#SHORT} and the locale
281     * based on this instance.<p>
282     *
283     * @param time the time value to format as date
284     * @return the formatted date and time
285     */
286    public String getDateTime(long time) {
287
288        return CmsDateUtil.getDateTime(new Date(time), DateFormat.SHORT, m_locale);
289    }
290
291    /**
292     * Returns the locale to use for looking up this messages.<p>
293     *
294     * @return the locale to use for looking up this messages
295     */
296    public Locale getLocale() {
297
298        return m_locale;
299    }
300
301    /**
302     * Returns the resource bundle this message object was initialized with.<p>
303     *
304     * @return the resource bundle this message object was initialized with or null if initialization was not successful
305     */
306    public ResourceBundle getResourceBundle() {
307
308        return m_resourceBundle;
309    }
310
311    /**
312     * Directly calls the getString(String) method of the wrapped ResourceBundle.<p>
313     *
314     * If you use this this class on a template, you should consider using
315     * the {@link #key(String)} method to get the value from the ResourceBundle because it
316     * handles the exception for you in a convenient way.
317     *
318     * @param keyName the key
319     * @return the resource string for the given key
320     *
321     * @throws CmsMessageException in case the key is not found or the bundle is not initialized
322     */
323    public String getString(String keyName) throws CmsMessageException {
324
325        if (m_resourceBundle != null) {
326            try {
327                return m_resourceBundle.getString(keyName);
328            } catch (MissingResourceException e) {
329                throw new CmsMessageException(
330                    Messages.get().container(Messages.ERR_CANT_FIND_RESOURCE_FOR_BUNDLE_2, keyName, m_bundleName));
331            }
332        } else {
333            throw new CmsMessageException(
334                Messages.get().container(Messages.ERR_MESSAGE_BUNDLE_NOT_INITIALIZED_1, m_bundleName));
335        }
336    }
337
338    /**
339     * @see java.lang.Object#hashCode()
340     */
341    @Override
342    public int hashCode() {
343
344        return m_locale.hashCode() + (m_bundleName == null ? 0 : m_bundleName.hashCode());
345    }
346
347    /**
348     * Checks if the bundle was properly initialized.
349     *
350     * @return <code>true</code> if bundle was initialized, <code>false</code> otherwise
351     */
352    public boolean isInitialized() {
353
354        return (m_resourceBundle != null);
355    }
356
357    /**
358     * Indicates that users of this CmsMessages instance should not cache message from it.<p>
359     *
360     * @return true if messages from this CmsMessages instance should not be cached
361     */
362    public boolean isUncacheable() {
363
364        return (m_resourceBundle instanceof CmsVfsResourceBundle);
365    }
366
367    /**
368     * Returns the localized resource string for a given message key.<p>
369     *
370     * If the key was not found in the bundle, the return value is
371     * <code>"??? " + keyName + " ???"</code>. This will also be returned
372     * if the bundle was not properly initialized first.
373     *
374     * @param keyName the key for the desired string
375     * @return the resource string for the given key
376     */
377    public String key(String keyName) {
378
379        return key(keyName, false);
380    }
381
382    /**
383     * Returns the localized resource string for a given message key.<p>
384     *
385     * If the key was not found in the bundle, the return value
386     * depends on the setting of the allowNull parameter. If set to false,
387     * the return value is always a String in the format
388     * <code>"??? " + keyName + " ???"</code>.
389     * If set to true, null is returned if the key is not found.
390     * This will also be returned
391     * if the bundle was not properly initialized first.
392     *
393     * @param keyName the key for the desired string
394     * @param allowNull if true, 'null' is an allowed return value
395     * @return the resource string for the given key
396     */
397    public String key(String keyName, boolean allowNull) {
398
399        try {
400            if (m_resourceBundle != null) {
401                return m_resourceBundle.getString(keyName);
402            }
403        } catch (MissingResourceException e) {
404            // not found, return warning
405            if (allowNull) {
406                return null;
407            }
408        }
409        return formatUnknownKey(keyName);
410    }
411
412    /**
413     * Returns the selected localized message for the initialized resource bundle and locale.<p>
414     *
415     * Convenience method for messages with one argument.<p>
416     *
417     * @param key the message key
418     * @param arg0 the message argument
419     *
420     * @return the selected localized message for the initialized resource bundle and locale
421     */
422    public String key(String key, Object arg0) {
423
424        return key(key, new Object[] {arg0});
425    }
426
427    /**
428     * Returns the selected localized message for the initialized resource bundle and locale.<p>
429     *
430     * Convenience method for messages with two arguments.<p>
431     *
432     * @param key the message key
433     * @param arg0 the first message argument
434     * @param arg1 the second message argument
435     *
436     * @return the selected localized message for the initialized resource bundle and locale
437     */
438    public String key(String key, Object arg0, Object arg1) {
439
440        return key(key, new Object[] {arg0, arg1});
441    }
442
443    /**
444     * Returns the selected localized message for the initialized resource bundle and locale.<p>
445     *
446     * Convenience method for messages with three arguments.<p>
447     *
448     * @param key the message key
449     * @param arg0 the first message argument
450     * @param arg1 the second message argument
451     * @param arg2 the third message argument
452     *
453     * @return the selected localized message for the initialized resource bundle and locale
454     */
455    public String key(String key, Object arg0, Object arg1, Object arg2) {
456
457        return key(key, new Object[] {arg0, arg1, arg2});
458    }
459
460    /**
461     * Returns the selected localized message for the initialized resource bundle and locale.<p>
462     *
463     * If the key was found in the bundle, it will be formatted using
464     * a <code>{@link MessageFormat}</code> using the provided parameters.<p>
465     *
466     * If the key was not found in the bundle, the return value is
467     * <code>"??? " + keyName + " ???"</code>. This will also be returned
468     * if the bundle was not properly initialized first.
469     *
470     * @param key the message key
471     * @param args the message arguments
472     *
473     * @return the selected localized message for the initialized resource bundle and locale
474     */
475    public String key(String key, Object[] args) {
476
477        return key(key, args, CmsMessages::formatUnknownKey);
478    }
479
480    /**
481     * Returns the selected localized message for the initialized resource bundle and locale.<p>
482     *
483     * If the key was found in the bundle, it will be formatted using
484     * a <code>{@link MessageFormat}</code> using the provided parameters.<p>
485     *
486     * If the key was not found in the bundle, the return value will be the result of passing the key to
487     * the unknownKeyFormatter function.
488     *
489     * @param key the message key
490     * @param args the message arguments
491     * @param unknownKeyFormatter the function for formatting unknown keys
492     *
493     * @return the selected localized message for the initialized resource bundle and locale
494     */
495    public String key(String key, Object[] args, Function<String, String> unknownKeyFormatter) {
496
497        String result = key(key, true);
498        if (result == null) {
499            // key was not found
500            result = unknownKeyFormatter.apply(key);
501        } else {
502            if ((args == null) || (args.length == 0)) {
503                // A message string actually only needs to be a valid message format if it actually is used with parameters.
504                // In practice there are several message strings that contain text in curly braces and are thus not valid message formats.
505                // So we just return the message string as-is.
506                return result;
507            }
508            try {
509                // key was found in the bundle - create and apply the formatter
510                MessageFormat formatter = new MessageFormat(result, m_locale);
511                result = formatter.format(args);
512            } catch (Exception e) {
513                // illegal message format - don't propagate the exception, just return the message string, with unfilled placeholders
514                // (this is probably better than crashing)
515                LOG.error(e.getLocalizedMessage(), e);
516            }
517        }
518        // return the result
519        return result;
520    }
521
522    /**
523     * Returns the localized resource string for a given message key.<p>
524     *
525     * If the key was not found in the bundle, the provided default value
526     * is returned.<p>
527     *
528     * @param keyName the key for the desired string
529     * @param defaultValue the default value in case the key does not exist in the bundle
530     * @return the resource string for the given key it it exists, or the given default if not
531     */
532    public String keyDefault(String keyName, String defaultValue) {
533
534        String result = key(keyName, true);
535        return (result == null) ? defaultValue : result;
536    }
537
538    /**
539     * Returns the localized resource string for a given message key,
540     * treating all values appended with "|" as replacement parameters.<p>
541     *
542     * If the key was found in the bundle, it will be formatted using
543     * a <code>{@link MessageFormat}</code> using the provided parameters.
544     * The parameters have to be appended to the key separated by a "|".
545     * For example, the keyName <code>error.message|First|Second</code>
546     * would use the key <code>error.message</code> with the parameters
547     * <code>First</code> and <code>Second</code>. This would be the same as calling
548     * <code>{@link CmsMessages#key(String, Object[])}</code>.<p>
549     *
550     * If no parameters are appended with "|", this is the same as calling
551     * <code>{@link CmsMessages#key(String)}</code>.<p>
552     *
553     * If the key was not found in the bundle, the return value is
554     * <code>"??? " + keyName + " ???"</code>. This will also be returned
555     * if the bundle was not properly initialized first.
556     *
557     * @param keyName the key for the desired string, optinally containing parameters appended with a "|"
558     * @return the resource string for the given key
559     *
560     * @see #key(String, Object[])
561     * @see #key(String)
562     */
563    public String keyWithParams(String keyName) {
564
565        return keyWithParams(keyName, CmsMessages::formatUnknownKey);
566    }
567
568    /**
569     * Returns the localized resource string for a given message key,
570     * treating all values appended with "|" as replacement parameters.<p>
571     *
572     * If the key was found in the bundle, it will be formatted using
573     * a <code>{@link MessageFormat}</code> using the provided parameters.
574     * The parameters have to be appended to the key separated by a "|".
575     * For example, the keyName <code>error.message|First|Second</code>
576     * would use the key <code>error.message</code> with the parameters
577     * <code>First</code> and <code>Second</code>. This would be the same as calling
578     * <code>{@link CmsMessages#key(String, Object[])}</code>.<p>
579     *
580     * If no parameters are appended with "|", this is the same as calling
581     * <code>{@link CmsMessages#key(String)}</code>.<p>
582     *
583     * If the key was not found in the bundle, the function from the unknownKeyFormatter parameter is used to generate the return value.
584     *
585     * @param keyName the key for the desired string, optinally containing parameters appended with a "|"
586     * @param unknownKeyFormatter the function to use to generate the returned result for unknown keys
587     * @return the resource string for the given key
588     *
589     * @see #key(String, Object[])
590     * @see #key(String)
591     */
592    public String keyWithParams(String keyName, Function<String, String> unknownKeyFormatter) {
593
594        if (keyName.indexOf('|') == -1) {
595            // no separator found, key has no parameters
596            return key(keyName, null, unknownKeyFormatter);
597        } else {
598            // this key contains parameters
599            String[] values = CmsStringUtil.splitAsArray(keyName, '|');
600            String cutKeyName = values[0];
601            String[] params = new String[values.length - 1];
602            System.arraycopy(values, 1, params, 0, params.length);
603            return key(cutKeyName, params, unknownKeyFormatter);
604        }
605    }
606
607    /**
608     * @see java.lang.Object#toString()
609     */
610    @Override
611    public String toString() {
612
613        StringBuffer result = new StringBuffer();
614
615        result.append('[');
616        result.append(this.getClass().getName());
617        result.append(", baseName: ");
618        result.append(m_bundleName);
619        result.append(", locale: ");
620        result.append(getLocale());
621        result.append(']');
622
623        return result.toString();
624    }
625
626    /**
627     * Returns the name of the resource bundle this object was initialized with.<p>
628     *
629     * @return the name of the resource bundle this object was initialized with
630     */
631    protected String getBundleName() {
632
633        return m_bundleName;
634    }
635
636    /**
637     * Sets the bundleName.<p>
638     *
639     * @param bundleName the bundleName to set
640     */
641    protected void setBundleName(String bundleName) {
642
643        m_bundleName = bundleName;
644    }
645
646    /**
647     * Sets the locale.<p>
648     *
649     * @param locale the locale to set
650     */
651    protected void setLocale(Locale locale) {
652
653        m_locale = locale;
654    }
655
656    /**
657     * Sets the resource bundle.<p>
658     *
659     * @param resourceBundle the resource bundle to set
660     */
661    protected void setResourceBundle(ResourceBundle resourceBundle) {
662
663        m_resourceBundle = resourceBundle;
664    }
665}