MessageFormat aufgefrischt

“How you look at it is pretty much how you’ll see it”

Rasheed Ogunlaru

Die Klasse MessageFormat ist ein Urgestein der Java API und schon seit der Version 1.1 mit an Bord. Die Klasse ist ein nützlicher Helfer, wurde aber in den letzten Versionen der API stark vernachlässigt. Nicht nur in der Java API wurde sie längere Zeit nicht beachtet, auch manches Projekt hat diese Klasse unnötigerweise gemieden.

Sollen in einer Java Anwendungen Textfragmente produziert werden, dann bietet es sich an, diese Texte nicht in der Anwendung selbst zu speichern, sondern in ResourceBundles außerhalb des Codes. In diesem Fall können die Textfragmente unabhängig vom Quellcode geändert und sogar übersetzt werden. Ein Beispiel für solch eine Anwendung ist im Beitrag Kalenderspielereien mit Java – I18N beschrieben.

Solche Texte sind aber in der Regel nicht statisch sondern müssen dynamische Teile enthalten. Dazu werden, je nach Lösungsansatz, spezielle Platzhalter verwendet. Dies sind häufig die Platzhalter im printf Style Formats des java.util.Formatter oder die Platzhalter für das MessageFormat. Da der Beitrag sich mit der Klasse MessageFormat beschäftigt, folgt hier ein ResourceBundle mit einem einzelnen englischsprachigen Eintrag.

licence.period=Your {0,choice,1#licence is|2#two licences|2<{0} licences are} valid between {1,date} and {2,date}.

Der Eintrag enthält drei Platzhalter. Der erste ist ein Choice Format, das abhängig von einer numerischen Variablen unterschiedliche Texte darstellt. Dies erlaubt Texte mit unterschiedlichen Numerus. Im obigen Beispiel wird für unterschiedliche licenceCount Werte (1, 2 und 42) unterschiedliche Formulierungen ausgegeben.

Your licence is valid between 2.11.2022 and 14.11.2022.
Your two licences are valid between 2.11.2022 and 14.11.2022.
Your 42 licences are valid between 2.11.2022 and 14.11.2022.

Die beiden anderen werden als Datumswerte entsprechend des aktuellen Locale dargestellt. Der Eintrag kann mit dem MessageFormat einfach formatiert werden.

String text = MessageFormat.format(bundle.getMessage("licence.period"), licenceCount, beginDate, endDate);

Leider ist bei dem MessageFormat die Zeit ein wenig stehen geblieben, denn als Datumswerte können nur die klassischen java.util.Date und ihre Untertypen direkt interpretiert werden.

Die folgende Variante führt zu einem Fehler, da LocalDate nicht als Datum erkannt wird.

String text = MessageFormat.format(bundle.getMessage("licence.period"), licenceType, LocalDate.now(), LocalDate.now().plusDays(14L));

Glücklicherweise bietet die Java API einige Methoden, die uns aus dieser misslichen Situation befreien können. Die folgende Klasse MessageFormatHelper nutzt diese Methoden um LocalTime und andere Klassen aus dem java.time Package mit dem MessageFormat zu verwenden.

public final class MessageFormatHelper {
  public static MessageFormat create(String pattern, Locale locale) {
    return patchDateTimeFormatter(new MessageFormat(pattern, locale));
  }

  public static MessageFormat create(String pattern) {
    return create(pattern, Locale.getDefault());
  }

  public static String format(String pattern, Object... arguments) {
    return create(pattern).format(arguments);
  }

  private MessageFormat patchDateTimeFormatter(MessageFormat messageFormat) {
    Format[] formats = messageFormat.getFormats();
    for (int i = 0; i < formats.length; i++) {
      if (formats[i] instanceof SimpleDateFormat format) {
        formats[i] = DateTimeFormatter.ofPattern(format.toPattern()).toFormat();
      }
    }
    messageFormat.setFormats(formats);
    return messageFormat;
  }
}

Die ersten beiden Methoden erzeugen eine modifiziertes MessageFormat Instanz, mit dem Default oder einem explizit angegebenen Locale. Diese sind nützlich, wenn die MessageFormat Instanz wiederverwendet werden soll. Ansonsten ist die dritte Methode das Pendant der format Methode aus der MessageFormat Klasse. Mit ihr kann der bislang fehlerhafte Aufruf mit LocalDate Parametern erfolgreich durchgeführt werden.

Der Trick liegt in der Methode patchDateTimeFormatter Methode verborgen. Diese Methode liest die aktuell konfigurierten Format Instanzen aus und ersetzt alle SimpleDateFormat Instanzen durch DateTimeFormatter mit dem entsprechenden Pattern.

Diese Variante hat den kleinen Nachteil, dass nur java.time Klassen oder die veralteten Date Klassen zu verwenden sind. Ein Mischen (warum auch immer nötig) innerhalb eines format Aufrufs ist so nicht möglich.

Eine Alternative ist eine format Methode, in der anhand der Argumente die Format Instanzen ausgetauscht werden. Eine Implementierung ist im folgenden Beispiel dargestellt.

private String format(String pattern, Object... arguments) {
  MessageFormat messageFormat = new MessageFormat(pattern);
  for (int i = 0; i < arguments.length; i++) {
    if (arguments[i] instanceof Temporal) {
      Format format = messageFormat.getFormatsByArgumentIndex()[i];
      if (format instanceof SimpleDateFormat simpleDateFormat) {
        messageFormat.setFormatByArgumentIndex(i,
            DateTimeFormatter.ofPattern(simpleDateFormat.toPattern()).toFormat());
      }
    }
  }
  return messageFormat.format(arguments);
}

Nur wenn das Argument eine Instanz von Temporal ist und eine SimpleDateFormat verwendet wird, tauscht die Methode die Format Instanz aus.

Hoffen wir darauf, dass eines Tages auch die java.time Klassen direkt aus der MessageFormat Klasse verwendet werden können. Nützlich wäre es allemal.

Schreibe einen Kommentar