001package jmri;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004import java.text.MessageFormat;
005import java.util.Locale;
006import java.util.MissingResourceException;
007import java.util.ResourceBundle;
008import javax.annotation.CheckReturnValue;
009import javax.annotation.CheckForNull;
010import javax.annotation.ParametersAreNonnullByDefault;
011
012/**
013 * Provides standard access for resource bundles in a package.
014 * <p>
015 * Convention is to provide a subclass of this same name in each package,
016 * working off the local resource bundle name, usually 'package.Bundle' stored
017 * in a Bundle.properties file.
018 * <p>
019 * This is the root of a tree of classes that are chained through class-static
020 * members so that they each do a search as a request works up the inheritance
021 * tree.
022 * <p>
023 * Only package-scope methods exposed are from the class, forcing all requests
024 * for strings to be a the package level.
025 * <p>
026 * To add this to a new package, copy exactly a subclass file such as
027 * jmri.jmrit.Bundle, and change three places:
028 * <ol>
029 * <li>The import statement at the top
030 * <li>The extends clause in the class definition statement
031 * <li>The resource pathname assigned to the name variable, which must be set to
032 * null if there are no local resources.
033 * </ol>
034 *
035 * <hr>
036 * This file is part of JMRI.
037 * <p>
038 * JMRI is free software; you can redistribute it and/or modify it under the
039 * terms of version 2 of the GNU General Public License as published by the Free
040 * Software Foundation. See the "COPYING" file for a copy of this license.
041 * <p>
042 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
043 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
044 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
045 *
046 * @author Bob Jacobsen Copyright (C) 2012
047 * @since 3.3.1
048 */
049@ParametersAreNonnullByDefault
050@CheckReturnValue
051@javax.annotation.concurrent.Immutable
052@SuppressFBWarnings(value = {"HSM_HIDING_METHOD"},
053    justification = "Desired pattern is repeated class names with package-level access to members")
054public class Bundle {
055
056    @CheckForNull
057    private final static String name = "jmri.NamedBeanBundle";  // NOI18N
058
059    /**
060     * Provides a translated string for a given key from the package resource
061     * bundle or parent.
062     * <p>
063     * Note that this is intentionally package-local access.
064     *
065     * @param key Bundle key to be translated
066     * @return Internationalized text
067     */
068    static String getMessage(String key) {
069        return getBundle().handleGetMessage(key);
070    }
071
072    /**
073     * Provides a translated string for a given key in a given locale from the
074     * package resource bundle or parent.
075     * <p>
076     * Note that this is intentionally package-local access.
077     *
078     * @param locale The locale to be used
079     * @param key    Bundle key to be translated
080     * @return Internationalized text
081     */
082    static String getMessage(Locale locale, String key) {
083        return getBundle().handleGetMessage(locale, key);
084    }
085
086    /**
087     * Merges user data with a translated string for a given key from the
088     * package resource bundle or parent.
089     * <p>
090     * Uses the transformation conventions of the Java MessageFormat utility.
091     * <p>
092     * Note that this is intentionally package-local access.
093     *
094     * @see java.text.MessageFormat
095     * @param key  Bundle key to be translated
096     * @param subs One or more objects to be inserted into the message
097     * @return Internationalized text
098     */
099    static String getMessage(String key, Object... subs) {
100        return getBundle().handleGetMessage(key, subs);
101    }
102
103    /**
104     * Merges user data with a translated string for a given key in a given
105     * locale from the package resource bundle or parent.
106     * <p>
107     * Uses the transformation conventions of the Java MessageFormat utility.
108     * <p>
109     * Note that this is intentionally package-local access.
110     *
111     * @see java.text.MessageFormat
112     * @param locale The locale to be used
113     * @param key    Bundle key to be translated
114     * @param subs   One or more objects to be inserted into the message
115     * @return Internationalized text
116     */
117    static String getMessage(Locale locale, String key, Object... subs) {
118        return getBundle().handleGetMessage(locale, key, subs);
119    }
120
121    /**
122     * This method handles the inheritance tree. At lower levels, it reflects
123     * upwards on failure. Once it reaches this root class, it will throw a
124     * MissingResourceException in the key can't be found via the local
125     * definition of retry().
126     *
127     * @param key Bundle key to be translated
128     * @return Internationalized text
129     * @throws MissingResourceException if message cannot be found
130     */
131    public String handleGetMessage(String key) {
132        return this.handleGetMessage(Locale.getDefault(), key);
133    }
134
135    /**
136     * This method handles the inheritance tree. At lower levels, it reflects
137     * upwards on failure. Once it reaches this root class, it will throw a
138     * MissingResourceException in the key can't be found via the local
139     * definition of retry().
140     *
141     * @param locale The locale to be used
142     * @param key    Bundle key to be translated
143     * @return Internationalized text
144     * @throws MissingResourceException if message cannot be found
145     */
146    public String handleGetMessage(Locale locale, String key) {
147        log.trace("handleGetMessage for key {}", key);
148        if (bundleName() != null) {
149            ResourceBundle rb = ResourceBundle.getBundle(bundleName(), locale);
150            if (rb.containsKey(key)) {
151                return rb.getString(key);
152            } else {
153                return retry(locale, key);
154            }
155        } else {  // case of no local bundle
156            return retry(locale, key);
157        }
158    }
159
160    /**
161     * Merges user data with a translated string for a given key from the
162     * package resource bundle or parent.
163     * <p>
164     * Uses the transformation conventions of the Java MessageFormat utility.
165     *
166     * @see java.text.MessageFormat
167     * @param key  Bundle key to be translated
168     * @param subs Array of objects to be inserted into the message
169     * @return Internationalized text
170     */
171    public String handleGetMessage(String key, Object[] subs) {
172        return this.handleGetMessage(Locale.getDefault(), key, subs);
173    }
174
175    /**
176     * Merges user data with a translated string for a given key in a given
177     * locale from the package resource bundle or parent.
178     * <p>
179     * Uses the transformation conventions of the Java MessageFormat utility.
180     *
181     * @see java.text.MessageFormat
182     * @param locale The locale to be used
183     * @param key    Bundle key to be translated
184     * @param subs   Array of objects to be inserted into the message
185     * @return Internationalized text
186     */
187    public String handleGetMessage(Locale locale, String key, Object[] subs) {
188        return MessageFormat.format(handleGetMessage(locale, key), subs);
189    }
190
191    /**
192     * Formats a message string with parameters.
193     * <p>
194     * It's used when a message is fetched from a foreign bundle.
195     * 
196     * @param message The message to be formatted
197     * @param subs    Array of objects to be inserted into the message
198     * @return The formatted message
199     */
200    public static String formatMessage(String message, Object... subs) {
201        return MessageFormat.format(message, subs);
202    }
203
204    // the following is different from the method in subclasses because
205    // this is the root of the search tree
206    protected String retry(Locale locale, String key) throws MissingResourceException {
207        throw new MissingResourceException("Resource '" + key + "' not found", this.getClass().toString(), key); // NOI18N
208    }
209
210    private final static Bundle b = new Bundle();
211
212    @CheckForNull
213    protected String bundleName() {
214        return name;
215    }
216
217    protected static jmri.Bundle getBundle() {
218        return b;
219    }
220
221    // Can get pathname of ctor class (to auto-generate BundleName) via getClass().getPackage()
222    // E.g. to cache a local bundle name via weak reference
223    //        if (rbr == null) rbr = new java.lang.ref.SoftReference<ResourceBundle>(
224    //                                   ResourceBundle.getBundle("jmri.NamedBeanBundle"));
225    //        ResourceBundle rb = rbr.get();
226    //        if (rb == null) {
227    //           log.error("Failed to load defaults because of missing bundle");
228    //           return;
229    //        }
230    
231    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Bundle.class);
232
233}