Wednesday, October 1, 2014

java.text.MessageFormat sucks

More on that in a minute.

In related news: Throwable.getLocalizedMessage() is practically useless, to the point it might as well be deprecated. I may be the only developer in the history of the Java language to actually implement that method.*

// java -Duser.language=fr
void printError(MyAwesomeException e) {
   System.out.println(e.getMessage()); // "by jove!"
   System.out.println(e.getLocalizedMessage()); // "sacrebleu!"
}

* Gross exaggeration

Now back to MessageFormat. By default it will only handle the formates built in to the JRE. Extending it is done per instance, by argument.

class ExceptionFormat extends Format {
    @Override
    public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) {
        if (!(obj instanceof Throwable)) throw new IllegalArgumentException();
        Throwable t = (Throwable) obj;
        String sMsg = t.getLocalizedMessage();
        if (sMsg == null) sMsg = t.getMessage();
        if (sMsg == null) sMsg = t.getClass().getName();
        toAppendTo.append(sMsg);
        return toAppendTo;
    }
    @Override
    public Object parseObject(String source, ParsePosition pos) {
        throw new UnsupportedOperationException();
    }
}

// java -Duser.language=fr
void printError(MyAwesomeException e) {
    MessageFormat mf = new MessageFormat("Error: {0}");
    mf.setFormatByArgumentIndex(0, new ExceptionFormat());
    System.out.println(mf.format(new Object[]{e})); // "Error: sacrebleu!"
}

Unfortunately, not only would be a hassle to manually set a custom format every time, it wouldn't even work half the time.

// java -Duser.language=fr
void logError(Exception e) {
   Logger log = Logger.getAnonymousLogger("fish");
   log.log(Level.WARN, "Error: {0}", new Object[] { e }); // "Error: by jove!"
   // NO!
}

MessageFormat is nested deep within the bowels of the JRE/JDK and many 3rd party libraries. It's permanently infected, so there's no way to override many cases.

While you can't get rid of MessageFormat entirely, there are a few better alternatives available.

ICU MessageFormat

If you are serious about localizing your app, it's just a matter of time before you start using the ICU library. It's pretty much an industry standard. The best part is it fixes the notorious apostrophe (') issue. You can also add arguments by name, as well as number. Unfortunately it doesn't solve my problem.

Apache lang ExtendedMessageFormat

Lets you register format factories, but only at the instance level.

class ExceptionFormatFactory implements FormatFactory {
    public Format getFormat(String name, String arguments, Locale locale) {
        if (!"exception".equals(name)) throw new IllegalArgumentException();
        return new ExceptionFormat();
    }
}

// java -Duser.language=fr
void printError(MyAwesomeException e) {
    Map<String, ExceptionFormatFactory> registry = new Map<String, ExceptionFormatFactory>();
    registry.put("exception", new ExceptionFormatFactory());
    ExtendedMessageFormat mf = new ExtendedMessageFormat("Error: {0,exception}");
    System.out.println(mf.format(new Object[]{e})); // "Error: sacrebleu!"
}

When using "choice" subformats, support for nested formatting instructions is limited to that provided by the base class.

Here is an example where there's no way around the notorious MessageFormat.

Humanize [Extended]MessageFormat

By jove this might be as good as it gets. This appears to actually let you register formatters at a global level so all uses of this class will automatically benefit.

public static class ExceptionFormatProvider implements FormatProvider {
    @Override
    public FormatFactory getFactory() {
        return new ExceptionFormatFactory();
    }
    @Override
    public String getFormatName() {
        return "exception";
    }
}

// java -Duser.language=fr
void printError(MyAwesomeException e) {
    System.out.println(Humanize.format("Error: {0,exception}", e)); // "Error: sacrebleu!"
}