|
Welcome to the Core Java Technologies Tech Tips for December 14, 2004. Here you'll get tips on using core Java technologies and APIs, such as those in Java 2 Platform, Standard Edition (J2SE).
This issue covers:
Resource Bundle Loading
Hiding ListResourceBundles from javadoc
These tips were developed using the Java 2 Platform Standard Edition Development Kit 5.0 (JDK 5.0). You can download JDK 5.0 at http://java.sun.com/j2se/1.5.0/download.jsp.
This issue of the Core Java Technologies Tech Tips is written by John Zukowski, president of JZ Ventures, Inc.
See the Subscribe/Unsubscribe note at the end of this newsletter to subscribe to Tech Tips that focus on technologies and products in other Java platforms.
For more Java technology content, visit these sites:
java.sun.com - The Java technology source for developers. Get the latest Java platform releases, tutorials, newsletters and more.
java.net - A web forum where enthusiasts of Java technology can collaborate and build solutions together.
java.com - The ultimate marketplace promoting Java technology,
applications and services.
Resource Bundle Loading
A resource bundle is a way of embedding text strings in a language-specific (or more precisely, locale-specific) manner. An earlier Tech Tip discussed the use of resource bundles. What follows is a short refresher. If you have a program that needs a string such as "Hello, World", one approach is to code it in the program. However with resource bundles, you don't hardcode the string. Instead, you put the string in a lookup table, and then your program looks up the string at runtime. If the program runs with a different locale, the lookup finds a different string, if translated, or finds the original string if not translated. This doesn't affect the code in your program -- it runs with the same code, irrespective of locale. The only thing you need to do is create and translate the lookup table of values.
As stated previously, resource bundles work with locales. You can say, "I want the 'greet' string for English," where English is the locale. Or, you can say you want 'color' for U.S. English, and 'colour' for U.K. English. Locales also support regionality. In other words, you can specify a phrase for one dialect of U.S. English (perhaps a phrase used in Southern California), and a different phrase for another U.S. region, say New York City.
You can define a resource bundle in a .class file that extends ListResourceBundle, or you can use a PopertyResourceBundle that is backed by a .properties file. When combining resource bundles and locales, there are two searches involved. The first finds the nearest resource bundle requested, the second finds the string for the requested key. Why the differentiation? When searching for resource bundles, the system stops as soon as it finds and loads the requested resource bundle. If the system doesn't find the key in the requested bundle, it then hunts in other resource bundles until it finds the key. Ultimately, if it doesn't find the key, the system throws a MissingResourceException.
To demonstrate, suppose you want to find a string for a New York locale in a bundle named Greeting. Suppose too that your Locale was created as follows:
Locale newYork = new Locale("en", "US", "NewYork")
and you asked for a resource bundle like this:
ResourceBundle bundle =
ResourceBundle.getBundle("Greeting", newYork);
The system first looks for the .class file for the bundle. With a region/variant level of locale, such as New York, the file would be Greeting_en_US_NewYork.class. If the system can't find the .class file in the classpath, it then searches for the file Greeting_en_US_NewYork.properties. And if it can't find that file, the system subsequently searches for Greeting_en_US.class, followed by Greeting_en_US.properties, Greeting_en.class, Greeting_en.properties, Greeting.class, and Greeting.properties. The searching stops when the system finds the resource bundle. Thankfully, there is caching involved, so the system doesn't always search everywhere, but that's still potentially a lot of different places that have to be searched.
The system then performs a second round of lookups -- this time for the requested key. If the key isn't in the bundle it found, the system looks for more resource bundles, beyond the language, country, and variant level of the current bundle. This could load more bundles, whether they are .class files or .properties files.
One question you might have is which approach is better, using .class files or using .properties files? Notice that .class files are searched for first, then .properties files. Also note that .class files are loaded directly by the class loader, but .properties files have to be parsed each time the bundle needs to be loaded. Parsing is a two-pass process. To deal with Unicode strings such as \uXXX, the system must scan each key=value line twice, and then split the key from the value.
Let's investigate both approaches further by comparing load times. Start with the following test program:
import java.util.*;
public class Test1 {
public static void main(String args[]) {
Locale locale = Locale.ENGLISH;
long start = System.nanoTime();
ResourceBundle myResources =
ResourceBundle.getBundle("MyResources", locale);
long end1 = System.nanoTime();
String string = myResources.getString("HelpKey");
long end2 = System.nanoTime();
System.out.println("Load: " + (end1 - start));
System.out.println("Fetch: " + (end2 - end1));
System.out.println("HelpKey: " + string);
}
}
If you are running on a 1.4 Java platform, you need to change the test program so that it calls currentTimeMillis instead of nanoTime. The nanoTime method works with nanosecond precision. The currentTimeMillis works only in milliseconds. Also, see the note about microbenchmarks at the end of this tip.
Next, create a ListResourceBundle class in the same directory as the test program:
import java.util.*;
public class MyResources extends ListResourceBundle {
public Object[][] getContents() {
return contents;
}
private static final Object[][] contents = {
{"OkKey", "OK"},
{"CancelKey", "Cancel"},
{"HelpKey", "Help"},
{"YesKey", "Yes"},
{"NoKey", "No"},
};
}
Compile the test program and the MyResources class. Then run the test program.
Your results will depends on your operating environment, your RAM size, and the speed of your processor. Here's a result produced in a 800 MHz machine running Windows XP with 768 MB RAM:
Load: 25937415
Fetch: 62994
Now create a properties file, MyResources.properties, with the following elements:
OkKey=OK
CancelKey=Cancel
HelpKey=Help
YesKey=Yes
NoKey=No
Run the test program again, but first remove the MyResources class. This will run the program using the .properties files. Here's the result produced in the same machine as before:
Load: 101469357
Fetch: 35450
The load times show the ListResourceBundle approach is faster than the PropertyResourceBundle approach. But, surprisingly, the fetch times show that the PropertyResourceBundle approach is almost twice as fast as ListResourceBundle approach. With roughly a five times difference in loading and a two times difference in fetching, you'd have to do a lot of fetches to catch up. Keep in mind that a nanosecond is a billionth of a second and a millisecond is a thousandth of a second.
Now run the tests again, but this time use 100 elements in the .class and .properties files. To create the file, you can simply copy the five elements in the previous files 20 times, and change the entries slightly with each copy. For example, change OkKey to OkKey1, CancelKey to CancelKey1, and so on. Your results should follows the earlier results. Loading should be faster with the ListResourceBundle, but fetching should be faster with the PropertyResourceBundle. Actually, you should find that the load time of 100 resources for a PropertyResourceBundle is close to that of five elements.
ListResourceBundle
Load: 12782686
Fetch: 262788
PropertyResourceBundle
Load: 12600795
Fetch: 35175
Changing the Locale from language (Locale.ENGLISH) to language
and country (Locale.US) produces even more interesting results:
Loading 5 / Locale.US:
ListResourceBundle
Load: 13152117
Fetch: 32921
PropertyResourceBundle
Load: 14592024
Fetch: 261060
Loading 100 / Locale.US:
ListResourceBundle:
Load: 12837863
Fetch: 264264
PropertyResourceBundle
Load: 14468366
Fetch: 33166
In all cases, while loading the initial bundle is always faster for the ListResourceBundle, fetching is sometimes slower. So which way do you go? For smaller resource bundles, the ListResourceBundle does seem to be the faster of the two. For larger ones, it seems best to stay away from ListResourceBundle. The ListResourceBundle needs to convert the two-dimensional array into a lookup map, that's the reason for the slower time.
Looking at these results, you might think that a ListResourceBundle should never be used. For instance, for a server-based program, it is easier to maintain a .properties file than a .class file, and the load time is negligible. But, a ListResourceBundle is not just a two-dimensional array of strings. The getContents method returns an Object array:
public Object[][] getContents()
What does this mean? If you want to localize content beyond simply strings, you must use ListResourceBundle objects. This allows you to localize content such as images, colors, and dimensions. You can't have any object in a PropertyResourceBundle, only strings.
Note that the timing test in the sample program can be considered a microbenchmark. It can certainly be improved. However, with the caching of resource bundle loading, it's hard to get accurate load times when looping multiple times in the same run. Multiple runs should be used to validate results. For information on techniques for writing microbenchmarks, see the JavaOne 2002 presentation How NOT To Write A Microbenchmark. In addition, a lot of performance work in this area has been done for JDK 5.0. Your numbers may differ substantially using Java 2 SDK, Standard Edition, v 1.4.x.
For additional information about working with resource bundles,
see the javadoc for the ResourceBundle class,
the internationalization trail in the Java Tutorial, and the Core Java Internationalization page.
Hiding ListResourceBundles from javadoc
The first Tech Tip in this issue, Resource Bundle Loading, made some performance comparisons between the ListResourceBundle approach and the PropertyResourceBundle approach. If you decide to take the ListResourceBundles approach instead of the alternative PropertyResourceBundle route, there is one more thing to consider. The classes for the list resource bundles must be defined in public classes. The problem is that when you run the javadoc tool on your source tree, the API documentation for the resource bundle class will appear with the other classes if located within the same package. This level of implementation detail should be hidden. That's because if you later change to a PropertyResourceBundle, the public class will be gone, changing the public API for your product. How to you address this issue? In fact, is there a way to hide ListResourceBundles from javadoc? This tip shows you a way to do that.
By default, the javadoc tool supports two options for suppressing classes from the output. You can specify a list of all the classes in a file and direct the tool to run javadoc on this fixed set. Or you can place all the resource bundles in a package and then direct the tool to run on a set of packages that ignores the package in which the resource bundle is located. The first technique is cumbersome -- maintaining the list is difficult. The second technique prevents you from keeping the resource bundles in the same directories as the source that uses them.
So how can you customize javadoc to ignore specific classes when generating its output? The answer is that instead of generating a complete list of classes (with the resource bundle classes missing), you simply provide a list of resource bundle classes.
This solution works for both the 1.4 and 5.0 releases of J2SE. To do this you run a doclet that accepts an option, -excludefile, which excludes a set of classes that you specify. Here's how you run the doclet (note that the command should go on one line):
java -classpath <path to doclet and path to tools.jar>
ExcludeDoclet -excludefile <path to exclude file>
<javadoc options>
In response to the command, the validOptions method of the Doclet class looks for the -excludefile option. If it finds it, the method reads the contents of the exclude file -- these are the set of classes and packages to ignore. Then the start method is called. As each class or package is processed, the method throws away the classes and packages in the exclude set. The doclet includes the optionLength method, this allows the doclet to run under both J2SE 1.4 and 5.0. Here is the doclet, ExcludeDoclet:
import java.io.*;
import java.util.*;
import com.sun.tools.javadoc.Main;
import com.sun.javadoc.*;
/**
* A wrapper for Javadoc. Accepts an additional option
* called "-excludefile", which specifies which classes
* and packages should be excluded from the output.
*
* @author Jamie Ho
*/
public class ExcludeDoclet extends Doclet {
private static List m_args = new ArrayList();
private static Set m_excludeSet = new HashSet();
/**
* Iterate through the documented classes and remove the
* ones that should be excluded.
*
* @param root the initial RootDoc (before filtering).
*/
public static boolean start(RootDoc root) {
root.printNotice
("\n\nRemoving excluded source files.......\n\n");
ClassDoc[] classes = root.classes();
for (int i = 0; i < classes.length; i++) {
if (m_excludeSet.contains(classes[i].qualifiedName()) ||
m_excludeSet.contains
(classes[i].containingPackage().name())) {
root.printNotice
("Excluding " + classes[i].qualifiedName());
continue;
}
m_args.add(classes[i].position().file().getPath());
}
root.printNotice("\n\n");
return true;
}
/**
* Let every option be valid. The real validation happens
* in the standard doclet, not here. Remove the "-excludefile"
* and "-subpackages" options because they are not needed by
* the standard doclet.
*
* @param options the options from the command line.
* @param reporter the error reporter.
*/
public static boolean validOptions(String[][] options,
DocErrorReporter reporter) {
for (int i = 0; i < options.length; i++) {
if (options[i][0].equalsIgnoreCase("-excludefile")) {
try {
readExcludeFile(options[i][1]);
} catch (Exception e) {
e.printStackTrace();
}
continue;
}
if (options[i][0].equals("-subpackages")) {
continue;
}
for (int j = 0; j < options[i].length; j++) {
m_args.add(options[i][j]);
}
}
return true;
}
/**
* Parse the file that specifies which classes and packages
* to exclude from the output. You can write comments in this
* file by starting the line with a '#' character.
*
* @param filePath the path to the exclude file.
*/
private static void readExcludeFile(String filePath)
throws Exception {
LineNumberReader reader =
new LineNumberReader(new FileReader(filePath));
String line;
while ((line = reader.readLine()) != null) {
if (line.trim().startsWith("#"))
continue;
m_excludeSet.add(line.trim());
}
}
/**
* Method required to validate the length of the given option.
* This is a bit ugly but the options must be hard coded here.
* Otherwise, Javadoc will throw errors when parsing options.
* We could delegate to the Standard doclet when computing
* option lengths, but then this doclet would be dependent on
* the version of J2SE used. I'd rather hard code so that
* this doclet can be used with 1.4.x or 1.5.x.
*
* @param option the option to compute the length for.
*/
public static int optionLength(String option) {
if (option.equalsIgnoreCase("-excludefile")) {
return 2;
}
//General options
if (option.equals("-author") ||
option.equals("-docfilessubdirs") ||
option.equals("-keywords") ||
option.equals("-linksource") ||
option.equals("-nocomment") ||
option.equals("-nodeprecated") ||
option.equals("-nosince") ||
option.equals("-notimestamp") ||
option.equals("-quiet") ||
option.equals("-xnodate") ||
option.equals("-version")) {
return 1;
} else if (option.equals("-d") ||
option.equals("-docencoding") ||
option.equals("-encoding") ||
option.equals("-excludedocfilessubdir") ||
option.equals("-link") ||
option.equals("-sourcetab") ||
option.equals("-noqualifier") ||
option.equals("-output") ||
option.equals("-sourcepath") ||
option.equals("-tag") ||
option.equals("-taglet") ||
option.equals("-tagletpath")) {
return 2;
} else if (option.equals("-group") ||
option.equals("-linkoffline")) {
return 3;
}
//Standard doclet options
option = option.toLowerCase();
if (option.equals("-nodeprecatedlist") ||
option.equals("-noindex") ||
option.equals("-notree") ||
option.equals("-nohelp") ||
option.equals("-splitindex") ||
option.equals("-serialwarn") ||
option.equals("-use") ||
option.equals("-nonavbar") ||
option.equals("-nooverview")) {
return 1;
} else if (option.equals("-footer") ||
option.equals("-header") ||
option.equals("-packagesheader") ||
option.equals("-doctitle") ||
option.equals("-windowtitle") ||
option.equals("-bottom") ||
option.equals("-helpfile") ||
option.equals("-stylesheetfile") ||
option.equals("-charset") ||
option.equals("-overview")) {
return 2;
} else {
return 0;
}
}
/**
* Execute this doclet to filter out the unwanted classes
* and packages. Then execute the standard doclet.
*
* @param args The Javadoc arguments from the command line.
*/
public static void main(String[] args) {
String name = ExcludeDoclet.class.getName();
Main.execute(name, name, args);
Main.execute((String[]) m_args.toArray(new String[] {}));
}
}
Compile the doclet as follows:
javac -classpath tools.jar ExcludeDoclet.java
Replace tools.jar with the appropriate location of your JDK installation. For example, if you're running in the Windows environment and your JDK is installed in the c:\jdk1.5.0 directory, specify c:\jdk1.5.0\lib\tools.jar.
Next, create a file such as skip.txt to identify which classes to skip. Normally, this would be your set of ListResourceBundle subclasses. For this example, run ExcludeDoclet with the standard JDK classes, and ignore a set in the java.lang package:
java.lang.Math
java.lang.Long
java.lang.InternalError
java.lang.InterruptedException
java.lang.Iterable
java.lang.LinkageError
Then run the following command (on one line):
java -classpath .;c:\jdk1.5.0\lib\tools.jar ExcludeDoclet
-d docs -excludefile skip.txt -sourcepath c:\jdk1.5.0\src
-source 1.5 java.lang
The command will generate the javadoc for the java.lang package, excluding the six classes and interfaces identified in skip.txt.
Here is part of the generated javadoc showing the interfaces in the java.lang package. Notice that the Iterable interface is excluded.
For additional information about creating custom doclets, see the
tip Generating Custom Doclets.
|
|
 |
 |
|
|
 |
 |
IMPORTANT: Please read our Licensing, Terms of Use, and Privacy policies:
http://developer.java.sun.com/berkeley_license.html
http://www.sun.com/share/text/termsofuse.html
Privacy Statement: Sun respects your online time and privacy (http://sun.com/privacy). You have received this based on your email preferences. If you would prefer not to receive this information, please follow the steps at the bottom of this message to unsubscribe.
Comments? Send your feedback on the Core Java Technologies Tech Tips to: http://developers.sun.com/contact/feedback.jsp?category=newslet
Subscribe to other Java developer Tech Tips:
- Enterprise Java Technologies Tech Tips. Get tips on using enterprise Java technologies and APIs, such as those in the Java 2 Platform, Enterprise Edition (J2EE).
- Wireless Developer Tech Tips. Get tips on using wireless Java technologies and APIs, such as those in the Java 2 Platform, Micro Edition (J2ME).
To subscribe to these and other Sun Developer Network publications:
- Go to the Sun Developer Network Subscriptions page, choose the newsletters you want to subscribe to and click
"Submit".
- To unsubscribe, go to the subscriptions page, uncheck the appropriate checkbox, and click "Submit".
ARCHIVES: You'll find the Core Java Technologies Tech Tips archives at:
http://java.sun.com/developer/JDCTechTips/index.html
Copyright 2004 Sun Microsystems, Inc. All rights reserved.
4150 Network Circle, Santa Clara, CA 95054 USA.
This document is protected by copyright. For more information, see:
http://java.sun.com/developer/copyright.html
Java, J2SE, J2EE, J2ME, and all Java-based marks are trademarks or registered trademarks (http://www.sun.com/suntrademarks/) of Sun Microsystems, Inc. in the United States and other countries.
|