0001: /*
0002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
0003: *
0004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
0005: *
0006: * The contents of this file are subject to the terms of either the GNU
0007: * General Public License Version 2 only ("GPL") or the Common
0008: * Development and Distribution License("CDDL") (collectively, the
0009: * "License"). You may not use this file except in compliance with the
0010: * License. You can obtain a copy of the License at
0011: * http://www.netbeans.org/cddl-gplv2.html
0012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
0013: * specific language governing permissions and limitations under the
0014: * License. When distributing the software, include this License Header
0015: * Notice in each file and include the License file at
0016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
0017: * particular file as subject to the "Classpath" exception as provided
0018: * by Sun in the GPL Version 2 section of the License file that
0019: * accompanied this code. If applicable, add the following below the
0020: * License Header, with the fields enclosed by brackets [] replaced by
0021: * your own identifying information:
0022: * "Portions Copyrighted [year] [name of copyright owner]"
0023: *
0024: * Contributor(s):
0025: *
0026: * The Original Software is NetBeans. The Initial Developer of the Original
0027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
0028: * Microsystems, Inc. All Rights Reserved.
0029: *
0030: * If you wish your version of this file to be governed by only the CDDL
0031: * or only the GPL Version 2, indicate your decision by adding
0032: * "[Contributor] elects to include this software in this distribution
0033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
0034: * single choice of license, a recipient has the option to distribute
0035: * your version of this file under either the CDDL, the GPL Version 2 or
0036: * to extend the choice of license to its licensees as provided above.
0037: * However, if you add GPL Version 2 code and therefore, elected the GPL
0038: * Version 2 license, then the option applies only if the new code is
0039: * made subject to such option by the copyright holder.
0040: */
0041:
0042: package org.openide.util;
0043:
0044: import java.io.IOException;
0045: import java.io.InputStream;
0046: import java.lang.ref.Reference;
0047: import java.lang.ref.WeakReference;
0048: import java.net.URL;
0049: import java.util.ArrayList;
0050: import java.util.Collections;
0051: import java.util.Enumeration;
0052: import java.util.HashMap;
0053: import java.util.Iterator;
0054: import java.util.LinkedList;
0055: import java.util.List;
0056: import java.util.Locale;
0057: import java.util.Map;
0058: import java.util.MissingResourceException;
0059: import java.util.NoSuchElementException;
0060: import java.util.Properties;
0061: import java.util.ResourceBundle;
0062: import java.util.WeakHashMap;
0063: import java.util.jar.Attributes;
0064: import java.util.logging.Level;
0065: import java.util.logging.Logger;
0066:
0067: /** Convenience class permitting easy loading of localized resources of various sorts.
0068: * Extends the functionality of the default Java resource support, and interacts
0069: * better with class loaders in a multiple-loader system.
0070: * <p>Example usage:
0071: * <p><code><pre>
0072: * package com.mycom;
0073: * public class Foo {
0074: * // Search for tag Foo_theMessage in /com/mycom/Bundle.properties:
0075: * private static String theMessage = {@link NbBundle#getMessage(Class, String) NbBundle.getMessage} (Foo.class, "Foo_theMessage");
0076: * // Might also look in /com/mycom/Bundle_de.properties, etc.
0077: * }
0078: * </pre></code>
0079: */
0080: public class NbBundle extends Object {
0081:
0082: private static final boolean USE_DEBUG_LOADER = Boolean
0083: .getBoolean("org.openide.util.NbBundle.DEBUG"); // NOI18N
0084: private static String brandingToken = null;
0085:
0086: /**
0087: * Cache of URLs for localized files.
0088: * Keeps only weak references to the class loaders.
0089: * @see "#9275"
0090: */
0091: static final Map<ClassLoader, Map<String, URL>> localizedFileCache = new WeakHashMap<ClassLoader, Map<String, URL>>();
0092:
0093: /**
0094: * Cache of resource bundles.
0095: */
0096: static final Map<ClassLoader, Map<String, Reference<ResourceBundle>>> bundleCache = new WeakHashMap<ClassLoader, Map<String, Reference<ResourceBundle>>>();
0097:
0098: /**
0099: * Do not call.
0100: * @deprecated There is no reason to instantiate or subclass this class.
0101: * All methods in it are static.
0102: */
0103: @Deprecated
0104: public NbBundle() {
0105: }
0106:
0107: /** Get the current branding token.
0108: * @return the branding, or <code>null</code> for none
0109: */
0110: public static String getBranding() {
0111: return brandingToken;
0112: }
0113:
0114: /** Set the current branding token.
0115: * The permitted format, as a regular expression:
0116: * <pre>[a-z][a-z0-9]*(_[a-z][a-z0-9]*)*</pre>
0117: * <p class="nonnormative">
0118: * This is normally only called by NetBeans startup code and unit tests.
0119: * Currently the branding may be specified by passing the <code>--branding</code>
0120: * command-line option to the launcher.
0121: * </p>
0122: * @param bt the new branding, or <code>null</code> to clear
0123: * @throws IllegalArgumentException if in an incorrect format
0124: */
0125: public static void setBranding(String bt)
0126: throws IllegalArgumentException {
0127: if (bt != null
0128: && !bt.matches("[a-z][a-z0-9]*(_[a-z][a-z0-9]*)*")) { // NOI18N
0129: throw new IllegalArgumentException(
0130: "Malformed branding token: " + bt); // NOI18N
0131: }
0132: brandingToken = bt;
0133: }
0134:
0135: /**
0136: * Get a localized and/or branded file in the default locale with the default class loader.
0137: * <p>Note that use of this call is similar to using the URL protocol <code>nbresloc</code>
0138: * (which may in fact be implemented using the fuller form of the method).
0139: * <p>The extension may be null, in which case no final dot will be appended.
0140: * If it is the empty string, the resource will end in a dot.
0141: * @param baseName base name of file, as dot-separated path (e.g. <code>some.dir.File</code>)
0142: * @param ext extension of file (or <code>null</code>)
0143: * @return URL of matching localized file
0144: * @throws MissingResourceException if not found
0145: * @deprecated Use the <code>nbresloc</code> URL protocol instead. This method does a poor
0146: * job of handling resources such as <samp>/some.dir/res.txt</samp> or
0147: * <samp>/some/res.txt.sample</samp>.
0148: */
0149: @Deprecated
0150: public static synchronized URL getLocalizedFile(String baseName,
0151: String ext) throws MissingResourceException {
0152: return getLocalizedFile(baseName, ext, Locale.getDefault(),
0153: getLoader());
0154: }
0155:
0156: /**
0157: * Get a localized and/or branded file with the default class loader.
0158: * @param baseName base name of file, as dot-separated path (e.g. <code>some.dir.File</code>)
0159: * @param ext extension of file (or <code>null</code>)
0160: * @param locale locale of file
0161: * @return URL of matching localized file
0162: * @throws MissingResourceException if not found
0163: * @deprecated Use the <code>nbresloc</code> URL protocol instead. This method does a poor
0164: * job of handling resources such as <samp>/some.dir/res.txt</samp> or
0165: * <samp>/some/res.txt.sample</samp>.
0166: */
0167: @Deprecated
0168: public static synchronized URL getLocalizedFile(String baseName,
0169: String ext, Locale locale) throws MissingResourceException {
0170: return getLocalizedFile(baseName, ext, locale, getLoader());
0171: }
0172:
0173: /**
0174: * Get a localized and/or branded file.
0175: * @param baseName base name of file, as dot-separated path (e.g. <code>some.dir.File</code>)
0176: * @param ext extension of file (or <code>null</code>)
0177: * @param locale locale of file
0178: * @param loader class loader to use
0179: * @return URL of matching localized file
0180: * @throws MissingResourceException if not found
0181: * @deprecated Use the <code>nbresloc</code> URL protocol instead. This method does a poor
0182: * job of handling resources such as <samp>/some.dir/res.txt</samp> or
0183: * <samp>/some/res.txt.sample</samp>.
0184: */
0185: @Deprecated
0186: public static synchronized URL getLocalizedFile(String baseName,
0187: String ext, Locale locale, ClassLoader loader)
0188: throws MissingResourceException {
0189: // [PENDING] in the future, could maybe do something neat if
0190: // USE_DEBUG_LOADER and ext is "html" or "txt" etc...
0191: URL lookup = null;
0192: Iterator<String> it = new LocaleIterator(locale);
0193: List<String> cacheCandidates = new ArrayList<String>(10);
0194: String baseNameSlashes = baseName.replace('.', '/');
0195: Map<String, URL> perLoaderCache = localizedFileCache
0196: .get(loader);
0197:
0198: if (perLoaderCache == null) {
0199: localizedFileCache.put(loader,
0200: perLoaderCache = new HashMap<String, URL>());
0201: }
0202:
0203: // #31008: better use of domain cache priming.
0204: // [PENDING] remove this hack in case the domain cache is precomputed
0205: URL baseVariant;
0206: String path;
0207:
0208: if (ext != null) {
0209: path = baseNameSlashes + '.' + ext;
0210: } else {
0211: path = baseNameSlashes;
0212: }
0213:
0214: lookup = perLoaderCache.get(path);
0215:
0216: if (lookup == null) {
0217: baseVariant = loader.getResource(path);
0218: } else {
0219: // who cares? already in cache anyway
0220: baseVariant = null;
0221: }
0222:
0223: while (it.hasNext()) {
0224: String suffix = it.next();
0225:
0226: if (ext != null) {
0227: path = baseNameSlashes + suffix + '.' + ext;
0228: } else {
0229: path = baseNameSlashes + suffix;
0230: }
0231:
0232: lookup = perLoaderCache.get(path);
0233:
0234: if (lookup != null) {
0235: break;
0236: }
0237:
0238: cacheCandidates.add(path);
0239:
0240: if (suffix.length() == 0) {
0241: lookup = baseVariant;
0242: } else {
0243: lookup = loader.getResource(path);
0244: }
0245:
0246: if (lookup != null) {
0247: break;
0248: }
0249: }
0250:
0251: if (lookup == null) {
0252: path = baseName.replace('.', '/');
0253:
0254: if (ext != null) {
0255: path += ('.' + ext);
0256: }
0257:
0258: throw new MissingResourceException(
0259: "Cannot find localized resource " + path + " in "
0260: + loader, loader.toString(), path); // NOI18N
0261: } else {
0262: // Note that this is not 100% accurate. If someone calls gLF on something
0263: // with a locale/branding combo such as _brand_ja, and the answer is found
0264: // as _ja, then a subsequent call with param _brand will find this _ja
0265: // version - since the localizing iterator does *not* have the property that
0266: // each subsequent item is more general than the previous. However, this
0267: // situation is very unlikely, so consider this close enough.
0268: it = cacheCandidates.iterator();
0269:
0270: while (it.hasNext()) {
0271: perLoaderCache.put(it.next(), lookup);
0272: }
0273:
0274: return lookup;
0275: }
0276: }
0277:
0278: /**
0279: * Find a localized and/or branded value for a given key and locale.
0280: * Scans through a map to find
0281: * the most localized match possible. For example:
0282: * <p><code><PRE>
0283: * findLocalizedValue (hashTable, "keyName", new Locale ("cs_CZ"))
0284: * </PRE></code>
0285: * <p>This would return the first non-<code>null</code> value obtained from the following tests:
0286: * <UL>
0287: * <LI> <CODE>hashTable.get ("keyName_cs_CZ")</CODE>
0288: * <LI> <CODE>hashTable.get ("keyName_cs")</CODE>
0289: * <LI> <CODE>hashTable.get ("keyName")</CODE>
0290: * </UL>
0291: *
0292: * @param table mapping from localized strings to objects
0293: * @param key the key to look for
0294: * @param locale the locale to use
0295: * @return the localized object or <code>null</code> if no key matches
0296: */
0297: public static <T> T getLocalizedValue(Map<String, T> table,
0298: String key, Locale locale) {
0299: for (String suffix : NbCollections.iterable(new LocaleIterator(
0300: locale))) {
0301: String physicalKey = key + suffix;
0302: T v = table.get(physicalKey);
0303:
0304: if (v != null) {
0305: // ok
0306: if (USE_DEBUG_LOADER && (v instanceof String)) {
0307: // Not read from a bundle, but still localized somehow:
0308: @SuppressWarnings("unchecked")
0309: T _v = (T) (((String) v) + " (?:" + physicalKey + ")"); // NOI18N;
0310: return _v;
0311: } else {
0312: return v;
0313: }
0314: }
0315: }
0316:
0317: return null;
0318: }
0319:
0320: /**
0321: * Find a localized and/or branded value for a given key in the default system locale.
0322: *
0323: * @param table mapping from localized strings to objects
0324: * @param key the key to look for
0325: * @return the localized object or <code>null</code> if no key matches
0326: * @see #getLocalizedValue(Map,String,Locale)
0327: */
0328: public static <T> T getLocalizedValue(Map<String, T> table,
0329: String key) {
0330: return getLocalizedValue(table, key, Locale.getDefault());
0331: }
0332:
0333: /**
0334: * Find a localized and/or branded value in a JAR manifest.
0335: * @param attr the manifest attributes
0336: * @param key the key to look for (case-insensitive)
0337: * @param locale the locale to use
0338: * @return the value if found, else <code>null</code>
0339: */
0340: public static String getLocalizedValue(Attributes attr,
0341: Attributes.Name key, Locale locale) {
0342: return getLocalizedValue(attr2Map(attr), key.toString()
0343: .toLowerCase(Locale.US), locale);
0344: }
0345:
0346: /**
0347: * Find a localized and/or branded value in a JAR manifest in the default system locale.
0348: * @param attr the manifest attributes
0349: * @param key the key to look for (case-insensitive)
0350: * @return the value if found, else <code>null</code>
0351: */
0352: public static String getLocalizedValue(Attributes attr,
0353: Attributes.Name key) {
0354: // Yes, US locale is intentional! The attribute name may only be ASCII anyway.
0355: // It is necessary to lowercase it *as ASCII* as in Turkish 'I' does not go to 'i'!
0356: return getLocalizedValue(attr2Map(attr), key.toString()
0357: .toLowerCase(Locale.US));
0358: }
0359:
0360: /** Necessary because Attributes implements Map; however this is dangerous!
0361: * The keys are Attributes.Name's, not Strings.
0362: * Also manifest lookups should not be case-sensitive.
0363: * (Though the locale suffix still will be!)
0364: */
0365: private static Map<String, String> attr2Map(Attributes attr) {
0366: return new AttributesMap(attr);
0367: }
0368:
0369: // ---- LOADING RESOURCE BUNDLES ----
0370:
0371: /**
0372: * Get a resource bundle with the default class loader and locale/branding.
0373: * <strong>Caution:</strong> {@link #getBundle(Class)} is generally
0374: * safer when used from a module as this method relies on the module's
0375: * classloader to currently be part of the system classloader. NetBeans
0376: * does add enabled modules to this classloader, however calls to
0377: * this variant of the method made in <a href="@org-openide-modules@/org/openide/modules/ModuleInstall.html#validate()">ModuleInstall.validate</a>,
0378: * or made soon after a module is uninstalled (due to background threads)
0379: * could fail unexpectedly.
0380: * @param baseName bundle basename
0381: * @return the resource bundle
0382: * @exception MissingResourceException if the bundle does not exist
0383: */
0384: public static final ResourceBundle getBundle(String baseName)
0385: throws MissingResourceException {
0386: return getBundle(baseName, Locale.getDefault(), getLoader());
0387: }
0388:
0389: /** Get a resource bundle in the same package as the provided class,
0390: * with the default locale/branding and the class' own classloader.
0391: * This is the usual style of invocation.
0392: *
0393: * @param clazz the class to take the package name from
0394: * @return the resource bundle
0395: * @exception MissingResourceException if the bundle does not exist
0396: */
0397: public static ResourceBundle getBundle(Class clazz)
0398: throws MissingResourceException {
0399: String name = findName(clazz);
0400:
0401: return getBundle(name, Locale.getDefault(), clazz
0402: .getClassLoader());
0403: }
0404:
0405: /** Finds package name for given class */
0406: private static String findName(Class clazz) {
0407: String pref = clazz.getName();
0408: int last = pref.lastIndexOf('.');
0409:
0410: if (last >= 0) {
0411: pref = pref.substring(0, last + 1);
0412:
0413: return pref + "Bundle"; // NOI18N
0414: } else {
0415: // base package, search for bundle
0416: return "Bundle"; // NOI18N
0417: }
0418: }
0419:
0420: /**
0421: * Get a resource bundle with the default class loader and branding.
0422: * @param baseName bundle basename
0423: * @param locale the locale to use (but still uses {@link #getBranding default branding})
0424: * @return the resource bundle
0425: * @exception MissingResourceException if the bundle does not exist
0426: */
0427: public static final ResourceBundle getBundle(String baseName,
0428: Locale locale) throws MissingResourceException {
0429: return getBundle(baseName, locale, getLoader());
0430: }
0431:
0432: /** Get a resource bundle the hard way.
0433: * @param baseName bundle basename
0434: * @param locale the locale to use (but still uses {@link #getBranding default branding})
0435: * @param loader the class loader to use
0436: * @return the resource bundle
0437: * @exception MissingResourceException if the bundle does not exist
0438: */
0439: public static final ResourceBundle getBundle(String baseName,
0440: Locale locale, ClassLoader loader)
0441: throws MissingResourceException {
0442: if (USE_DEBUG_LOADER) {
0443: loader = DebugLoader.get(loader);
0444: }
0445:
0446: // Could more simply use ResourceBundle.getBundle (plus some special logic
0447: // with MergedBundle to handle branding) instead of manually finding bundles.
0448: // However this code is faster and has some other desirable properties.
0449: // Cf. #13847.
0450: ResourceBundle b = getBundleFast(baseName, locale, loader);
0451:
0452: if (b != null) {
0453: return b;
0454: } else {
0455: MissingResourceException e = new MissingResourceException(
0456: "No such bundle " + baseName, baseName, null); // NOI18N
0457:
0458: if (Lookup.getDefault().lookup(ClassLoader.class) == null) {
0459: Exceptions.attachMessage(e,
0460: "Class loader not yet initialized in lookup"); // NOI18N
0461: } else {
0462: Exceptions.attachMessage(e, "Offending classloader: "
0463: + loader); // NOI18N
0464: }
0465:
0466: throw e;
0467: }
0468: }
0469:
0470: /**
0471: * Get a resource bundle by name.
0472: * Like {@link ResourceBundle#getBundle(String,Locale,ClassLoader)} but faster,
0473: * and also understands branding.
0474: * First looks for <samp>.properties</samp>-based bundles, then <samp>.class</samp>-based.
0475: * @param name the base name of the bundle, e.g. <samp>org.netbeans.modules.foo.Bundle</samp>
0476: * @param locale the locale to use
0477: * @param loader a class loader to search in
0478: * @return a resource bundle (locale- and branding-merged), or null if not found
0479: */
0480: private static ResourceBundle getBundleFast(String name,
0481: Locale locale, ClassLoader loader) {
0482: Map<String, Reference<ResourceBundle>> m;
0483:
0484: synchronized (bundleCache) {
0485: m = bundleCache.get(loader);
0486:
0487: if (m == null) {
0488: bundleCache
0489: .put(
0490: loader,
0491: m = new HashMap<String, Reference<ResourceBundle>>());
0492: }
0493: }
0494:
0495: //A minor optimization to cut down on StringBuffer allocations - OptimizeIt
0496: //showed the commented out code below was a major source of them. This
0497: //just does the same thing with a char array - Tim
0498: String localeStr = locale.toString();
0499: char[] k = new char[name.length()
0500: + ((brandingToken != null) ? brandingToken.length() : 1)
0501: + 2 + localeStr.length()];
0502: name.getChars(0, name.length(), k, 0);
0503: k[name.length()] = '/'; //NOI18N
0504:
0505: int pos = name.length() + 1;
0506:
0507: if (brandingToken == null) {
0508: k[pos] = '-'; //NOI18N
0509: pos++;
0510: } else {
0511: brandingToken.getChars(0, brandingToken.length(), k, pos);
0512: pos += brandingToken.length();
0513: }
0514:
0515: k[pos] = '/'; //NOI18N
0516: pos++;
0517: localeStr.getChars(0, localeStr.length(), k, pos);
0518:
0519: String key = new String(k);
0520:
0521: /*
0522: String key = name + '/' + (brandingToken != null ? brandingToken : "-") + '/' + locale; // NOI18N
0523: */
0524: synchronized (m) {
0525: Reference<ResourceBundle> o = m.get(key);
0526: ResourceBundle b = o != null ? o.get() : null;
0527:
0528: if (b != null) {
0529: return b;
0530: } else {
0531: b = loadBundle(name, locale, loader);
0532:
0533: if (b != null) {
0534: m.put(key, new TimedSoftReference<ResourceBundle>(
0535: b, m, key));
0536: } else {
0537: // Used to cache misses as well, to make the negative test faster.
0538: // However this caused problems: see #31578.
0539: }
0540:
0541: return b;
0542: }
0543: }
0544: }
0545:
0546: /**
0547: * Load a resource bundle (without caching).
0548: * @param name the base name of the bundle, e.g. <samp>org.netbeans.modules.foo.Bundle</samp>
0549: * @param locale the locale to use
0550: * @param loader a class loader to search in
0551: * @return a resource bundle (locale- and branding-merged), or null if not found
0552: */
0553: private static ResourceBundle loadBundle(String name,
0554: Locale locale, ClassLoader loader) {
0555: String sname = name.replace('.', '/');
0556: Iterator<String> it = new LocaleIterator(locale);
0557: LinkedList<String> l = new LinkedList<String>();
0558:
0559: while (it.hasNext()) {
0560: l.addFirst(it.next());
0561: }
0562:
0563: Properties p = new Properties();
0564:
0565: for (String suffix : l) {
0566: String res = sname + suffix + ".properties";
0567:
0568: // #49961: don't use getResourceAsStream; catch all errors opening it
0569: URL u = loader != null ? loader.getResource(res)
0570: : ClassLoader.getSystemResource(res);
0571:
0572: if (u != null) {
0573: //System.err.println("Loading " + res);
0574: try {
0575: // #51667: but in case we are in USE_DEBUG_LOADER mode, use gRAS (since getResource is not overridden)
0576: InputStream is = USE_DEBUG_LOADER ? (loader != null ? loader
0577: .getResourceAsStream(res)
0578: : ClassLoader
0579: .getSystemResourceAsStream(res))
0580: : u.openStream();
0581:
0582: try {
0583: p.load(is);
0584: } finally {
0585: is.close();
0586: }
0587: } catch (IOException e) {
0588: Exceptions
0589: .attachMessage(e, "While loading: " + res); // NOI18N
0590: Logger.getLogger(NbBundle.class.getName()).log(
0591: Level.WARNING, null, e);
0592:
0593: return null;
0594: }
0595: } else if (suffix.length() == 0) {
0596: // No base *.properties. Try *.class.
0597: // Note that you may not mix *.properties w/ *.class this way.
0598: return loadBundleClass(name, sname, locale, l, loader);
0599: }
0600: }
0601:
0602: return new PBundle(NbCollections.checkedMapByFilter(p,
0603: String.class, String.class, true), locale);
0604: }
0605:
0606: /**
0607: * Load a class-based resource bundle.
0608: * @param name the base name of the bundle, e.g. <samp>org.netbeans.modules.foo.Bundle</samp>
0609: * @param sname the name with slashes, e.g. <samp>org/netbeans/modules/foo/Bundle</samp>
0610: * @param locale the locale to use
0611: * @param suffixes a list of suffixes to apply to the bundle name, in <em>increasing</em> order of specificity
0612: * @param loader a class loader to search in
0613: * @return a resource bundle (merged according to the suffixes), or null if not found
0614: */
0615: private static ResourceBundle loadBundleClass(String name,
0616: String sname, Locale locale, List<String> suffixes,
0617: ClassLoader l) {
0618: if (l != null && l.getResource(sname + ".class") == null) { // NOI18N
0619:
0620: // No chance - no base bundle. Don't waste time catching CNFE.
0621: return null;
0622: }
0623:
0624: ResourceBundle master = null;
0625:
0626: for (String suffix : suffixes) {
0627: try {
0628: Class<? extends ResourceBundle> c = Class.forName(
0629: name + suffix, true, l).asSubclass(
0630: ResourceBundle.class);
0631: ResourceBundle b = c.newInstance();
0632:
0633: if (master == null) {
0634: master = b;
0635: } else {
0636: master = new MergedBundle(locale, b, master);
0637: }
0638: } catch (ClassNotFoundException cnfe) {
0639: // fine - ignore
0640: } catch (Exception e) {
0641: Logger.getLogger(NbBundle.class.getName()).log(
0642: Level.WARNING, null, e);
0643: } catch (LinkageError e) {
0644: Logger.getLogger(NbBundle.class.getName()).log(
0645: Level.WARNING, null, e);
0646: }
0647: }
0648:
0649: return master;
0650: }
0651:
0652: //
0653: // Helper methods to simplify localization of messages
0654: //
0655:
0656: /**
0657: * Finds a localized and/or branded string in a bundle.
0658: * @param clazz the class to use to locate the bundle
0659: * @param resName name of the resource to look for
0660: * @return the string associated with the resource
0661: * @throws MissingResourceException if either the bundle or the string cannot be found
0662: */
0663: public static String getMessage(Class clazz, String resName)
0664: throws MissingResourceException {
0665: return getBundle(clazz).getString(resName);
0666: }
0667:
0668: /**
0669: * Finds a localized and/or branded string in a bundle and formats the message
0670: * by passing requested parameters.
0671: *
0672: * @param clazz the class to use to locate the bundle
0673: * @param resName name of the resource to look for
0674: * @param param1 the argument to use when formatting the message
0675: * @return the string associated with the resource
0676: * @throws MissingResourceException if either the bundle or the string cannot be found
0677: * @see java.text.MessageFormat#format(String,Object[])
0678: */
0679: public static String getMessage(Class clazz, String resName,
0680: Object param1) throws MissingResourceException {
0681: return getMessage(clazz, resName, new Object[] { param1 });
0682: }
0683:
0684: /**
0685: * Finds a localized and/or branded string in a bundle and formats the message
0686: * by passing requested parameters.
0687: *
0688: * @param clazz the class to use to locate the bundle
0689: * @param resName name of the resource to look for
0690: * @param param1 the argument to use when formatting the message
0691: * @param param2 the second argument to use for formatting
0692: * @return the string associated with the resource
0693: * @throws MissingResourceException if either the bundle or the string cannot be found
0694: * @see java.text.MessageFormat#format(String,Object[])
0695: */
0696: public static String getMessage(Class clazz, String resName,
0697: Object param1, Object param2)
0698: throws MissingResourceException {
0699: return getMessage(clazz, resName,
0700: new Object[] { param1, param2 });
0701: }
0702:
0703: /**
0704: * Finds a localized and/or branded string in a bundle and formats the message
0705: * by passing requested parameters.
0706: *
0707: * @param clazz the class to use to locate the bundle
0708: * @param resName name of the resource to look for
0709: * @param param1 the argument to use when formatting the message
0710: * @param param2 the second argument to use for formatting
0711: * @param param3 the third argument to use for formatting
0712: * @return the string associated with the resource
0713: * @throws MissingResourceException if either the bundle or the string cannot be found
0714: * @see java.text.MessageFormat#format(String,Object[])
0715: */
0716: public static String getMessage(Class clazz, String resName,
0717: Object param1, Object param2, Object param3)
0718: throws MissingResourceException {
0719: return getMessage(clazz, resName, new Object[] { param1,
0720: param2, param3 });
0721: }
0722:
0723: /**
0724: * Finds a localized and/or branded string in a bundle and formats the message
0725: * by passing requested parameters.
0726: *
0727: * @param clazz the class to use to locate the bundle
0728: * @param resName name of the resource to look for
0729: * @param arr array of parameters to use for formatting the message
0730: * @return the string associated with the resource
0731: * @throws MissingResourceException if either the bundle or the string cannot be found
0732: * @see java.text.MessageFormat#format(String,Object[])
0733: */
0734: public static String getMessage(Class clazz, String resName,
0735: Object[] arr) throws MissingResourceException {
0736: return java.text.MessageFormat.format(
0737: getMessage(clazz, resName), arr);
0738: }
0739:
0740: /** @return default class loader which is used, when we don't have
0741: * any other class loader. (in function getBundle(String), getLocalizedFile(String),
0742: * and so on...
0743: */
0744: private static ClassLoader getLoader() {
0745: ClassLoader c = Lookup.getDefault().lookup(ClassLoader.class);
0746:
0747: return (c != null) ? c : ClassLoader.getSystemClassLoader();
0748: }
0749:
0750: /**
0751: * Get a list of all suffixes used to search for localized/branded resources.
0752: * Based on the default locale and branding, returns the list of suffixes
0753: * which various <code>NbBundle</code> methods use as the search order.
0754: * For example, when {@link #getBranding} returns <code>branding</code>
0755: * and the default locale is German, you might get a sequence such as:
0756: * <ol>
0757: * <li><samp>"_branding_de"</samp>
0758: * <li><samp>"_branding"</samp>
0759: * <li><samp>"_de"</samp>
0760: * <li><samp>""</samp>
0761: * </ol>
0762: * @return a read-only iterator of type <code>String</code>
0763: * @since 1.1.5
0764: */
0765: public static Iterator<String> getLocalizingSuffixes() {
0766: return new LocaleIterator(Locale.getDefault());
0767: }
0768:
0769: /**
0770: * Do not use.
0771: * @param loaderFinder ignored
0772: * @deprecated Useless.
0773: */
0774: @Deprecated
0775: public static void setClassLoaderFinder(
0776: ClassLoaderFinder loaderFinder) {
0777: throw new Error();
0778: }
0779:
0780: /**
0781: * Do not use.
0782: * @deprecated Useless.
0783: */
0784: @Deprecated
0785: public static interface ClassLoaderFinder {
0786: /**
0787: * Do not use.
0788: * @return nothing
0789: * @deprecated Useless.
0790: */
0791: @Deprecated
0792: public ClassLoader find();
0793: }
0794:
0795: private static class AttributesMap extends HashMap<String, String> {
0796: private Attributes attrs;
0797:
0798: public AttributesMap(Attributes attrs) {
0799: super (7);
0800: this .attrs = attrs;
0801: }
0802:
0803: public String get(Object _k) {
0804: if (!(_k instanceof String)) {
0805: return null;
0806: }
0807: String k = (String) _k;
0808:
0809: Attributes.Name an;
0810:
0811: try {
0812: an = new Attributes.Name(k);
0813: } catch (IllegalArgumentException iae) {
0814: // Robustness, and workaround for reported MRJ locale bug:
0815: Exceptions.attachLocalizedMessage(iae, getMessage(
0816: NbBundle.class, "EXC_bad_attributes_name", k,
0817: Locale.getDefault().toString()));
0818: Exceptions.printStackTrace(iae);
0819:
0820: return null;
0821: }
0822:
0823: return attrs.getValue(an);
0824: }
0825: }
0826:
0827: /**
0828: * A resource bundle based on <samp>.properties</samp> files (or any map).
0829: */
0830: private static final class PBundle extends ResourceBundle {
0831: private final Map<String, String> m;
0832: private final Locale locale;
0833:
0834: /**
0835: * Create a new bundle based on a map.
0836: * @param m a map from resources keys to values (typically both strings)
0837: * @param locale the locale it represents <em>(informational)</em>
0838: */
0839: public PBundle(Map<String, String> m, Locale locale) {
0840: this .m = m;
0841: this .locale = locale;
0842: }
0843:
0844: public Enumeration<String> getKeys() {
0845: return Collections.enumeration(m.keySet());
0846: }
0847:
0848: protected Object handleGetObject(String key) {
0849: return m.get(key);
0850: }
0851:
0852: public Locale getLocale() {
0853: return locale;
0854: }
0855: }
0856:
0857: /** Special resource bundle which delegates to two others.
0858: * Ideally could just set the parent on the first, but this is protected, so...
0859: */
0860: private static class MergedBundle extends ResourceBundle {
0861: private Locale loc;
0862: private ResourceBundle sub1;
0863: private ResourceBundle sub2;
0864:
0865: /**
0866: * Create a new bundle delegating to two others.
0867: * @param loc the locale it represents <em>(informational)</em>
0868: * @param sub1 one delegate (taking precedence over the other in case of overlap)
0869: * @param sub2 the other (weaker) delegate
0870: */
0871: public MergedBundle(Locale loc, ResourceBundle sub1,
0872: ResourceBundle sub2) {
0873: this .loc = loc;
0874: this .sub1 = sub1;
0875: this .sub2 = sub2;
0876: }
0877:
0878: public Locale getLocale() {
0879: return loc;
0880: }
0881:
0882: public Enumeration<String> getKeys() {
0883: return Enumerations.removeDuplicates(Enumerations.concat(
0884: sub1.getKeys(), sub2.getKeys()));
0885: }
0886:
0887: protected Object handleGetObject(String key)
0888: throws MissingResourceException {
0889: try {
0890: return sub1.getObject(key);
0891: } catch (MissingResourceException mre) {
0892: // Ignore exception, and...
0893: return sub2.getObject(key);
0894: }
0895: }
0896: }
0897:
0898: /** This class (enumeration) gives all localized sufixes using nextElement
0899: * method. It goes through given Locale and continues through Locale.getDefault()
0900: * Example 1:
0901: * Locale.getDefault().toString() -> "_en_US"
0902: * you call new LocaleIterator(new Locale("cs", "CZ"));
0903: * ==> You will gets: "_cs_CZ", "_cs", "", "_en_US", "_en"
0904: *
0905: * Example 2:
0906: * Locale.getDefault().toString() -> "_cs_CZ"
0907: * you call new LocaleIterator(new Locale("cs", "CZ"));
0908: * ==> You will gets: "_cs_CZ", "_cs", ""
0909: *
0910: * If there is a branding token in effect, you will get it too as an extra
0911: * prefix, taking precedence, e.g. for the token "f4jce":
0912: *
0913: * "_f4jce_cs_CZ", "_f4jce_cs", "_f4jce", "_f4jce_en_US", "_f4jce_en", "_cs_CZ", "_cs", "", "_en_US", "_en"
0914: *
0915: * Branding tokens with underscores are broken apart naturally: so e.g.
0916: * branding "f4j_ce" looks first for "f4j_ce" branding, then "f4j" branding, then none.
0917: */
0918: private static class LocaleIterator extends Object implements
0919: Iterator<String> {
0920: /** this flag means, if default locale is in progress */
0921: private boolean defaultInProgress = false;
0922:
0923: /** this flag means, if empty suffix was exported yet */
0924: private boolean empty = false;
0925:
0926: /** current locale, and initial locale */
0927: private Locale locale;
0928:
0929: /** current locale, and initial locale */
0930: private Locale initLocale;
0931:
0932: /** current suffix which will be returned in next calling nextElement */
0933: private String current;
0934:
0935: /** the branding string in use */
0936: private String branding;
0937:
0938: /** Creates new LocaleIterator for given locale.
0939: * @param locale given Locale
0940: */
0941: public LocaleIterator(Locale locale) {
0942: this .locale = this .initLocale = locale;
0943:
0944: if (locale.equals(Locale.getDefault())) {
0945: defaultInProgress = true;
0946: }
0947:
0948: current = '_' + locale.toString();
0949:
0950: if (brandingToken == null) {
0951: branding = null;
0952: } else {
0953: branding = "_" + brandingToken; // NOI18N
0954: }
0955:
0956: //System.err.println("Constructed: " + this);
0957: }
0958:
0959: /** @return next suffix.
0960: * @exception NoSuchElementException if there is no more locale suffix.
0961: */
0962: public String next() throws NoSuchElementException {
0963: if (current == null) {
0964: throw new NoSuchElementException();
0965: }
0966:
0967: final String ret;
0968:
0969: if (branding == null) {
0970: ret = current;
0971: } else {
0972: ret = branding + current;
0973: }
0974:
0975: int lastUnderbar = current.lastIndexOf('_');
0976:
0977: if (lastUnderbar == 0) {
0978: if (empty) {
0979: reset();
0980: } else {
0981: current = ""; // NOI18N
0982: empty = true;
0983: }
0984: } else {
0985: if (lastUnderbar == -1) {
0986: if (defaultInProgress) {
0987: reset();
0988: } else {
0989: // [PENDING] stuff with trying the default locale
0990: // after the real one does not actually seem to work...
0991: locale = Locale.getDefault();
0992: current = '_' + locale.toString();
0993: defaultInProgress = true;
0994: }
0995: } else {
0996: current = current.substring(0, lastUnderbar);
0997: }
0998: }
0999:
1000: //System.err.println("Returning: `" + ret + "' from: " + this);
1001: return ret;
1002: }
1003:
1004: /** Finish a series.
1005: * If there was a branding prefix, restart without that prefix
1006: * (or with a shorter prefix); else finish.
1007: */
1008: private void reset() {
1009: if (branding != null) {
1010: current = '_' + initLocale.toString();
1011:
1012: int idx = branding.lastIndexOf('_');
1013:
1014: if (idx == 0) {
1015: branding = null;
1016: } else {
1017: branding = branding.substring(0, idx);
1018: }
1019:
1020: empty = false;
1021: } else {
1022: current = null;
1023: }
1024: }
1025:
1026: /** Tests if there is any sufix.*/
1027: public boolean hasNext() {
1028: return (current != null);
1029: }
1030:
1031: public void remove() throws UnsupportedOperationException {
1032: throw new UnsupportedOperationException();
1033: }
1034: }
1035:
1036: // end of LocaleIterator
1037:
1038: /** Classloader whose special trick is inserting debug information
1039: * into any *.properties files it loads.
1040: */
1041: static final class DebugLoader extends ClassLoader {
1042: /** global bundle index, each loaded bundle gets its own */
1043: private static int count = 0;
1044:
1045: /** indices of known bundles; needed since DebugLoader's can be collected
1046: * when softly reachable, but this should be transparent to the user
1047: */
1048: private static final Map<String, Integer> knownIDs = new HashMap<String, Integer>();
1049:
1050: /** cache of existing debug loaders for regular loaders */
1051: private static final Map<ClassLoader, Reference<ClassLoader>> existing = new WeakHashMap<ClassLoader, Reference<ClassLoader>>();
1052:
1053: private DebugLoader(ClassLoader cl) {
1054: super (cl);
1055:
1056: //System.err.println ("new DebugLoader: cl=" + cl);
1057: }
1058:
1059: private static int getID(String name) {
1060: synchronized (knownIDs) {
1061: Integer i = knownIDs.get(name);
1062:
1063: if (i == null) {
1064: i = ++count;
1065: knownIDs.put(name, i);
1066: System.err.println("NbBundle trace: #" + i + " = "
1067: + name); // NOI18N
1068: }
1069:
1070: return i;
1071: }
1072: }
1073:
1074: public static ClassLoader get(ClassLoader normal) {
1075: //System.err.println("Lookup: normal=" + normal);
1076: synchronized (existing) {
1077: Reference<ClassLoader> r = existing.get(normal);
1078:
1079: if (r != null) {
1080: ClassLoader dl = r.get();
1081:
1082: if (dl != null) {
1083: //System.err.println("\tcache hit");
1084: return dl;
1085: } else {
1086: //System.err.println("\tcollected ref");
1087: }
1088: } else {
1089: //System.err.println("\tnot in cache");
1090: }
1091:
1092: ClassLoader dl = new DebugLoader(normal);
1093: existing
1094: .put(normal, new WeakReference<ClassLoader>(dl));
1095:
1096: return dl;
1097: }
1098: }
1099:
1100: public InputStream getResourceAsStream(String name) {
1101: InputStream base = super .getResourceAsStream(name);
1102:
1103: if (base == null) {
1104: return null;
1105: }
1106:
1107: if (name.endsWith(".properties")) { // NOI18N
1108:
1109: int id = getID(name);
1110:
1111: //System.err.println ("\tthis=" + this + " parent=" + getParent ());
1112: boolean loc = name.indexOf("Bundle") != -1; // NOI18N
1113:
1114: return new DebugInputStream(base, id, loc);
1115: } else {
1116: return base;
1117: }
1118: }
1119:
1120: // [PENDING] getResource not overridden; but ResourceBundle uses getResourceAsStream anyhow
1121:
1122: /** Wrapper input stream which parses the text as it goes and adds annotations.
1123: * Resource-bundle values are annotated with their current line number and also
1124: * the supplied it, so e.g. if in the original input stream on line 50 we have:
1125: * somekey=somevalue
1126: * so in the wrapper stream (id 123) this line will read:
1127: * somekey=somevalue (123:50)
1128: * Since you see on stderr what #123 is, you can then pinpoint where any bundle key
1129: * originally came from, assuming NbBundle loaded it from a *.properties file.
1130: * @see {@link Properties#load} for details on the syntax of *.properties files.
1131: */
1132: static final class DebugInputStream extends InputStream {
1133: /** state transition diagram constants */
1134: private static final int WAITING_FOR_KEY = 0;
1135:
1136: /** state transition diagram constants */
1137: private static final int IN_COMMENT = 1;
1138:
1139: /** state transition diagram constants */
1140: private static final int IN_KEY = 2;
1141:
1142: /** state transition diagram constants */
1143: private static final int IN_KEY_BACKSLASH = 3;
1144:
1145: /** state transition diagram constants */
1146: private static final int AFTER_KEY = 4;
1147:
1148: /** state transition diagram constants */
1149: private static final int WAITING_FOR_VALUE = 5;
1150:
1151: /** state transition diagram constants */
1152: private static final int IN_VALUE = 6;
1153:
1154: /** state transition diagram constants */
1155: private static final int IN_VALUE_BACKSLASH = 7;
1156: private final InputStream base;
1157: private final int id;
1158: private final boolean localizable;
1159:
1160: /** current line number */
1161: private int line = 0;
1162:
1163: /** line number in effect for last-encountered key */
1164: private int keyLine = 0;
1165:
1166: /** current state in state machine */
1167: private int state = WAITING_FOR_KEY;
1168:
1169: /** if true, the last char was a CR, waiting to see if we get a NL too */
1170: private boolean twixtCrAndNl = false;
1171:
1172: /** if non-null, a string to serve up before continuing (length must be > 0) */
1173: private String toInsert = null;
1174:
1175: /** if true, the next value encountered should be localizable if normally it would not be, or vice-versa */
1176: private boolean reverseLocalizable = false;
1177:
1178: /** text of currently read comment, including leading comment character */
1179: private StringBuffer lastComment = null;
1180:
1181: /** Create a new InputStream which will annotate resource bundles.
1182: * Bundles named Bundle*.properties will be treated as localizable by default,
1183: * and so annotated; other bundles will be treated as nonlocalizable and not annotated.
1184: * Messages can be individually marked as localizable or not to override this default,
1185: * in accordance with some I18N conventions for NetBeans.
1186: * @param base the unannotated stream
1187: * @param id an identifying number to use in annotations
1188: * @param localizable if true, this bundle is expected to be localizable
1189: * @see http://www.netbeans.org/i18n/
1190: */
1191: public DebugInputStream(InputStream base, int id,
1192: boolean localizable) {
1193: this .base = base;
1194: this .id = id;
1195: this .localizable = localizable;
1196: }
1197:
1198: public int read() throws IOException {
1199: if (toInsert != null) {
1200: char result = toInsert.charAt(0);
1201:
1202: if (toInsert.length() > 1) {
1203: toInsert = toInsert.substring(1);
1204: } else {
1205: toInsert = null;
1206: }
1207:
1208: return result;
1209: }
1210:
1211: int next = base.read();
1212:
1213: if (next == '\n') {
1214: twixtCrAndNl = false;
1215: line++;
1216: } else if (next == '\r') {
1217: if (twixtCrAndNl) {
1218: line++;
1219: } else {
1220: twixtCrAndNl = true;
1221: }
1222: } else {
1223: twixtCrAndNl = false;
1224: }
1225:
1226: switch (state) {
1227: case WAITING_FOR_KEY:
1228:
1229: switch (next) {
1230: case '#':
1231: case '!':
1232: state = IN_COMMENT;
1233: lastComment = new StringBuffer();
1234: lastComment.append((char) next);
1235:
1236: return next;
1237:
1238: case ' ':
1239: case '\t':
1240: case '\n':
1241: case '\r':
1242: case -1:
1243: return next;
1244:
1245: case '\\':
1246: state = IN_KEY_BACKSLASH;
1247:
1248: return next;
1249:
1250: default:
1251: state = IN_KEY;
1252: keyLine = line + 1;
1253:
1254: return next;
1255: }
1256:
1257: case IN_COMMENT:
1258:
1259: switch (next) {
1260: case '\n':
1261: case '\r':
1262:
1263: String comment = lastComment.toString();
1264: lastComment = null;
1265:
1266: if (localizable && comment.equals("#NOI18N")) { // NOI18N
1267: reverseLocalizable = true;
1268: } else if (localizable
1269: && comment.equals("#PARTNOI18N")) { // NOI18N
1270: System.err
1271: .println("NbBundle WARNING ("
1272: + id
1273: + ":"
1274: + line
1275: + "): #PARTNOI18N encountered, will not annotate I18N parts"); // NOI18N
1276: reverseLocalizable = true;
1277: } else if (!localizable
1278: && comment.equals("#I18N")) { // NOI18N
1279: reverseLocalizable = true;
1280: } else if (!localizable
1281: && comment.equals("#PARTI18N")) { // NOI18N
1282: System.err
1283: .println("NbBundle WARNING ("
1284: + id
1285: + ":"
1286: + line
1287: + "): #PARTI18N encountered, will not annotate I18N parts"); // NOI18N
1288: reverseLocalizable = false;
1289: } else if ((localizable && (comment
1290: .equals("#I18N") || comment
1291: .equals("#PARTI18N")))
1292: || // NOI18N
1293: (!localizable && (comment
1294: .equals("#NOI18N") || comment
1295: .equals("#PARTNOI18N")))) { // NOI18N
1296: System.err.println("NbBundle WARNING ("
1297: + id + ":" + line
1298: + "): incongruous comment "
1299: + comment + " found for bundle"); // NOI18N
1300: reverseLocalizable = false;
1301: }
1302:
1303: state = WAITING_FOR_KEY;
1304:
1305: return next;
1306:
1307: default:
1308: lastComment.append((char) next);
1309:
1310: return next;
1311: }
1312:
1313: case IN_KEY:
1314:
1315: switch (next) {
1316: case '\\':
1317: state = IN_KEY_BACKSLASH;
1318:
1319: return next;
1320:
1321: case ' ':
1322: case '\t':
1323: state = AFTER_KEY;
1324:
1325: return next;
1326:
1327: case '=':
1328: case ':':
1329: state = WAITING_FOR_VALUE;
1330:
1331: return next;
1332:
1333: case '\r':
1334: case '\n':
1335: state = WAITING_FOR_KEY;
1336:
1337: return next;
1338:
1339: default:
1340: return next;
1341: }
1342:
1343: case IN_KEY_BACKSLASH:
1344: state = IN_KEY;
1345:
1346: return next;
1347:
1348: case AFTER_KEY:
1349:
1350: switch (next) {
1351: case '=':
1352: case ':':
1353: state = WAITING_FOR_VALUE;
1354:
1355: return next;
1356:
1357: case '\r':
1358: case '\n':
1359: state = WAITING_FOR_KEY;
1360:
1361: return next;
1362:
1363: default:
1364: return next;
1365: }
1366:
1367: case WAITING_FOR_VALUE:
1368:
1369: switch (next) {
1370: case '\r':
1371: case '\n':
1372: state = WAITING_FOR_KEY;
1373:
1374: return next;
1375:
1376: case ' ':
1377: case '\t':
1378: return next;
1379:
1380: case '\\':
1381: state = IN_VALUE_BACKSLASH;
1382:
1383: return next;
1384:
1385: default:
1386: state = IN_VALUE;
1387:
1388: return next;
1389: }
1390:
1391: case IN_VALUE:
1392:
1393: switch (next) {
1394: case '\\':
1395:
1396: // Gloss over distinction between simple escapes and \u1234, which is not important for us.
1397: // Also no need to deal specially with continuation lines; for us, there is an escaped
1398: // newline, after which will be more value, and that is all that is important.
1399: state = IN_VALUE_BACKSLASH;
1400:
1401: return next;
1402:
1403: case '\n':
1404: case '\r':
1405: case -1:
1406:
1407: // End of value. This is the tricky part.
1408: boolean revLoc = reverseLocalizable;
1409: reverseLocalizable = false;
1410: state = WAITING_FOR_KEY;
1411:
1412: if (localizable ^ revLoc) {
1413: // This value is intended to be localizable. Annotate it.
1414: assert keyLine > 0;
1415: toInsert = "(" + id + ":" + keyLine + ")"; // NOI18N
1416: if (next != -1) {
1417: toInsert += new Character((char) next);
1418: }
1419: keyLine = 0;
1420:
1421: // Now return the space before the rest of the string explicitly.
1422: return ' ';
1423: } else {
1424: // This is not supposed to be a localizable value, leave it alone.
1425: return next;
1426: }
1427:
1428: default:
1429: return next;
1430: }
1431:
1432: case IN_VALUE_BACKSLASH:
1433: state = IN_VALUE;
1434:
1435: return next;
1436:
1437: default:
1438: throw new IOException("should never happen"); // NOI18N
1439: }
1440: }
1441:
1442: }
1443: }
1444: }
|