Jackson am Telefon (2)

„The woods are lovely, dark, and deep.
But I have promises to keep,
And miles to go before I sleep,
And miles to go before I sleep.“

Robert Frost

Im ersten Teil des Beitrags wurde gezeigt, wie man die Klasse InternationalPhoneNumber aus dem Telephone Projekt mit der Jackson Bibliothek verwenden kann. Im zweiten Teil soll die erste Implementierung noch etwas anwendungsfreundlicher werden.

Insbesondere geht es hier um die Konfiguration der Serialisierer und Deserialisierer für Jackson. Die Ideen hierfür sind der Moneta Bibliothek aus dem Beitrag JSR 354 – Ihre Nummer fürs Geld entliehen. Nicht verwunderlich für ein Projekt, das im Beitrag Das Mimikri Muster, das Licht der Welt erblickt hat.

Bislang kann die Bibliothek ein InternationalPhoneNumber Attribut in einen String umwandeln.

"phone": "+49 521 112233"

Wünschenswert ist auch die Darstellung in einer strukturierten Form.

"phone": {
  "cc": "49",
  "ndc": "521",
  "sn": "112233"
}

Auch soll die strukturierte Variante in der Lage sein, die formatierte Telefonnummer im Attribute formatted anzuzeigen.

"phone": {
  "cc": "49",
  "ndc": "521",
  "sn": "112233",
  "formatted": "+49 521 112233"
}

Um die zusätzlichen Möglichkeiten anzubieten verwaltet das TelephoneModule nicht nur einen Serialisierer und einen Deserialisierer, sondern zwei von jeder Art.

Standardmäßig ist das strukturierte Format ausgewählt und mit der Methode withSimpleFormat() wird auf das einfache Format umgestellt.

public class TelephoneModule extends SimpleModule {

  private boolean simpleFormat;
  
  public TelephoneModule withSimpleFormat() {
    simpleFormat = true;
    return this;
  }

  // ...
}

Für das strukturierte Format können Standartwerte für die Vorwahlpräfixe und dem Country Code gewählt werden. Dadurch ist die Angabe des Country Codes optional. Werden die Standardwerte mit der Methode withDefaultCodes gesetzt wird auch impliziert auf das strukturierte Format umgeschaltet.

public class TelephoneModule extends SimpleModule {

  private String defaultCountryCallingCode = "49";
  private String defaultInternationalDialingPrefix = "00";
  private String defaultNationalAccessCode = "0";

  public TelephoneModule withDefaultCodes(String internationalDialingPrefix, String nationalAccessCode,
      String countryCallingCode) {
    simpleFormat = false;
    defaultInternationalDialingPrefix = Objects.requireNonNull(internationalDialingPrefix);
    defaultNationalAccessCode = Objects.requireNonNull(nationalAccessCode);
    defaultCountryCallingCode = Objects.requireNonNull(countryCallingCode);
    return this;
  }

  // ...
}

Fur die strukturierte Variante gibt es die Möglichkeit auch die formatierte Telefonnummer zu generieren. Dies kann mit der Methode withDefaultFormat aktiviert werden. Auch hier wird ein möglicherweise aktiviertes String Format deaktiviert.

public class TelephoneModule extends SimpleModule {

  private boolean defaultFormat;
  
  public TelephoneModule withDefaultFormat() {
    defaultFormat = true;
    simpleFormat = false;
    return this;
  }
  
  // ...
}

Mit Hilfe der konfigurierten Attribute können dann die passenden Serialisierer und Deserialisierer instanziiert werden.

public class TelephoneModule extends SimpleModule {

    // ...
 
    @Override
    public void setupModule(SetupContext context) {
        if (simpleFormat) {
            setUpModules(context, new InternationalPhoneNumberSimpleSerializer(), new InternationalPhoneNumberSimpleDeserializer());
        } else {
            setUpModules(context, new InternationalPhoneNumberSerializer(defaultFormat),
                    new InternationalPhoneNumberDeserializer(defaultInternationalDialingPrefix, defaultNationalAccessCode, defaultCountryCallingCode));
        }
    }

    private void setUpModules(SetupContext context, JsonSerializer<?> ser, JsonDeserializer<?> deser) {
        context.addSerializers(new SimpleSerializers(List.of(ser)));
        context.addDeserializers(new SimpleDeserializers(Map.of(InternationalPhoneNumber.class, deser)));
    }
}

Der neu hinzugefügte InternationalPhoneNumberSerializer schreibt nicht einen String, sondern insgesamt vier und fügt am Anfang und am Ende noch die entsprechenden Methoden writeStartObject und writeEndeObject hinzu, damit die Strings sich in der Ausgabe innerhalb eines JSON Objects befinden.

final class InternationalPhoneNumberSerializer extends StdSerializer<InternationalPhoneNumber> {

  @Override
  public void serialize(InternationalPhoneNumber value, JsonGenerator gen, SerializerProvider serializers)
      throws IOException {
    gen.writeStartObject();
    gen.writeStringField("cc", value.getnternationalDialingPrefix() + value.getCountryCode());
    gen.writeStringField("ndc", value.getNationalDestinationCode());
    gen.writeStringField("sn", value.getSubscriberNumber());
    if (withDefaultFormat) {
      gen.writeStringField("formatted", value.toString());
    }
    gen.writeEndObject();
  }
}

Das Auslesen der Informationen aus einer Struktur ist etwas aufwendiger als aus einem String. Aber Jackson bietet ausreichend Möglichkeiten um diese Aufgabe zu bewältigen.

final class InternationalPhoneNumberDeserializer extends StdDeserializer<InternationalPhoneNumber> {

  private final String defaultInternationalDialingPrefix;
  private final String defaultNationalAccessCode;
  private final String defaultCountryCallingCode;

  InternationalPhoneNumberDeserializer(String internationalDialingPrefix, String nationalAccessCode,
                                       String countryCallingCode) {
    super(InternationalPhoneNumber.class);
    this.defaultInternationalDialingPrefix = Objects.requireNonNull(internationalDialingPrefix);
    this.defaultNationalAccessCode = Objects.requireNonNull(nationalAccessCode);
    this.defaultCountryCallingCode = Objects.requireNonNull(countryCallingCode);
  }

  @Override
  public InternationalPhoneNumber deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
    try {
      JsonNode node = p.getCodec().readTree(p);
      String cc = Optional.of(node.path("cc")).map(JsonNode::asText).filter(s -> !s.isEmpty())
          .orElse(defaultCountryCallingCode);
      String ndc = node.path("ndc").asText();
      String sn = node.path("sn").asText();
      if (cc.isBlank() || ndc.isBlank() || sn.isBlank()) {
        ctxt.handleWeirdStringValue(_valueClass, p.getValueAsString(),
            "not a valid representation (error: %s)", "cc, ndc or sn is missing");
      }
      return InternationalPhoneNumber.of(defaultInternationalDialingPrefix, cc,defaultNationalAccessCode, ndc, sn);
    } catch (IllegalArgumentException e) {
      return (InternationalPhoneNumber) ctxt.handleWeirdStringValue(_valueClass, p.getValueAsString(),
          "not a valid representation (error: %s)", ClassUtil.exceptionMessage(e));
    }
  }
}

Die readTree Methode liefert den Wurzelknoten der gesamten Struktur und mit path werden die jeweiligen Einträge gelesen. Angenehm an der path Methode ist, dass sie dem Null-Pattern folgt und auch für fehlende Objekte einen JsonNode liefert. Sind alle drei Einträge vorhanden, dann wird daraus eine Telefonnummer konstruiert und zurückgegeben.

Damit ist auch dieser Beitrag an sein Ende gekommen und das Rätsel zum Zitat von Robert Frost kann aufgeklärt werden. Als Jugendlicher lernte ich das Gedicht nicht in der Schule sondern durch den Spionage Film Telefon mit Charles Bronson, Lee Remick und Donald Pleasence.

Leave a Comment