All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openide.util.NbBundle Maven / Gradle / Ivy

There is a newer version: RELEASE230
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.openide.util;

import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
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.MissingResourceException;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.WeakHashMap;
import java.util.jar.Attributes;
import java.util.logging.Level;
import java.util.logging.Logger;

/** Convenience class permitting easy loading of localized resources of various sorts.
* Extends the functionality of {@link ResourceBundle} to handle branding, and interacts
* better with class loaders in a module system.
* 

Example usage: *

* package com.mycom;
* public class Foo {
*     public String getDisplayName() {
*         return {@link #getMessage(Class,String) NbBundle.getMessage}(Foo.class, "Foo.displayName");
*     }
* }
* 
* will in German locale look for the key {@code Foo.displayName} in * {@code com/mycom/Bundle_de.properties} and then {@code com/mycom/Bundle.properties} (in that order). * Usually however it is easiest to use {@link org.openide.util.NbBundle.Messages}. */ public class NbBundle extends Object { private static final Logger LOG = Logger.getLogger(NbBundle.class.getName()); private static final boolean USE_DEBUG_LOADER = Boolean.getBoolean("org.openide.util.NbBundle.DEBUG"); // NOI18N private static String brandingToken = null; /** * Cache of URLs for localized files. * Keeps only weak references to the class loaders. * @see "#9275" */ static final Map> localizedFileCache = new WeakHashMap>(); /** * Cache of resource bundles. */ static final Map>> bundleCache = new WeakHashMap>>(); /** * Do not call. * @deprecated There is no reason to instantiate or subclass this class. * All methods in it are static. */ @Deprecated public NbBundle() { } /** Get the current branding token. * @return the branding, or null for none */ public static String getBranding() { return brandingToken; } /** Set the current branding token. * The permitted format, as a regular expression: *
[a-z][a-z0-9]*(_[a-z][a-z0-9]*)*
*

* This is normally only called by NetBeans startup code and unit tests. * Currently the branding may be specified by passing the --branding * command-line option to the launcher. *

* @param bt the new branding, or null to clear * @throws IllegalArgumentException if in an incorrect format */ public static void setBranding(String bt) throws IllegalArgumentException { if (bt != null && !bt.matches("[a-z][a-z0-9]*(_[a-z][a-z0-9]*)*")) { // NOI18N throw new IllegalArgumentException("Malformed branding token: " + bt); // NOI18N } brandingToken = bt; } /** * Get a localized and/or branded file in the default locale with the default class loader. *

Note that use of this call is similar to using the URL protocol nbresloc * (which may in fact be implemented using the fuller form of the method). *

The extension may be null, in which case no final dot will be appended. * If it is the empty string, the resource will end in a dot. * @param baseName base name of file, as dot-separated path (e.g. some.dir.File) * @param ext extension of file (or null) * @return URL of matching localized file * @throws MissingResourceException if not found * @deprecated Use the nbresloc URL protocol instead. This method does a poor * job of handling resources such as /some.dir/res.txt or * /some/res.txt.sample. */ @Deprecated public static synchronized URL getLocalizedFile(String baseName, String ext) throws MissingResourceException { return getLocalizedFile(baseName, ext, Locale.getDefault(), getLoader()); } /** * Get a localized and/or branded file with the default class loader. * @param baseName base name of file, as dot-separated path (e.g. some.dir.File) * @param ext extension of file (or null) * @param locale locale of file * @return URL of matching localized file * @throws MissingResourceException if not found * @deprecated Use the nbresloc URL protocol instead. This method does a poor * job of handling resources such as /some.dir/res.txt or * /some/res.txt.sample. */ @Deprecated public static synchronized URL getLocalizedFile(String baseName, String ext, Locale locale) throws MissingResourceException { return getLocalizedFile(baseName, ext, locale, getLoader()); } /** * Get a localized and/or branded file. * @param baseName base name of file, as dot-separated path (e.g. some.dir.File) * @param ext extension of file (or null) * @param locale locale of file * @param loader class loader to use * @return URL of matching localized file * @throws MissingResourceException if not found * @deprecated Use the nbresloc URL protocol instead. This method does a poor * job of handling resources such as /some.dir/res.txt or * /some/res.txt.sample. */ @Deprecated public static synchronized URL getLocalizedFile(String baseName, String ext, Locale locale, ClassLoader loader) throws MissingResourceException { // [PENDING] in the future, could maybe do something neat if // USE_DEBUG_LOADER and ext is "html" or "txt" etc... URL lookup = null; Iterator it = new LocaleIterator(locale); List cacheCandidates = new ArrayList(10); String baseNameSlashes = baseName.replace('.', '/'); Map perLoaderCache = localizedFileCache.get(loader); if (perLoaderCache == null) { localizedFileCache.put(loader, perLoaderCache = new HashMap()); } // #31008: better use of domain cache priming. // [PENDING] remove this hack in case the domain cache is precomputed URL baseVariant; String path; if (ext != null) { path = baseNameSlashes + '.' + ext; } else { path = baseNameSlashes; } lookup = perLoaderCache.get(path); if (lookup == null) { baseVariant = loader.getResource(path); } else { // who cares? already in cache anyway baseVariant = null; } while (it.hasNext()) { String suffix = it.next(); if (ext != null) { path = baseNameSlashes + suffix + '.' + ext; } else { path = baseNameSlashes + suffix; } lookup = perLoaderCache.get(path); if (lookup != null) { break; } cacheCandidates.add(path); if (suffix.length() == 0) { lookup = baseVariant; } else { lookup = loader.getResource(path); } if (lookup != null) { break; } } if (lookup == null) { path = baseName.replace('.', '/'); if (ext != null) { path += ('.' + ext); } throw new MissingResourceException( "Cannot find localized resource " + path + " in " + loader, loader.toString(), path ); // NOI18N } else { // Note that this is not 100% accurate. If someone calls gLF on something // with a locale/branding combo such as _brand_ja, and the answer is found // as _ja, then a subsequent call with param _brand will find this _ja // version - since the localizing iterator does *not* have the property that // each subsequent item is more general than the previous. However, this // situation is very unlikely, so consider this close enough. it = cacheCandidates.iterator(); while (it.hasNext()) { perLoaderCache.put(it.next(), lookup); } return lookup; } } /** * Find a localized and/or branded value for a given key and locale. * Scans through a map to find * the most localized match possible. For example: *

    *   findLocalizedValue (hashTable, "keyName", new Locale ("cs_CZ"))
    * 
*

This would return the first non-null value obtained from the following tests: *

    *
  • hashTable.get ("keyName_cs_CZ") *
  • hashTable.get ("keyName_cs") *
  • hashTable.get ("keyName") *
* * @param table mapping from localized strings to objects * @param key the key to look for * @param locale the locale to use * @return the localized object or null if no key matches */ public static T getLocalizedValue(Map table, String key, Locale locale) { for (String suffix : NbCollections.iterable(new LocaleIterator(locale))) { String physicalKey = key + suffix; T v = table.get(physicalKey); if (v != null) { // ok if (USE_DEBUG_LOADER && (v instanceof String)) { // Not read from a bundle, but still localized somehow: @SuppressWarnings("unchecked") T _v = (T) (((String) v) + " (?:" + physicalKey + ")"); // NOI18N; return _v; } else { return v; } } } return null; } /** * Find a localized and/or branded value for a given key in the default system locale. * * @param table mapping from localized strings to objects * @param key the key to look for * @return the localized object or null if no key matches * @see #getLocalizedValue(Map,String,Locale) */ public static T getLocalizedValue(Map table, String key) { return getLocalizedValue(table, key, Locale.getDefault()); } /** * Find a localized and/or branded value in a JAR manifest. * @param attr the manifest attributes * @param key the key to look for (case-insensitive) * @param locale the locale to use * @return the value if found, else null */ public static String getLocalizedValue(Attributes attr, Attributes.Name key, Locale locale) { return getLocalizedValue(attr2Map(attr), key.toString().toLowerCase(Locale.US), locale); } /** * Find a localized and/or branded value in a JAR manifest in the default system locale. * @param attr the manifest attributes * @param key the key to look for (case-insensitive) * @return the value if found, else null */ public static String getLocalizedValue(Attributes attr, Attributes.Name key) { // Yes, US locale is intentional! The attribute name may only be ASCII anyway. // It is necessary to lowercase it *as ASCII* as in Turkish 'I' does not go to 'i'! return getLocalizedValue(attr2Map(attr), key.toString().toLowerCase(Locale.US)); } /** Necessary because Attributes implements Map; however this is dangerous! * The keys are Attributes.Name's, not Strings. * Also manifest lookups should not be case-sensitive. * (Though the locale suffix still will be!) */ private static Map attr2Map(Attributes attr) { return new AttributesMap(attr); } // ---- LOADING RESOURCE BUNDLES ---- /** * Get a resource bundle with the default class loader and locale/branding. * Caution: {@link #getBundle(Class)} is generally * safer when used from a module as this method relies on the module's * classloader to currently be part of the system classloader. NetBeans * does add enabled modules to this classloader, however calls to * this variant of the method made in ModuleInstall.validate, * or made soon after a module is uninstalled (due to background threads) * could fail unexpectedly. * @param baseName bundle basename * @return the resource bundle * @exception MissingResourceException if the bundle does not exist */ public static ResourceBundle getBundle(String baseName) throws MissingResourceException { return getBundle(baseName, Locale.getDefault(), getLoader()); } /** Get a resource bundle in the same package as the provided class, * with the default locale/branding and the class' own classloader. * The usual style of invocation is {@link #getMessage(Class,String)} * or one of the other overloads taking message formats. * * @param clazz the class to take the package name from * @return the resource bundle * @exception MissingResourceException if the bundle does not exist */ public static ResourceBundle getBundle(Class clazz) throws MissingResourceException { String name = findName(clazz); return getBundle(name, Locale.getDefault(), clazz.getClassLoader()); } /** Finds package name for given class */ private static String findName(Class clazz) { String pref = clazz.getName(); int last = pref.lastIndexOf('.'); if (last >= 0) { pref = pref.substring(0, last + 1); return pref + "Bundle"; // NOI18N } else { // base package, search for bundle return "Bundle"; // NOI18N } } /** * Get a resource bundle with the default class loader and branding. * @param baseName bundle basename * @param locale the locale to use (but still uses {@link #getBranding default branding}) * @return the resource bundle * @exception MissingResourceException if the bundle does not exist */ public static ResourceBundle getBundle(String baseName, Locale locale) throws MissingResourceException { return getBundle(baseName, locale, getLoader()); } /** Get a resource bundle the hard way. * @param baseName bundle basename * @param locale the locale to use (but still uses {@link #getBranding default branding}) * @param loader the class loader to use * @return the resource bundle * @exception MissingResourceException if the bundle does not exist */ public static ResourceBundle getBundle(String baseName, Locale locale, ClassLoader loader) throws MissingResourceException { if (USE_DEBUG_LOADER) { loader = DebugLoader.get(loader); } // Could more simply use ResourceBundle.getBundle (plus some special logic // with MergedBundle to handle branding) instead of manually finding bundles. // However this code is faster and has some other desirable properties. // Cf. #13847. ResourceBundle b = getBundleFast(baseName, locale, loader); if (b != null) { return b; } else { MissingResourceException e = new MissingResourceException("No such bundle " + baseName, baseName, null); // NOI18N if (Lookup.getDefault().lookup(ClassLoader.class) == null) { Exceptions.attachMessage(e, "Class loader not yet initialized in lookup"); // NOI18N } else { Exceptions.attachMessage(e, "Offending classloader: " + loader); // NOI18N } throw e; } } /** * Get a resource bundle by name. * Like {@link ResourceBundle#getBundle(String,Locale,ClassLoader)} but faster, * and also understands branding. * First looks for .properties-based bundles, then .class-based. * @param name the base name of the bundle, e.g. org.netbeans.modules.foo.Bundle * @param locale the locale to use * @param loader a class loader to search in * @return a resource bundle (locale- and branding-merged), or null if not found */ private static ResourceBundle getBundleFast(String name, Locale locale, ClassLoader loader) { Map> m; synchronized (bundleCache) { m = bundleCache.get(loader); if (m == null) { bundleCache.put(loader, m = new HashMap>()); } } //A minor optimization to cut down on StringBuffer allocations - OptimizeIt //showed the commented out code below was a major source of them. This //just does the same thing with a char array - Tim String localeStr = locale.toString(); char[] k = new char[name.length() + ((brandingToken != null) ? brandingToken.length() : 1) + 2 + localeStr.length()]; name.getChars(0, name.length(), k, 0); k[name.length()] = '/'; //NOI18N int pos = name.length() + 1; if (brandingToken == null) { k[pos] = '-'; //NOI18N pos++; } else { brandingToken.getChars(0, brandingToken.length(), k, pos); pos += brandingToken.length(); } k[pos] = '/'; //NOI18N pos++; localeStr.getChars(0, localeStr.length(), k, pos); String key = new String(k); /* String key = name + '/' + (brandingToken != null ? brandingToken : "-") + '/' + locale; // NOI18N */ synchronized (m) { Reference o = m.get(key); ResourceBundle b = o != null ? o.get() : null; if (b != null) { return b; } else { b = loadBundle(name, locale, loader); if (b != null) { m.put(key, new TimedSoftReference(b, m, key)); } else { // Used to cache misses as well, to make the negative test faster. // However this caused problems: see #31578. } return b; } } } /** * Load a resource bundle (without caching). * @param name the base name of the bundle, e.g. org.netbeans.modules.foo.Bundle * @param locale the locale to use * @param loader a class loader to search in * @return a resource bundle (locale- and branding-merged), or null if not found */ private static ResourceBundle loadBundle(String name, Locale locale, ClassLoader loader) { String sname = name.replace('.', '/'); Iterator it = new LocaleIterator(locale); LinkedList l = new LinkedList(); while (it.hasNext()) { l.addFirst(it.next()); } Properties p = new Properties(); for (String suffix : l) { String res = sname + suffix + ".properties"; // #49961: don't use getResourceAsStream; catch all errors opening it URL u = loader != null ? loader.getResource(res) : ClassLoader.getSystemResource(res); if (u != null) { //System.err.println("Loading " + res); try { // #51667: but in case we are in USE_DEBUG_LOADER mode, use gRAS (since getResource is not overridden) InputStream is = USE_DEBUG_LOADER ? (loader != null ? loader.getResourceAsStream(res) : ClassLoader.getSystemResourceAsStream(res)) : u.openStream(); try { p.load(is); } finally { is.close(); } } catch (IOException e) { Exceptions.attachMessage(e, "While loading: " + res); // NOI18N LOG.log(Level.WARNING, null, e); return null; } } else if (suffix.length() == 0) { // No base *.properties. Try *.class. // Note that you may not mix *.properties w/ *.class this way. return loadBundleClass(name, sname, locale, l, loader); } } return new PBundle(NbCollections.checkedMapByFilter(p, String.class, String.class, true), locale); } /** * Load a class-based resource bundle. * @param name the base name of the bundle, e.g. org.netbeans.modules.foo.Bundle * @param sname the name with slashes, e.g. org/netbeans/modules/foo/Bundle * @param locale the locale to use * @param suffixes a list of suffixes to apply to the bundle name, in increasing order of specificity * @param loader a class loader to search in * @return a resource bundle (merged according to the suffixes), or null if not found */ private static ResourceBundle loadBundleClass( String name, String sname, Locale locale, List suffixes, ClassLoader l ) { if (l != null && l.getResource(sname + ".class") == null) { // NOI18N // No chance - no base bundle. Don't waste time catching CNFE. return null; } ResourceBundle master = null; for (String suffix : suffixes) { try { Class c = Class.forName(name + suffix, true, l).asSubclass(ResourceBundle.class); ResourceBundle b = c.newInstance(); if (master == null) { master = b; } else { master = new MergedBundle(locale, b, master); } } catch (ClassNotFoundException cnfe) { // fine - ignore } catch (Exception e) { LOG.log(Level.WARNING, null, e); } catch (LinkageError e) { LOG.log(Level.WARNING, null, e); } } return master; } // // Helper methods to simplify localization of messages // /** * Finds a localized and/or branded string in a bundle. * @param clazz the class to use to locate the bundle (see {@link #getBundle(Class)} for details) * @param resName name of the resource to look for * @return the string associated with the resource * @throws MissingResourceException if either the bundle or the string cannot be found */ public static String getMessage(Class clazz, String resName) throws MissingResourceException { return getBundle(clazz).getString(resName); } /** * Finds a localized and/or branded string in a bundle and formats the message * by passing requested parameters. * * @param clazz the class to use to locate the bundle (see {@link #getBundle(Class)} for details) * @param resName name of the resource to look for * @param param1 the argument to use when formatting the message * @return the string associated with the resource * @throws MissingResourceException if either the bundle or the string cannot be found * @see java.text.MessageFormat#format(String,Object[]) */ public static String getMessage(Class clazz, String resName, Object param1) throws MissingResourceException { return getMessage(clazz, resName, new Object[] { param1 }); } /** * Finds a localized and/or branded string in a bundle and formats the message * by passing requested parameters. * * @param clazz the class to use to locate the bundle (see {@link #getBundle(Class)} for details) * @param resName name of the resource to look for * @param param1 the argument to use when formatting the message * @param param2 the second argument to use for formatting * @return the string associated with the resource * @throws MissingResourceException if either the bundle or the string cannot be found * @see java.text.MessageFormat#format(String,Object[]) */ public static String getMessage(Class clazz, String resName, Object param1, Object param2) throws MissingResourceException { return getMessage(clazz, resName, new Object[] { param1, param2 }); } /** * Finds a localized and/or branded string in a bundle and formats the message * by passing requested parameters. * * @param clazz the class to use to locate the bundle (see {@link #getBundle(Class)} for details) * @param resName name of the resource to look for * @param param1 the argument to use when formatting the message * @param param2 the second argument to use for formatting * @param param3 the third argument to use for formatting * @return the string associated with the resource * @throws MissingResourceException if either the bundle or the string cannot be found * @see java.text.MessageFormat#format(String,Object[]) */ public static String getMessage(Class clazz, String resName, Object param1, Object param2, Object param3) throws MissingResourceException { return getMessage(clazz, resName, new Object[] { param1, param2, param3 }); } /** * Finds a localized and/or branded string in a bundle and formats the message * by passing requested parameters. * * @param clazz the class to use to locate the bundle (see {@link #getBundle(Class)} for details) * @param resName name of the resource to look for * @param param1 the argument to use when formatting the message * @param param2 the second argument to use for formatting * @param param3 the third argument to use for formatting * @param param4 the fourth argument to use for formatting * @param params fifth, sixth, ... arguments as needed * @return the string associated with the resource * @throws MissingResourceException if either the bundle or the string cannot be found * @see java.text.MessageFormat#format(String,Object[]) * @since org.openide.util 7.27 */ public static String getMessage(Class clazz, String resName, Object param1, Object param2, Object param3, Object param4, Object... params) throws MissingResourceException { Object[] allParams = new Object[params.length + 4]; allParams[0] = param1; allParams[1] = param2; allParams[2] = param3; allParams[3] = param4; System.arraycopy(params, 0, allParams, 4, params.length); return getMessage(clazz, resName, allParams); } /** * Finds a localized and/or branded string in a bundle and formats the message * by passing requested parameters. * * @param clazz the class to use to locate the bundle (see {@link #getBundle(Class)} for details) * @param resName name of the resource to look for * @param arr array of parameters to use for formatting the message * @return the string associated with the resource * @throws MissingResourceException if either the bundle or the string cannot be found * @see java.text.MessageFormat#format(String,Object[]) */ public static String getMessage(Class clazz, String resName, Object[] arr) throws MissingResourceException { return java.text.MessageFormat.format(getMessage(clazz, resName), arr); } /** @return default class loader which is used, when we don't have * any other class loader. (in function getBundle(String), getLocalizedFile(String), * and so on... */ private static ClassLoader getLoader() { ClassLoader c = Lookup.getDefault().lookup(ClassLoader.class); return (c != null) ? c : ClassLoader.getSystemClassLoader(); } /** * Get a list of all suffixes used to search for localized/branded resources. * Based on the default locale and branding, returns the list of suffixes * which various NbBundle methods use as the search order. * For example, when {@link #getBranding} returns branding * and the default locale is German, you might get a sequence such as: *
    *
  1. "_branding_de" *
  2. "_branding" *
  3. "_de" *
  4. "" *
* @return a read-only iterator of type String * @since 1.1.5 */ public static Iterator getLocalizingSuffixes() { return new LocaleIterator(Locale.getDefault()); } /** * Do not use. * @param loaderFinder ignored * @deprecated Useless. */ @Deprecated public static void setClassLoaderFinder(ClassLoaderFinder loaderFinder) { throw new Error(); } /** * Creates a helper class with static definitions of bundle keys. *

* The generated class will be called {@code Bundle} and be in the same package. * Each key is placed in a {@code Bundle.properties} file also in the same package, * and the helper class gets a method with the same name as the key * (converted to a valid Java identifier as needed) * which loads the key from the (possibly now localized) bundle using {@link NbBundle#getMessage(Class, String)}. * The method will have as many arguments (of type {@code Object}) as there are message format parameters. *

*

It is an error to duplicate a key within a package, even if the duplicates are from different compilation units.

*

Example usage:

*
     * package some.where;
     * import org.openide.util.NbBundle.Messages;
     * import static some.where.Bundle.*;
     * import org.openide.DialogDisplayer;
     * import org.openide.NotifyDescriptor;
     * class Something {
     *     @Messages({
     *         "dialog.title=Bad File",
     *         "# {0} - file path",
     *         "dialog.message=The file {0} was invalid."
     *     })
     *     void showError(File f) {
     *         NotifyDescriptor d = new NotifyDescriptor.Message(
     *             dialog_message(f), NotifyDescriptor.ERROR_MESSAGE);
     *         d.setTitle(dialog_title());
     *         DialogDisplayer.getDefault().notify(d);
     *     }
     * }
     * 
*

which generates during compilation {@code Bundle.java}:

*
     * class Bundle {
     *     static String dialog_title() {...}
     *     static String dialog_message(Object file_path) {...}
     * }
     * 
*

and {@code Bundle.properties}:

*
     * dialog.title=Bad File
     * # {0} - file path
     * dialog.message=The file {0} was invalid.
     * 
* @since org.openide.util 8.10 (available also on fields since 8.22) */ @Retention(RetentionPolicy.SOURCE) @Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD}) public @interface Messages { /** * List of key/value pairs. * Each must be of the form {@code key=Some Value}. * Anything is permitted in the value, including newlines. * Unlike in a properties file, there should be no whitespace before the key or around the equals sign. * Values containing {0} etc. are assumed to be message formats and so may need escapes for metacharacters such as {@code '}. * A line may also be a comment if it starts with {@code #}, which may be useful for translators; * it is recommended to use the format {@code # {0} - summary of param}. */ String[] value(); } /** * Do not use. * @deprecated Useless. */ @Deprecated public static interface ClassLoaderFinder { /** * Do not use. * @return nothing * @deprecated Useless. */ @Deprecated public ClassLoader find(); } private static class AttributesMap extends HashMap { private Attributes attrs; public AttributesMap(Attributes attrs) { super(7); this.attrs = attrs; } public @Override String get(Object _k) { if (!(_k instanceof String)) { return null; } String k = (String) _k; Attributes.Name an; try { an = new Attributes.Name(k); } catch (IllegalArgumentException iae) { // Robustness, and workaround for reported MRJ locale bug: LOG.log(Level.FINE, null, iae); return null; } return attrs.getValue(an); } } /** * A resource bundle based on .properties files (or any map). */ private static final class PBundle extends ResourceBundle { private final Map m; private final Locale locale; /** * Create a new bundle based on a map. * @param m a map from resources keys to values (typically both strings) * @param locale the locale it represents (informational) */ public PBundle(Map m, Locale locale) { this.m = m; this.locale = locale; } public @Override Enumeration getKeys() { return Collections.enumeration(m.keySet()); } protected @Override Object handleGetObject(String key) { return m.get(key); } public @Override Locale getLocale() { return locale; } } /** Special resource bundle which delegates to two others. * Ideally could just set the parent on the first, but this is protected, so... */ private static class MergedBundle extends ResourceBundle { private Locale loc; private ResourceBundle sub1; private ResourceBundle sub2; /** * Create a new bundle delegating to two others. * @param loc the locale it represents (informational) * @param sub1 one delegate (taking precedence over the other in case of overlap) * @param sub2 the other (weaker) delegate */ public MergedBundle(Locale loc, ResourceBundle sub1, ResourceBundle sub2) { this.loc = loc; this.sub1 = sub1; this.sub2 = sub2; } public @Override Locale getLocale() { return loc; } public @Override Enumeration getKeys() { return Enumerations.removeDuplicates(Enumerations.concat(sub1.getKeys(), sub2.getKeys())); } protected @Override Object handleGetObject(String key) throws MissingResourceException { try { return sub1.getObject(key); } catch (MissingResourceException mre) { // Ignore exception, and... return sub2.getObject(key); } } } /** This class (enumeration) gives all localized sufixes using nextElement * method. It goes through given Locale and continues through Locale.getDefault() * Example 1: * Locale.getDefault().toString() -> "_en_US" * you call new LocaleIterator(new Locale("cs", "CZ")); * ==> You will gets: "_cs_CZ", "_cs", "", "_en_US", "_en" * * Example 2: * Locale.getDefault().toString() -> "_cs_CZ" * you call new LocaleIterator(new Locale("cs", "CZ")); * ==> You will gets: "_cs_CZ", "_cs", "" * * If there is a branding token in effect, you will get it too as an extra * prefix, taking precedence, e.g. for the token "f4jce": * * "_f4jce_cs_CZ", "_f4jce_cs", "_f4jce", "_f4jce_en_US", "_f4jce_en", "_cs_CZ", "_cs", "", "_en_US", "_en" * * Branding tokens with underscores are broken apart naturally: so e.g. * branding "f4j_ce" looks first for "f4j_ce" branding, then "f4j" branding, then none. */ private static class LocaleIterator extends Object implements Iterator { /** this flag means, if default locale is in progress */ private boolean defaultInProgress = false; /** this flag means, if empty suffix was exported yet */ private boolean empty = false; /** current locale, and initial locale */ private Locale locale; /** current locale, and initial locale */ private Locale initLocale; /** current suffix which will be returned in next calling nextElement */ private String current; /** the branding string in use */ private String branding; /** Creates new LocaleIterator for given locale. * @param locale given Locale */ public LocaleIterator(Locale locale) { this.locale = this.initLocale = locale; if (locale.equals(Locale.getDefault())) { defaultInProgress = true; } current = '_' + locale.toString(); if (brandingToken == null) { branding = null; } else { branding = "_" + brandingToken; // NOI18N } //System.err.println("Constructed: " + this); } /** @return next suffix. * @exception NoSuchElementException if there is no more locale suffix. */ public @Override String next() throws NoSuchElementException { if (current == null) { throw new NoSuchElementException(); } final String ret; if (branding == null) { ret = current; } else { ret = branding + current; } int lastUnderbar = current.lastIndexOf('_'); if (lastUnderbar == 0) { if (empty) { reset(); } else { current = ""; // NOI18N empty = true; } } else { if (lastUnderbar == -1) { if (defaultInProgress) { reset(); } else { // [PENDING] stuff with trying the default locale // after the real one does not actually seem to work... locale = Locale.getDefault(); current = '_' + locale.toString(); defaultInProgress = true; } } else { current = current.substring(0, lastUnderbar); } } //System.err.println("Returning: `" + ret + "' from: " + this); return ret; } /** Finish a series. * If there was a branding prefix, restart without that prefix * (or with a shorter prefix); else finish. */ private void reset() { if (branding != null) { current = '_' + initLocale.toString(); int idx = branding.lastIndexOf('_'); if (idx == 0) { branding = null; } else { branding = branding.substring(0, idx); } empty = false; } else { current = null; } } /** Tests if there is any sufix.*/ public @Override boolean hasNext() { return (current != null); } public @Override void remove() throws UnsupportedOperationException { throw new UnsupportedOperationException(); } } // end of LocaleIterator /** Classloader whose special trick is inserting debug information * into any *.properties files it loads. */ static final class DebugLoader extends ClassLoader { /** global bundle index, each loaded bundle gets its own */ private static int count = 0; /** indices of known bundles; needed since DebugLoader's can be collected * when softly reachable, but this should be transparent to the user */ private static final Map knownIDs = new HashMap(); /** cache of existing debug loaders for regular loaders */ private static final Map> existing = new WeakHashMap>(); private DebugLoader(ClassLoader cl) { super(cl); //System.err.println ("new DebugLoader: cl=" + cl); } private static int getID(String name) { synchronized (knownIDs) { Integer i = knownIDs.get(name); if (i == null) { i = ++count; knownIDs.put(name, i); System.err.println("NbBundle trace: #" + i + " = " + name); // NOI18N } return i; } } public static ClassLoader get(ClassLoader normal) { //System.err.println("Lookup: normal=" + normal); synchronized (existing) { Reference r = existing.get(normal); if (r != null) { ClassLoader dl = r.get(); if (dl != null) { //System.err.println("\tcache hit"); return dl; } else { //System.err.println("\tcollected ref"); } } else { //System.err.println("\tnot in cache"); } ClassLoader dl = new DebugLoader(normal); existing.put(normal, new WeakReference(dl)); return dl; } } public @Override InputStream getResourceAsStream(String name) { InputStream base = super.getResourceAsStream(name); if (base == null) { return null; } if (name.endsWith(".properties")) { // NOI18N int id = getID(name); //System.err.println ("\tthis=" + this + " parent=" + getParent ()); boolean loc = name.indexOf("Bundle") != -1; // NOI18N return new DebugInputStream(base, id, loc); } else { return base; } } // [PENDING] getResource not overridden; but ResourceBundle uses getResourceAsStream anyhow /** Wrapper input stream which parses the text as it goes and adds annotations. * Resource-bundle values are annotated with their current line number and also * the supplied it, so e.g. if in the original input stream on line 50 we have: * somekey=somevalue * so in the wrapper stream (id 123) this line will read: * somekey=somevalue (123:50) * Since you see on stderr what #123 is, you can then pinpoint where any bundle key * originally came from, assuming NbBundle loaded it from a *.properties file. * @see {@link Properties#load} for details on the syntax of *.properties files. */ static final class DebugInputStream extends InputStream { /** state transition diagram constants */ private static final int WAITING_FOR_KEY = 0; /** state transition diagram constants */ private static final int IN_COMMENT = 1; /** state transition diagram constants */ private static final int IN_KEY = 2; /** state transition diagram constants */ private static final int IN_KEY_BACKSLASH = 3; /** state transition diagram constants */ private static final int AFTER_KEY = 4; /** state transition diagram constants */ private static final int WAITING_FOR_VALUE = 5; /** state transition diagram constants */ private static final int IN_VALUE = 6; /** state transition diagram constants */ private static final int IN_VALUE_BACKSLASH = 7; private final InputStream base; private final int id; private final boolean localizable; /** current line number */ private int line = 0; /** line number in effect for last-encountered key */ private int keyLine = 0; /** current state in state machine */ private int state = WAITING_FOR_KEY; /** if true, the last char was a CR, waiting to see if we get a NL too */ private boolean twixtCrAndNl = false; /** if non-null, a string to serve up before continuing (length must be > 0) */ private String toInsert = null; /** if true, the next value encountered should be localizable if normally it would not be, or vice-versa */ private boolean reverseLocalizable = false; /** text of currently read comment, including leading comment character */ private StringBuffer lastComment = null; /** text of currently read value, ignoring escapes for now */ private final StringBuilder currentValue = new StringBuilder(); /** Create a new InputStream which will annotate resource bundles. * Bundles named Bundle*.properties will be treated as localizable by default, * and so annotated; other bundles will be treated as nonlocalizable and not annotated. * Messages can be individually marked as localizable or not to override this default, * in accordance with some I18N conventions for NetBeans. * @param base the unannotated stream * @param id an identifying number to use in annotations * @param localizable if true, this bundle is expected to be localizable * @see http://www.netbeans.org/i18n/ */ public DebugInputStream(InputStream base, int id, boolean localizable) { this.base = base; this.id = id; this.localizable = localizable; } public @Override int read() throws IOException { if (toInsert != null) { char result = toInsert.charAt(0); if (toInsert.length() > 1) { toInsert = toInsert.substring(1); } else { toInsert = null; } return result; } int next = base.read(); if (next == '\n') { twixtCrAndNl = false; line++; } else if (next == '\r') { if (twixtCrAndNl) { line++; } else { twixtCrAndNl = true; } } else { twixtCrAndNl = false; } switch (state) { case WAITING_FOR_KEY: switch (next) { case '#': case '!': state = IN_COMMENT; lastComment = new StringBuffer(); lastComment.append((char) next); return next; case ' ': case '\t': case '\n': case '\r': case -1: return next; case '\\': state = IN_KEY_BACKSLASH; return next; default: state = IN_KEY; keyLine = line + 1; return next; } case IN_COMMENT: switch (next) { case '\n': case '\r': String comment = lastComment.toString(); lastComment = null; if (localizable && comment.equals("#NOI18N")) { // NOI18N reverseLocalizable = true; } else if (localizable && comment.equals("#PARTNOI18N")) { // NOI18N System.err.println( "NbBundle WARNING (" + id + ":" + line + "): #PARTNOI18N encountered, will not annotate I18N parts" ); // NOI18N reverseLocalizable = true; } else if (!localizable && comment.equals("#I18N")) { // NOI18N reverseLocalizable = true; } else if (!localizable && comment.equals("#PARTI18N")) { // NOI18N System.err.println( "NbBundle WARNING (" + id + ":" + line + "): #PARTI18N encountered, will not annotate I18N parts" ); // NOI18N reverseLocalizable = false; } else if ( (localizable && (comment.equals("#I18N") || comment.equals("#PARTI18N"))) || // NOI18N (!localizable && (comment.equals("#NOI18N") || comment.equals("#PARTNOI18N"))) ) { // NOI18N System.err.println( "NbBundle WARNING (" + id + ":" + line + "): incongruous comment " + comment + " found for bundle" ); // NOI18N reverseLocalizable = false; } state = WAITING_FOR_KEY; return next; default: lastComment.append((char) next); return next; } case IN_KEY: switch (next) { case '\\': state = IN_KEY_BACKSLASH; return next; case ' ': case '\t': state = AFTER_KEY; return next; case '=': case ':': state = WAITING_FOR_VALUE; return next; case '\r': case '\n': state = WAITING_FOR_KEY; return next; default: return next; } case IN_KEY_BACKSLASH: state = IN_KEY; return next; case AFTER_KEY: switch (next) { case '=': case ':': state = WAITING_FOR_VALUE; return next; case '\r': case '\n': state = WAITING_FOR_KEY; return next; default: return next; } case WAITING_FOR_VALUE: switch (next) { case '\r': case '\n': state = WAITING_FOR_KEY; return next; case ' ': case '\t': return next; case '\\': state = IN_VALUE_BACKSLASH; return next; default: state = IN_VALUE; currentValue.setLength(0); return next; } case IN_VALUE: switch (next) { case '\\': // Gloss over distinction between simple escapes and \u1234, which is not important for us. // Also no need to deal specially with continuation lines; for us, there is an escaped // newline, after which will be more value, and that is all that is important. state = IN_VALUE_BACKSLASH; return next; case '\n': case '\r': case -1: // End of value. This is the tricky part. boolean revLoc = reverseLocalizable; reverseLocalizable = false; state = WAITING_FOR_KEY; if (localizable ^ revLoc) { // This value is intended to be localizable. Annotate it. assert keyLine > 0; toInsert = "(" + id + ":" + keyLine + ")"; // NOI18N if (next != -1) { toInsert += Character.valueOf((char) next); } keyLine = 0; // Now return the space before the rest of the string explicitly. return ' '; } else { // This is not supposed to be a localizable value, leave it alone. return next; } default: currentValue.append((char) next); return next; } case IN_VALUE_BACKSLASH: state = IN_VALUE; return next; default: throw new IOException("should never happen"); // NOI18N } } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy