Friday, January 18, 2013

java.util.Timezone

What happens when calling Timezone.getTimezone("blabla")?

Even seen, what happens when you access a Timezone using java.uil.Timezone.getTimezone("blabla")? I would expect an exception to be thrown, since the input is completely invalid, but nothing similar happens. The method silently returns a TimeZone initialized to GMT+0.
But there are other flaws in TimeZone. At a first look, it looks like an immutable object, but when look at it carefully, there is a setId(String) method. This allows to set the time zone ID to an arbitrary value:
TimeZone tz = Timezone.getTimezone("Europe/Zurich"); 
     // will return a zone with GMT+1 offset (DST ignored)
tz.setId("blabla");
     // now we have a time zone 'blabla' with identical offsets.
System.out.println(tz);

This results in the following output:
sun.util.calendar.ZoneInfo[id="blabla",offset=3600000,dstSavings=3600000,useDaylight=true,transitions=119,lastRule=java.util.SimpleTimeZone[id=blabla,offset=3600000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=2,startMonth=2,startDay=-1,startDayOfWeek=1,startTime=3600000,startTimeMode=2,endMode=2,endMonth=9,endDay=-1,endDayOfWeek=1,endTime=3600000,endTimeMode=2]]

Obviously this API is very dangerous. So looking forward what JSR 310 will bring...

JDK used was 1.7.09 and jdk-8-ea-bin-b74-windows-x64-24_jan_2013, but it is similarly also the case in earlier versions.



Wednesday, January 16, 2013

Complex Formatting and Parsing

Complex Formatting and Parsing

Problem

In the area of internationalization (i18n) formatting and parsing is a typical use case. Unfortunately java.util.Locale as the only parameter controlling such a process has shown not to be sufficient. Refer to the following use cases:

  • Formatting of an monetary amount contains of a numeric part, as well as a currency part. Now both can be required to be formatted using different locales, e.g. 
    • German numeric format and 
    • English currency symbol.
  • But when considering display requirements for financial applications very different formats are required
    • based on the usage scenario, e.g. numbers included into balances may be totally different than numbers formatted on a account summary (different number groups, signs and symbol placements etc).
    • based on individual user settings.
    • based on the type of formatting, e.g. for display, print out or for textual representation within legacy systems.
  • When parsing a literal representation back into a type instance similar scenarios may be possible. Additionally, especially when parsing user input, the things can get even more complex:
    • the currency symbol may be English, similar to above, but...
    • the number formats supported can be in several formats, since users do not always enter the 100% correct format (which is totally OK from a user's perspective!).
    • additionally depending on use case different number precision may ve required, that must not match the fraction digits defined by the currency entered (e.g. 2 for Swiss Francs, but 0 for Japanese Yen). In some usage scenarios one even wants lenient fraction parsing to be possible.
Summarizing a Locale with its country, language, variant scheme is not sufficient to define the scenarios above. 

Solution: Defining a LocalizationStyle

I propose to model such things as LocalizationStyle, containing the following data:
  • an identifier defining the style. Different use cases (e.g. different display scenarios, as well as technical scenarios can be separated).
  • a target type, since a style typically is bound to a specific type and should not be mixed up.
  • a leading Locale, or translation Locale.
  • an (optional) number Locale, if missing falling back to the translation Locale
  • an (optional) date Locale, if missing falling back to the translation Locale
  • an (optional) time Locale, if missing falling back to the date Locale
  • any additional optional attributes
Additionally a Locale is an immutable instance. With a LocalizationStyle this would be also be feasible:
  • by defining a LocalizationStyleFactory to create immutable instances.
  • or by setting a LocaliuationStyle to read-only, when it is completely initialized and configured.
Consequently the most simple and common variant of a LocalizationStyle is basically very similar as a Locale:
  • it's identifier is set to "default". This is also visible by the corresponding method boolean isDefault(); which returns true.
  • Its leading translation Localeis set to the required Locale.
  • Since this functionaliy is quite common, it can be provided using a static factory method:
    LocalizationStyle style = LocalizationStyle .valueOf(Locale.GERMAN);

Using the LocalizationStyle for Configuring Complex Formatting and Parsing


As a consequence according formatters and parsers only must support LocalizationStyle as valid input parameters (Locale can still be supported for convenience), e.g.
  String format(T item, LocalizationStyle style);
or
  String getLocalized(LocalizationStyle style);

But now it is possible to pass much more detailed configuration what how formatter or parser must behave. Nevertheless in mny cases such formatters/parsers are not to be implemented as big and complex single data types. It is recommended to implement something like a FormatterManager/ParserManager that is able to manage the different implementations. Nevertheless since a Formatter/Parser instance can be identified by the duplet [style-id, target-type] this is not as complex as it seems on a first look.

Detailed interface

The proposed class would be as follows:

public class LocalizationStyle implements Serializable {

/**
* serialVersionUID.
*/
private static final long serialVersionUID = 8612440355369457473L;
/** The internal key used for a time locale set. */
public static final String TIME_LOCALE = "timeLocale";
/** The internal key used for a date locale set. */
public static final String DATE_LOCALE = "dateLocale";
/** The internal key used for a number locale set. */
public static final String NUMBER_LOCALE = "numberLocale";
/** The internal key used for a translation locale set (default). */
public static final String TRANSLATION_LOCALE = "locale";
/** The internal key used for a formatting/parsing style. */
private static final String DEFAULT_ID = "default";
/** The style's name, by default ({@link #DEFAULT_ID}. */
private String id;
/** The style's generic properties. */
private Map<String, Object> attributes = Collections
.synchronizedMap(new HashMap<String, Object>());

/**
* Flag to make a localization style read only, so it can be used (and
* cached) similar to a immutable object.
*/
private boolean readOnly = false;

/**
* Creates a new instance of a style. This method will use the Locale
* returned from {@link Locale#getDefault()} as the style's default locale.
* @param id
*            The style's identifier (not null).
*/
public LocalizationStyle(String id) {
this(id, Locale.getDefault());
}

/**
* Creates a new instance of a style.
* @param id
*            The style's identifier (not null).
* @param locale
*            the default locale to be used for all locale usages.
*/
public LocalizationStyle(String id, Locale locale) {
this(id, locale, locale);
}

/**
* Creates a new instance of a style.
* @param id
*            The style's identifier (not null).
* @param translationLocale
*            the default locale (translation locale) to be used for all
*            locale usages.
* @param numberLocale
*            the locale to be used for numbers.
*/
public LocalizationStyle(String id, Locale translationLocale,
Locale numberLocale) {
if (id == null) {
throw new IllegalArgumentException("ID must not be null.");
}
this.id = id;
}

/**
* Creates a new instance of a style. This method will copy all attributes
* and properties from the given style. The style created will not be
* read-only, even when the base style is read-only.
* @param baseStyle
*            The style to be used as a base style.
*/
public LocalizationStyle(LocalizationStyle baseStyle) {
this.attributes.putAll(baseStyle.getAttributes());
this.id = baseStyle.getId();
}

/**
* Allows to evaluate if a style is a default style. A style is a default
* style, if its id equals to {@link #DEFAULT_ID}.
* <p>
* Note that nevertheless multiple default style instances may be defined
* that are not equal, since its attributes may differ.
* @return true, if this style is a default style.
*/
public boolean isDefault() {
return DEFAULT_ID.equals(getId());
}

/**
* This method allows to check, if the given style can be changed or, if it
* read only.
* @return true, if the style is read-only.
*/
public final boolean isReadOnly() {
return readOnly;
}

/**
* This method renders this style instance into an immutable instance.
* Subsequent calls to {@link #setAttribute(String, Serializable)},
* {@link #setDateLocale(Locale)}, {@link #setNumberLocale(Locale)},
* {@link #setTimeLocale(Locale)}or {@link #removeAttribute(String)} will
* throw an {@link IllegalStateException}.
*/
public void setImmutable() {
this.readOnly = true;
}

/**
* Method used to simply create a {@link IllegalStateException}, if this
* instance is read-only. This prevents duplicating the corresponding code.
*/
private void throwsExceptionIfReadonly() {
if (readOnly) {
throw new IllegalStateException(
"This instance is immutable and can not be ^changed.");
}
}

/**
* Get the style's identifier, not null.
* @return the style's id.
*/
public String getId() {
return id;
}

/**
* Get the style's (default) locale used for translation of textual values,
* and (if not specified explicitly as a fallback) for date, time and
* numbers.
* @return the translation (default) locale
*/
public final Locale getTranslationLocale() {
Locale locale = (Locale) getAttribute(TRANSLATION_LOCALE);
if (locale != null) {
return locale;
}
return Locale.getDefault();
}

/**
* Get the style's locale used for formatting/parsing of numbers.
* @return the number locale
*/
public final Locale getNumberLocale() {
Locale locale = (Locale) getAttribute(NUMBER_LOCALE);
if (locale != null) {
return locale;
}
return getTranslationLocale();
}

/**
* Get the style's locale for formatting/parsing of date instances.
* @return the date locale
*/
public final Locale getDateLocale() {
Locale locale = (Locale) getAttribute(DATE_LOCALE);
if (locale != null) {
return locale;
}
return getTranslationLocale();
}

/**
* Set the style's locale for formatting/parsing of dates.
* @param locale
*            The date locale to be used, or null for falling back to the
*            translation locale.
* @return the date locale previously set, or null.
*/
public final Locale setDateLocale(Locale locale) {
return (Locale) setAttribute(DATE_LOCALE, locale);
}

/**
* Set the style's locale for formatting/parsing of time.
* @param locale
*            The time locale to be used, or null for falling back to the
*            translation locale.
* @return the time locale previously set, or null.
*/
public final Locale setTimeLocale(Locale locale) {
return (Locale) setAttribute(TIME_LOCALE, locale);
}

/**
* Set the style's locale for formatting/parsing of numbers.
* @param locale
*            The number locale to be used, or null for falling back to the
*            number locale.
* @return the number locale previously set, or null.
*/
public final Locale setNumberLocale(Locale locale) {
return (Locale) setAttribute(NUMBER_LOCALE, locale);
}

/**
* Get the style's locale for formatting/parsing of time data.
* @return the time locale
*/
public final Locale getTimeLocale() {
Locale locale = (Locale) getAttribute(TIME_LOCALE);
if (locale != null) {
return locale;
}
return getDateLocale();
}

/**
* Get the current defined properties fo this style.
* @return the properties defined
*/
public final Map<String, Object> getAttributes() {
synchronized (attributes) {
return new HashMap<String, Object>(attributes);
}
}

/**
* Sets the given property. This method is meant for adding custom
* properties. Setting a predefined property, e.g. {@link #DATE_LOCALE} will
* throw an {@link IllegalArgumentException}.
* @param key
*            The target key
* @param value
*            The target value
* @return The object previously set, or null.
* @throws IllegalArgumentException
*             if the key passed equals to a key used for a predefined
*             property.
*/
public Object setAttribute(String key, Serializable value) {
throwsExceptionIfReadonly();
synchronized (attributes) {
return attributes.put(key, value);
}
}

/**
* Read a property from this style.
* @param key
*            The property's key
* @return the current property value, or null.
*/
public Object getAttribute(String key) {
synchronized (attributes) {
return attributes.get(key);
}
}

/**
* Removes the given property. This method is meant for removing custom
* properties. Setting a predefined property, e.g. {@link #DATE_LOCALE} will
* throw an {@link IllegalArgumentException}.
* @param key
*            The key to be removed
* @return The object previously set, or null.
* @throws IllegalArgumentException
*             if the key passed equals to a key used for a predefined
*             property.
*/
public Object removeAttribute(String key) {
throwsExceptionIfReadonly();
synchronized (attributes) {
return attributes.remove(key);
}
}

/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
synchronized (attributes) {
result = prime * result
+ ((attributes == null) ? 0 : attributes.hashCode());
}
return result;
}

/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
LocalizationStyle other = (LocalizationStyle) obj;
synchronized (attributes) {
if (attributes == null) {
if (other.attributes != null)
return false;
} else if (!attributes.equals(other.attributes))
return false;
}
return true;
}

/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
synchronized (attributes) {
return "LocalizationContext [id=" + id + ", properties="
+ attributes + "]";
}
}

/**
* Factory method to create a {@link LocalizationStyle} using a single
* {@link Locale}.
* @param locale
* @return
*/
public static LocalizationStyle of(Locale locale) {
return new LocalizationStyle(DEFAULT_ID, locale);
}

}