Tuesday, May 24, 2022

Java Tutorial: ResourceBundle

Chapters

Introduction

ResourceBundle enables us to pack and load locale-specific data in a persistent file. By using resource bundles, we can assign a name to our application's elements, such as GUI elements, with multiple-locale.

Also, updating locale-specific data in a resource bundle is much more easier than hardcoding it in our code. Thus, it's recommended to use resource bundles for storing names such as names of GUI elements. resource bundle has a hierarchy.

We define a base name to our base resource. Then, we can create a family of resources. For naming convention, Each bundle name in a resource bundle family, except for the base resource, should copy the name of the base resource with an abbreviation of language code. We may append country code if there's a language code in the name. We may append platform code if the country and language code are present in the name.

User underscore to separate base name and codes. For example:
//base name
MyResources

//sibling name with language code
MyResources_en

//sibling name with language code
//and country code
MyResources_en_US

//sibling name with language code,
//country code and platform code
MyResources_en_US_UNIX
For language code reference, you may look at this article. For country code reference, you may look at this article.

Codes in the file name are case-insensitive
e.g. MyResources_en_uS
However, by convention, lowercase language code; uppercase country and platform codes are preferrable. In a list resource file name, country and platform codes should in uppercase.

Resource bundles store data in the form of case-sensitive key-value pairs. Resource bundle is categorized into two types: PropertyResourceBundle and ListResourceBundle.

PropertyResourceBundle

PropertyResourceBundle is a concrete subclass of ResourceBundle that manages resources for a locale using a set of static strings from a property file. The file extension of a property file is .properties

These are the elements that we can write to our properties file.
# Labels
HiLabel = Hi

! Buttons
abortButton Abort
aboutButton: About
"#" and "!" denotes a comment. Characters after these symbols are ignored. Next, key-value pairs can be separated by whitespace ( ), colon (:) or equal (=) symbols.

API Note:
PropertyResourceBundle can be constructed either from an InputStream or a Reader, which represents a property file. Constructing a PropertyResourceBundle instance from an InputStream requires that the input stream be encoded in UTF-8.

By default, if a MalformedInputException or an UnmappableCharacterException occurs on reading the input stream, then the PropertyResourceBundle instance resets to the state before the exception, re-reads the input stream in ISO-8859-1, and continues reading.

If the system property java.util.PropertyResourceBundle.encoding is set to either "ISO-8859-1" or "UTF-8", the input stream is solely read in that encoding, and throws the exception if it encounters an invalid sequence.

If "ISO-8859-1" is specified, characters that cannot be represented in ISO-8859-1 encoding must be represented by Unicode Escapes as defined in section 3.3 of The Java Language Specification whereas the other constructor which takes a Reader does not have that limitation.

Other encoding values are ignored for this system property. The system property is read and evaluated when initializing this class. Changing or removing the property has no effect after the initialization.

ListResourceBundle

ListResourceBundle is an abstract subclass of ResourceBundle that manages resources for a locale in a convenient and easy to use list. File extension of this resource bundle is .java

This is how we create key-value pairs in this resource bundle.
import java.util.ListResourceBundle;
import java.time.LocalDate;

public class ListResource extends ListResourceBundle{

  @Override
  protected Object[][] getContents(){
    return new Object[][]{
      {"item1", "Toy Car"},
      {"release-date", LocalDate.of(2021, 10, 20)},
      {"tags", new String[]{"Automotive", "Toys"}}
    };
  }
}
Remember that keys are String type and values can be of any type.

Using ResourceBundle

Now we know how to create a resource bundle. Let's try using a resource bundle in our program. First off, let's create a properties files. Create a file and name it "PropertyResources.properties" and write this in the file:
# Labels
HiLabel: Hi

# Buttons
abortButton: Abort
aboutButton: About
We're going to create a family bundle so let's create another bundle and name it "PropertyResource_en_GB.properties" and write this in the file:
# Labels
HiLabel = Hello

# Buttons
okButton Okay
aboutButton: About
Next, let's create a code that will access our bundle.
import java.util.Locale;
import java.util.ResourceBundle;

public class SampleClass{

  public static void main(String[] args){
    ResourceBundle rb =
    ResourceBundle.getBundle
    ("MyResources", Locale.UK);
    //If your resource is in a package
    //("Package-Name.MyResources", Locale.UK);
    
    for(String key : rb.keySet()){
      System.out.println(key + " | " + 
      rb.getString(key));
    }
  }
}

Result
HiLabel | Hello
okButton | Okay
aboutButton | About
This code above also works with list resources which have .java file extension. Remember to compile your list resources first because java look for .class file of your list resources. In the getBundle method, we just need to put the base name of our resource bundle. We don't need to add file extensions or codes like country code.

One of the advantages of list resource to a property resource is that list resource contains values of any type whereas property resource only contain values of String type. We can use handleGetObject(String key) if we want to convert values to Object type that can be cast to other object types. handleGetObject(String key) gets an object for the given key.

handleGetObject(String key) has protected access in ResourceBundle. We need to use ListResourceBundle or PropertiesResourceBundle reference in order to access this method. For example:
ListResourceBundle prb = 
(ListResourceBundle)ResourceBundle.getBundle
("MyResources", Locale.US);
...
prb.handleGetObject(key);
ListResourceBundle has getContents() method. This method returns an array in which each item is a pair of objects in an Object array. Resource bundles can be deployed in different ways such as deploying them together with an application. You can read them in the documentation.

When java looks up for a resource bundle, it looks for a specific bundle with specified locale. If there's no match, java will look for a bundle in the family with a locale that is equivalent to our platform's default locale which is returned by invoking Locale.getDefault.

If there's still no match, java will look for default resource bundle or base resource bundle. If there's still no match, an exception will be thrown. If a property resource and list resource have the same name and in the same package or directory, java will prioritize the list resource.

Inheritance

Resource bundle hierarchy implements inheritance. A bundle with more specific name in the family inherits key-value pairs from a bundle with less specific name. For example, Assume we have three resource bundles in the same family and the base name of the family is "MyResources":
//base 
MyResources.properties
okButton = Ok

//specific
MyResources_en.properties
acceptButton = accept

//more specific
MyResources_en_GB.properties
confirmButton = confirm
When we use MyResources_en_GB.properties in our program, this resource bundle will inherit the values in MyResources.properties and MyResources_en.properties.

However, if the bundle with more specific name has equivalent key-value pairs to its less specific counterparts, the key-value pairs in the bundle with more specific name override the equivalent key-value pairs in the bundle with less specific name.

bundles with equivalent specificity don't inherit key-value pairs. For example:
...
MyResources_en_US.properties
confirmButton = confirm

MyResources_en_GB.properties
commitButton = commit
When we use MyResources_en_GB.properties, it won't inherit the key-value pair in MyResources_en_US.properties. Also, list resource and property resource have different hierarchies. Thus, a list resource can't inherit any key-value pairs from property resource and vice-versa.

ResourceBundle.Control

ResourceBundle.Control defines a set of callback methods that are invoked by the ResourceBundle.getBundle factory methods during the bundle loading process.

In other words, a ResourceBundle.Control collaborates with the factory methods for loading resource bundles. The default implementation of the callback methods provides the information necessary for the factory methods to perform the default behavior.

We can override some methods of ResourceBundle.Control to change some behaviors during bundle loading process. For example, we can override getCandidateLocales method to filter candidate locales.
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.ResourceBundle.Control;

public class SampleClass{

  public static void main(String[] args){
    ResourceBundle rb = 
    ResourceBundle.getBundle
    ("MyResources", Locale.FRANCE, 
     new CustomResourceControl());
     
    for(String key : rb.keySet()){
      System.out.println(key + " | " + 
      rb.getString(key));
    }
     
  }
}

class CustomResourceControl extends 
ResourceBundle.Control{
  
  @Override
  public List<Locale> 
  getCandidateLocales(String s, Locale locale) {
    if(locale.getCountry().equals("US") || 
       locale.getCountry().equals("UK")){
       return super.getCandidateLocales(s, locale);
    }
    else{
      System.err.println
      ("Warning: Locale should be US or UK.");
      System.err.println("Locale has been "
      +"automatically set to Locale.ROOT");
      return Arrays.asList(Locale.ROOT);
      System.out.println();
    }
  }
}

Result
Warning: Locale should be US or UK.
Locale has been automatically set to Locale.ROOT

No comments:

Post a Comment