“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.