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 !

Schlanke JEE-Services mit Apache Meecrowave

Das Programmmodell von JEE (aka Java EE / Jakarta EE) ermöglicht schon seit vielen Jahren – namentlich seit der Version 5 – eine effiziente Entwicklung von schlanken Geschäftsanwendungen. Als Ablaufumgebungen stehen ausgereifte und leistungsfähige JEE-Server wie WildFly oder Open Liberty zur Verfügung, die als eine Art Allrounder den Anwendungen recht umfassende Infrastruktur anbieten und sogar mehrere Anwendungen gleichzeitig betreiben können.

Gerade der letzte Punkt, der Parallelbetrieb mehrere Anwendungen, wird allerdings nur selten genutzt, um Störeffekte zwischen Anwendungen zu minimieren. Es gibt also typischerweise genau eine Anwendung pro Server, der ggf. sogar selbst in einem dedizierten Rechner – meist einer virtuelle Maschine – läuft.

Wenn man nun noch einen nur eingeschränkten Teil der angebotenen Infrastruktur benötigt, bspw. JPA, CDI und JAX-RS, also den klassischen Microservice-Stack, kann man sich die Frage stellen, ob tatsächlich ein kompletter JEE-Server gebraucht wird, oder ob man nicht einfach die benötigten Dienste in die Anwendung einbetten könnte.

An dieser Stelle setzt Apache Meecrowave an. Dieses Apache-Projekt verknüpft den JPA-Provider EclipseLink, den CDI-Container OpenWebBeans und die REST-Implementierung CXF zu einem ganz schlanken Server – oder eigentlich zu einer Server-Bibliothek, mit deren Hilfe man den Server einfach als Main-Programm startet.

Zur Einbindung von Meecrowave reicht es aus, die folgende Dependency in den Classpath zu holen:

  
    org.apache.meecrowave
    meecrowave-core
    1.2.3
  

Der in der Anwendung eingebettete Server wird dann z. B. so gestartet:

  public static void main(String[] args) {
    try (Meecrowave meecrowave = new Meecrowave().bake()) {
      // Do work here ...
    }

Beim Eintritt in den try-Block wird der Server gestartet, beim Verlassen wieder gestoppt. Sämtliche aus CDI bekannte Mechanismen für den Bootstrap einer Anwendung können zur Aktivierung von Services etc. genutzt werden, z. B. Observer für den Container Lifecycle:

  void startSomething(@Observes @Initialized(ApplicationScoped.class) Object obj) {
    // Gets called on server start ...
  }

Ein einfacher REST-Service ist dann nur noch ein Ding von wenigen Zeilen:

@Path("hello")
@ApplicationScoped
public class HelloResource {

  @GET
  @Produces(MediaType.TEXT_PLAIN)
  public String getHello() {
    return "Hello!";
  }
}

So ist also dieser – zugegebenermaßen winzige – Microservice mit nur ganz wenig Code schon fertig. Und beeindruckt mit Startzeiten im Sekundenbruchteil-Bereich sowie einem Memory Footprint von nur ca. 12 MB.

Ein einfaches Demoprojekt kann von GitHub heruntergeladen werden: https://github.com/GEDOPLAN/meecrowave-demo.

Bis bald – vielleicht in einem unserer Trainings in Berlin, Bielefeld, Köln oder bei Ihnen!
http://gedoplan-it-training.de/

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