Angular, i18n mit ngx-translate

Internationalisierung. Eine typische Aufgaben bei der Implementierung von Web-Anwendungen. Diese Anforderung macht auch vor Angular nicht halt. Hier bieten sich dem Entwickler gleich zwei Möglichkeiten: im ersten Teil haben wir die Möglichkeiten der Core-Bibliothek betrachtet, heute werfen wir einen Blick auf eine zusätzliche Bibliothek: ngx-translate.

i18n-2

@ngx-translate/core kann wie gewohnt per npm-Install im eigenen Projekt integriert werden. Ab hier ist die Verwendung (vielleicht im vergleich zur Core-Variante) relativ gut nach zu vollziehen. Anstatt den Build-Prozess für unterschiedliche Sprachen zu manipulieren werden die Texte hier zur Laufzeit umgewandelt. Damit das funktioniert muss das entsprechende Modul registiert und konfiguirert werden:

import {TranslateModule} from '@ngx-translate/core';


@NgModule({
  imports: [
      TranslateModule.forRoot()
  ],
   ...

Grundsätzlich wäre damit der Service schon einsatzbereit, müsste aber „manuell“ mit Übersetzungen versehen werden. In aller Regel wollen wir hier aber die Übersetzungen in einer separaten Datei vorhalten. Im Fall von ngx-translate geschieht das in einer JSON-Struktur.

{
  TITLE: 'Willkommen',
  HOME: {
    TEXT: 'Hallo Welt',
    FOOTER: 'verwendete Sprache: {{loc}}'
  }

Wie diese zugreifbar gemacht wird, wird über so genannte „TranslateLoader“ entschieden. Dabei handelt es sich um ein simples Interfaces welches entweder selber implementiert werden kann oder man greift z.B. auf den HTTPLoader (@ngx-translate/http-loader) zurück. Als Beispiel einer Implementierung wäre auch eine lokale Variante denkbar:

export class I18nLoader implements TranslateLoader {
  getTranslation(lang: string): Observable {
    if (lang === 'de') {
      return of(de);
    } else {
      return of(en);
    }
  }
}

---- Registrierung ----    
TranslateModule.forRoot({
  loader: {
    provide: TranslateLoader,
    useClass: I18nLoader
  }
})

(nicht für Produktion gedacht, alle Übersetzungen der Anwendungen würden immer zum Browser übertragen werden!)

Damit sind wir schon in der Lage unsere Übersetzungen ein zu setzen. Dies geschieht entweder:

im Template mittels Pipes

{{ 'HOME.TEXT' | translate }}
{{ 'HOME.FOOTER' | translate: { loc: 'de' } }}

oder im Code mittels Service

constructor(private translateService: TranslateService) {
  this.locale = translateService.getBrowserLang();
  translateService.use(this.locale);
  translateService.get('TITLE').subscribe(t => console.log('ngx: ' + t));
}

Das Vorgehen mittels ngx-translate ist sicherlich etwas weniger komplex in der Verwendung und Konfiguration und bietet genug Flexibilität um auch im Projektalltag gut einsetzbar zu sein. Ein Nachteil müssen wir uns allerdings bewusst machen: die Übersetzungen zur Laufzeit durchführen zu lassen kostet (zumindest ein wenig) Zeit, sodass mit der (komplizierteren?) Core-Variante ein leichter Performance-Vorteil vorliegt.

Live! In Farbe! auf github

Advertisements

Angular, i18n

Internationalisierung. Eine typische Aufgaben bei der Implementierung von Web-Anwendungen. Diese Anforderung macht auch vor Angular nicht halt. Hier bieten sich dem Entwickler gleich zwei Möglichkeiten: zum einen die Verwendung der Core-Bibliothek oder die Verwendung der Bibliothek: „ngx-translate“.

i18n-1

Angular i18n

Die Core Bibliothek bietet bereits die Funktion Texte innerhalb der Anwendung zu internationalisieren dazu ist in folgenden Schritten vorzugehen:

  1. Texte auszeichnen
  2. Übersetzungsdatei erzeugen
  3. Übersetzungen durchführen
  4. Anwendung bauen

 

Texte auszeichnen

Angular bedient sich für die Internationalisierung der Templates der Pipe „i18n“. Diese kann innerhalb des Templates verwendet werden um die Inhalte von beliebigen Komponenten zu markieren:

 
<h1>placeholder-text</h1> 

Neben der reinen Markierung existieren außerdem noch Möglichkeiten zum setzen
einer Beschreibung oder die Vergabe einer eindeutigen ID die später hilfreich ist wenn Texte der Anwendung hinzugefügt werden sollen: beschreibung@@id

 
<h1>placeholder-text</h1> 

 

Übersetzungsdatei erzeugen

Dank Angular-CLI ist die Erzeugung einer entsprechenden Übersetzungs-Datei dann ein Kinderspiel

 ng xi18n 

Führt zur Generierung einer „xlf-Datei“ welche alle markierten Textstellen als so genannte „trans-unit“ beinhaltet:

 
 
  placeholder 
   
    app/app.component.html 
    12 
  
 

 

Übersetzungen durchführen

Die Default-Sprache ist Englisch. Für weitere Übersetzungen wird die so erzeugte Datei nun kopiert und in aller Regel um einen Sprach-Post Fix erweitert (z.B. messages.de.xlf) Die eigentliche Übersetzung muss dann natürlich noch in den Dateien durchgeführt werden: dazu wird neben den bereits vorhandenen „source“ Attributen ein „target“ Attribut ergänzt, welches den übersetzten Wert erhält:

 

  (welcome) 
  Willkommen 
  ... 
 

 

(Alltagstauglich mit ngx-i18nsupport)

Bis hierher erscheint das Vorgehen noch nachvollziehbar. Allerdings zeigt sich die Core Bibliothek bei einem genaueren Blick etwas sperrig:

ein Update bestehender Texte ist direkt nicht möglich.

So würde bei einem erneuten Aufruf der Generierung die bestehende Datei einfach überschrieben werden und alle bisher gepflegten Übersetzungen wären dahin. Ein npm-Tool was hier Abhilfe schafft ist: xliffmerge welches über npm installiert werden kann (npm install –save-dev ngx-i18nsupport). Dieses Tool erlaubt uns ein intelligentes mergen von bestehenden Sprachdateien. Ein Typischer Ablauf lässt sich vermutlich am einfachsten anhand des projekt-tauglichen npm-Script erkären:

 
---- messages.config.json ----- 
{ 
 "xliffmergeOptions": { 
      "srcDir": "src/i18n", 
      "genDir": "src/i18n" 
    } 
} 

---- npm script ----- 
"i18n": 
   "ng xi18n --output-path i18n --i18n-locale en 
     && xliffmerge --profile src/messages.config.json en de" 

Zuerst lassen wir Angular-CLI ein Standard Message File generieren, welches wir in einem speziellen Ordner ablegen (ng xi18n –output-path i18n). Dann kommt xliffmerge ins Spiel: mittels einer Konfigurationsdatei (in der wir im einfachsten Fall wie zu sehen nur den Ordner angeben in dem unsere messages.xlf abgelegt wurde) und den Sprachen die wir unterstützen wollen (xliffmerge –profile src/messages.config.json en de) werden nun entsprechende Dateien erzeugt (messages.de.xlf, message.en.xlf). Der entscheidende Punkt:
sollten diese Dateien bereits vorhanden und mit Übersetzungen versorgt sein werden nur die neuen Elemente in diese Dateien übernommen.

 

Anwendung bauen

Ein zweiter Punkt dürfte den ein oder anderen überraschen. Wenn wir den AOT Compiler verwenden (was wir in aller Regel für unsere Produktions-Builds tun wollen und in der aktuellen Version auch der Standard Fall bei einem Prod-Build über
Angular CLI ist) ist ein dynamisches lesen / ermitteln der übersetzten Werte technisch nicht möglich. Der Compiler läuft ja im Fall von AOT nicht im Browser sondern bereits beim Build-Prozess.

Das heißt: ein separater Build pro unterstützte Sprache

und eine eigenständige Version unserer Anwendung die deployt werden muss. Das klingt nun etwas aufwendiger / dramatischer als es ist. Also schauen wir uns das ganze mal praktisch an:

mit AOT

Ein separater Build für jede unterstützte Sprache. Das lässt sich relativ einfach über ein npm Skript lösen das z.B. für Deutsch und Englisch auf Basis der oben erstellen Message-Dateien so aussehen könnte:

 "build": 
"npm run i18n 
&& ng build --prod --i18n-file src/i18n/messages.en.xlf --i18n-format xlf --i18n-locale en --output-path dist/en
--base-href . 

&& ng build --prod --i18n-file src/i18n/messages.de.xlf --i18n-format xlf --i18n-locale de --output-path
dist/de --base-href .", 

für jede Sprache haben wir nun im „dist“ Ordner eine lokalisierte Version unserer
Anwendung. Ein entsprechend konfigurierter Proxy oder eine simple Einstiegsseite mit ein wenig JavaScript könnte nun
automatisch auf Basis der Browser-Locale auf die korrekte Version umleiten.

mit JIT

Während der Entwicklung werden die meisten ( aus Gründen der Geschwindigkeit) auf den JIT Compiler setzen ( Standard bei ng serve). Zwar muss auch hier die Sprache vor Start der Anwendung feststehen ( und damit muss die Anwendung bei immer neu geladen werden wenn die Sprache geändert werden sollte) aber anders als bei AOT kann die Anwendung je nach Bedarf dynamisch initialisiert werden. Dazu konfigurieren wir die entsprechende Sprache und Message-Datei beim bootstrap unserer Anwendung in der main.ts, hier zum Beispiel über die Browser-Locale:

 
declare const require;
export function translationsFactory() {
  let locale = (window.clientInformation && window.clientInformation.language) || window.navigator.language;
  let language = locale.split('-')[0];

  return require(`raw-loader!./i18n/messages.${language}.xlf`);
}

platformBrowserDynamic()
  .bootstrapModule(AppModule, {
    providers: [
      { provide: TRANSLATIONS, useFactory: translationsFactory, deps: [] },
      { provide: TRANSLATIONS_FORMAT, useValue: 'xlf' }
    ]
  })
  .catch(err => console.error(err));

 

(Alltagstauglich mit i18n-polyfill)

Leider muss auch hier wieder ein „aber“ da gelassen werden. Eine typische Anforderung ist sicherlich auch Benachrichtigungen zu internationalisieren, also auch programmatisch auf dies Texte zugreifen zu können. Bisher hat es dieses Feature leider noch nicht direkt in den Standard geschafft, bis dahin existiert aber ein Polyfil der hoffentlich in ähnlicher Form auch in den Standard übergehen wird:
i18n-polyfill, https://github.com/ngx-translate/i18n-polyfill

Viele Ecken müssen bis hierher umschifft werden um die vermeidlich einfache Anforderung der Internationalisierung um zu setzen. Offiziell heißt es das dieser Prozess / das Vorgehen noch verbessert werden soll um die Verwendung von i18n zu vereinfachen,seien wir also gespannt.
Im zweiten Teil werfen wir trotzdem einen kurzen Blick auf eine Alternative Lösung die in Sachen Performance zwar der Core-Bibliothek nicht das Wasser reichen kann, aber ein etwas pragmatischeres Vorgehen wählt.

Live! In Farbe! auf github

Frohe Weihnachten, Angular Dynamic Components

demo

auf github

JSF 2.3 + Bean Validation

Bean Validation ist schon längst fester Bestandteil von JSF Projekten und sorgt dafür das Validierungs-Regeln unabhängig von JSF implementiert werden können. Eine Anforderung kommt dabei immer wieder auf: Feldübergreifende Validierung. Seit JSF 2.3 ist das nun möglich, hier ein kurzer Blick darauf und was mit „groups“ möglich ist

g4542
Fast schon ein alter Hut, die Deklaration von Pflichtfeldern und Mindestlängen für String-Eingaben:

public class DemoModel {

    @NotNull
    @Size(min = 4)
    private String firstname;

    @NotNull
    @Size(min = 4)
    private String lastname;

    private boolean reciveNewsletter;

JSF wird diese Regeln automatisch in der Validation-Phase heranziehen, diese prüfen und bei Fehlern FacesMessage an den Benutzer weiter geben. Diese Validierung findet ganz automatisch statt und stellt den Default-Fall war.

Ein spannendes Features von Bean Validation sind die so genannten „groups“. Hier können Regeln definiert werden die nur in bestimmten Fällen validiert werden sollen (z.B. auf Basis eines Status oder wenn zugehörige Daten erfasst wurden). Solche Gruppen sind im Kern nur ein Marker-Interface welches dem entsprechenden Attribut der Bean Validation Regel übergeben wird:

public interface NewsletterReciver {
    
}

public class DemoModel {
    ...

    @Min(value = 18, groups = NewsletterReciver.class)
    @NotNull(groups = NewsletterReciver.class)
    private Integer age;

= die Validierung dieser Regeln erfolgt nur bei aktivierter Gruppe „NewsletterReciver“. Gruppen können dann bei der programmatischen Prüfung mittels javax.validation.Validator angegeben werden oder aber auch in die JSF-View integriert werden:

            <h:panelGroup class="input">
                <h:outputLabel value="Newsletter empfangen:" for="reciveNewsletter" />
                <h:selectBooleanCheckbox id="reciveNewsletter" value="#{demoController.model.reciveNewsletter}">
                    <f:ajax event="change" render="age" />
                </h:selectBooleanCheckbox>
            </h:panelGroup>
            <h:panelGroup class="input">
                <h:outputLabel value="Alter:" for="age" /> 
                <h:inputText id="age" value="#{demoController.model.age}">
                    <f:validateBean validationGroups="de.gedoplan.blog.jsf.validation.groups.NewsletterReciver"  disabled="#{!demoController.model.reciveNewsletter}"/>
                </h:inputText>
            </h:panelGroup>

(in unserem Beispiel müssen alle Newsletter-Empfänger mindestens 18 Jahre alt sein)

In JSF 2.3 wurden die Möglichkeiten noch erweitert und ein neues Tag-Hinzugefügt welches es uns erlaubt eine Feld-übergreifende Validierung durch zu führen. Dazu sind folgende Schritte nötig.

1. Aktivierung WholeBeanValidation

    <context-param>
        <param-name>javax.faces.validator.ENABLE_VALIDATE_WHOLE_BEAN</param-name>
        <param-value>true</param-value>
    </context-param>

web.xml

2. Eigenen Bean Validation Validator implementieren

@Constraint(validatedBy = ValidAddressValidator.class)
@Target(value = {ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@interface ValidAddress {

    String message() default "Adresse unvollständig";

    Class[] groups() default {};

    Class[] payload() default {};
}

@ApplicationScoped
public class ValidAddressValidator implements ConstraintValidator {
    public boolean isValid(AddressModel value, ConstraintValidatorContext context) {
        Object[] attributes = new Object[]{value.getNumber(), value.getCity(), value.getStreet()};
        return Stream.of(attributes).allMatch(Objects::isNull) || Stream.of(attributes).noneMatch(Objects::isNull) ;
    }
}

(unser Beispiel validiert ein Adress-Objekt, es sollen entweder alle Felder gefüllt sein oder gar keins)

3. JSF View erweitern

<h:form>
    ...

    <f:validateWholeBean value="#{demoController.model.address}" />
</h:form>

Der entscheidenden Vorteil gegenüber einer „händischen“ Prüfung innerhalb der Submit Methode (s. DemoController@GitHub ) besteht darin das die fehlerhaften Werte bei der Verwendung dieser JSF-Variante noch nicht ins Model übertragen wurden. Stattdessen erstellt JSF eine Kopie des zu prüfenden Objektes und überträgt die Werte erst dann wenn die Prüfung erfolgreich war.

Github? Klar.

CDI Interceptoren, gesprächige Service-Schicht


Es gibt Momente da kann man nicht genug Informationen über den Ablauf seiner Anwendung haben, so zum Beispiel um Fehler-Situationen nach stellen zu können. Also muss ein ausführliches Logging her, am besten in der Form das wir möglichst wenig damit zu tun haben.

loggggs

Für diesen Fall bieten sich CDI Interceptoren an. Ein CDI Interceptor ist im Grunde eine einfache Methode die mittels Annotationen registriert werden kann und so Aufrufe von anderen Methoden umschließ. Mit diesem Vorgehen lässt sich sehr einfach ein entsprechendes Logging implementieren. Die Aktivierung eines solchen Loggings kann dann z.B. über eine entsprechende Logger-Kategorie geschehen (Vorteil: zur Laufzeit lässt sich diese Anpassen) oder (ohnehin Verpflichtend) über den Eintrag in der beans.xml realisiert werden. Auch wäre es denkbar solche Log-Informationen nur im Fehlerfall auszugeben. Was im Detail geloggt werden soll ist sicherlich immer Ansichtssache und unterscheidet sich je nach Art der Anwendung. Hier ein kurzes Beispiel wie so etwas aussehen könnte und die Ausgabe von Klasseninformationen( Klasse, Methode, Parameter), technischem HTTP-Context (IP Adresse, HTTP Headers) und zusätzlicher Debug Informationen über Annotations-Parameter

@Inherited
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Debug {

   @Nonbinding String message() default "";
}

Interceptor Binding

@Interceptor
@Debug
public class DebugInterceptor implements Serializable {

    @AroundInvoke
    public Object debug(InvocationContext ctx) throws Exception {
        String debugMessage = ctx.getMethod().getAnnotation(Debug.class).message();
        this.logRuntimeInformation(ctx, Level.FINE, debugMessage);
        return ctx.proceed();
    }

    private void logRuntimeInformation(InvocationContext ctx, Level level, String message) {
        String targetClass = ctx.getTarget().getClass().getSuperclass().getName();
        String targetMethod = ctx.getMethod().getName();
        String parameters = Stream.of(ctx.getParameters()).map(Object::toString).collect(Collectors.joining(", "));

        String remoteAddr;
        String headers;
        if (httpRequest != null) {
            remoteAddr = httpRequest.getRemoteAddr();
            headers = Collections.list(httpRequest.getHeaderNames()).stream().map((key) -> key + ":" + httpRequest.getHeader(key)).collect(Collectors.joining(", "));
        } else {
            remoteAddr = "unknown";
            headers = "undefined";
        }

        this.logger.log(level, String.format("Call of: %s#%s (%s) with parameters: [%s] from client %s (Headers: [%s])", targetClass, targetMethod, message, parameters, remoteAddr, headers));
    }

}

Implementierung

@ApplicationScoped
public class HelloWorldService implements Serializable{

    @Debug(message="Abruf formatiertes Datum")
    public String getFormattedDate(String format) {
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(format));
    }

    @Debug(message="Produziere einen Fehler")
    public String getSomeError(String msg) {
        return String.format("%s %d", msg, "World");
    }
}

Verwendung

 

Live? In Farbe? github !

Jackson, mehr Annotationen

In früheren Artikeln haben wir bereits einige Features von Jackson gesehen die uns das Erzeugen und Verarbeiten von JSON-Strukturen einfacher macht. So lassen sich unsere Businessobjekte mittels @JsonView gefilterte in eine JSON-Ansicht bringen oder Dank @JsonIdentityInfo und entsprechenden Resolver selbst JPA-Relationen sauber verarbeiten. Zwei weitere Möglichkeiten schauen wir uns heute an

updateValue

Wenn JPA-Relationen oder auch einzelne Felder nicht ins JSON Format übertragen werden sollen lässt sich das sehr einfach mittels @JsonIgnore oder @JsonView erledigen. Bei der Verwendung solcher Strukturen als Input für das Updaten unserer Entitäten führt das jedoch zu einem Problem: nicht vorhandene Attribute werden mit „null“ vorbelegt, was in aller Regel zu nicht gewollten Änderungen unserer Businessobjekte führt. Hier kommt, ähnlich wie bei der Verwendung von DTOs, in aller Regel ein Mapping-Framework ins Spiel welches die neuen Daten aus unserem JSON dazu verwendet das bestehende Objekt zu aktualisieren. Seit der Version 2.9 können wir dies auch getrost von Jackson erledigen lassen:

   @PUT
   @Path("{id}")
   public Response updateAuthor(@PathParam("id") Integer id, JsonNode authorJson){
        Author dbAuthor = this.authorRepository.getAuthorById(id);
        dbAuthor = objectMapper.updateValue(dbAuthor, authorJson);
        dbAuthor= this.authorRepository.merge(dbAuthor);
        return Response.ok().build();
   }

@JsonAppend

DTOs sind ein praktisches Vorgehen um zusätzliche Attribute oder gänzlich andere Strukturen als unsere Businessobjekte per REST zu liefern. Der Aufwand zum Schreiben und der Pflege dieser DTO-Klassen sollte allerdings nicht unterschätzt werden. Wollen wir lediglich einige einzelne zusätzliche Attribute zu den bestehenden Businessobjekt-Attribute an unser JSON anhängen hält Jackson auch hierfür eine Lösung parat:

//- Entity - ------------------------------------------
@Entity
@JsonAppend(
    attrs = {
        @JsonAppend.Attr(value = "bookcount")
    }
)
public class Author {...}

//- Resource - ------------------------------------------

    @GET
    @Path("{id}")
    public Response getAuthor(@PathParam("id") Integer id) {
        ...
            String response=objectMapper
                    .writerFor(Author.class)
                    .withView(GlobalViews.Overview.class)
                    .withAttribute("bookcount", authorById.getBooks().size())
                    .writeValueAsString(authorById);
            
            return Response.ok(response).build();
        }
    }

(im Default sind diese Append-Attribute optional, werden also nur gerendert wenn auch ein Wert zugewiesen wird. Dieses Verhalten kann durch das Setzen des Annotation-Attributes ‚include‘ geändert werden

Jackson und Java EE 8


Jackson ist eine Bibliothek die sich unter anderem darum kümmert das unsere Businessobjekte in JSON umgewandelt werden. Als default Provider für diese Aufgabe war Jackson bisher im Wildfly Application Server vorhanden. Seit Java EE 8 (und Wildfly 13) gibt es nun einen neuen Standard der sich um diese Aufgabe kümmert: JSON-B. Dieser ist leider in Sachen Funktionsumfang bei weitem noch nicht auf den Stand den Jackson erreicht hat. Um Jackson weiterhin zu nutzen sind (leider) einige Schritte von nöten

Laut Spezifikation von JAX-RS reicht es aus eine @Provider-Klasse an zu bieten welche für einen bestimmten Media Type die Konvertierung unserer Businessobjekte übernimmt. Diesen liefert Jackson mit einer entsprechenden Maven Dependency praktischerweise gleich mit (com.fasterxml.jackson.jaxrs, jackson-jaxrs-json-provider ). Schade nur das es bei dieser Theorie bleibt. Zumindest der Wildfly 13 (im EE8 Profil) und der Glassfish 5 weigern sich dieses einfache Vorgehen mit Erfolg zu krönen.

Wildfly 13

Ein Bug in RestEasy (RESTEASY-1911 ) verhindert hier das Jackson korrekt eingebunden wird. Ab der Version 3.6 soll dieser Umstand behoben sein, bis dahin hilft der Ausschluss des Resteasy Json Providers:

<jboss-deployment-structure xmlns="urn:jboss:deployment-structure:1.0">
    <jdeployment>
        <jexclusions>
            <jmodule name="org.jboss.resteasy.resteasy-json-binding-provider"><j/module>
        <j/exclusions>
    <j/deployment>
<j/jboss-deployment-structure>

src/main/webapp/WEB-INF/jboss-deployment-structure.xml

Glassfish 5

Die Referenzimplementierung für Java EE 8 ist hartnäckig was das Ersetzen von JSON-B als JSON Provider angeht. Hier ist ein zusätzlicher Konfigurationsparamter notwenig:

@ApplicationPath("resources")
public class ApplicationConfig extends Application {

    @Override
    public Map getProperties() {
        Map proprties = new HashMap();
        proprties.put("jersey.config.server.disableMoxyJson", true); //Glassfish = 5

        return proprties;
    }

}

Optional: globaler ObjectMapper

Jacksons ObjectMapper bietet eine ganze Reihe von globalen Einstellungen um das Verhalten beim Parsen von JSON-Strukturen zu beeinflussen (Format, Umgang mit null-Werten und unbekannter Attribute…). Um in der gesamten Anwendung eine einheitliche Konfiguration zu verwenden bietet sich ein entsprechner CDI Producer an:

@ApplicationScoped
public class GlobalCDIProducer {

    private ObjectMapper mapper;
    
    @Produces
    public ObjectMapper getMapper(){
        if(this.mapper==null){
            this.mapper=new ObjectMapper();
            DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm");
            this.mapper.setDateFormat(df);
        }
        return this.mapper;
    }
}

Diesen können wir natürlich selber in unseren Resouce-Klassen injizieren und Verwenden, aber auch JAX-RS zur Verfügung stellen:

@Provider
public class JacksonProvider implements ContextResolver<ObjectMapper> {

    @Inject
    private ObjectMapper mapper;
    
    @Override
    public ObjectMapper getContext(Class<?> type) {
        return getMapper();
    }
}