Hinweise zur Migration auf jadice web toolkit 6.0

Wichtiger Hinweis

Diese Anleitung gilt für jadice web toolkit Version 6.0.0 von 07/2024

Die Hinweise gelten für die Migration von jadice web toolkit 5.12 & 5.13 auf 6.0.

Eine Übersicht der Versionen des jadice web toolkit finden Sie hier.

Inhalt

Generationswechsel - Vorbereitung für ein modernes Frontend

Das jadice web toolkit erlebt mit Version 6 den ersten Generationswechsel in seiner Geschichte (die initiale Version war bereits Generation 5, da die Versionsnummer an den Swing Viewer / document platform angeglichen wurde). Die neue Generation glänzt nicht durch eine große Sammlung an neuen Features, sondern stellt die Weichen, um ein modernes Frontend schrittweise zu ermöglichen, ohne dabei vorhandene Integrationen unbrauchbar zu machen, oder etablierte Technologien und Konzepte über den Haufen zu werfen.

Wir sind stolz darauf, mit diesem Release die erste Version unseres modernen, auf Web Components basierten und für Angular optimierten Frontends für alle Kunden zur Verfügung zu stellen.

Der Umstieg von GWT auf Angular ist nicht als Update oder Upgrade möglich, sondern erfordert eine Neu-Implementierung mit modernen Tools und Frameworks. Elegant ist dabei jedoch, dass das Backend nicht angepasst werden muss - man kann ein und dasselbe Backend für einen GWT- und einen Angular-Viewer verwenden. Natürlich kann auch mit jadice web toolkit Version 6 ein bestehendes GWT Frontend weiter genutzt werden. Die Entwicklung von GWT wird jedoch nicht mehr aktiv fortgeführt, sodass neue Integrationen bestenfalls direkt mit dem Typescript Frontend umgesetzt werden.

Unser Viewer basiert auf Web Components und Typescript. Für Angular existieren entsprechende Komponenten, eine Integration in andere Frontend-Frameworks wie React ist aber ebenfalls möglich.


Neue Funktionen und Features

Kein GWT im Backend

Grundsätzlich gilt, dass Sie natürlich weiterhin auf GWT-RPC und weitere Funktionalitäten von GWT setzen können. Das jadice web toolkit tut dies nun nicht mehr, daher ist es nun möglich, auf weite Teile von GWT zu verzichten. Dies erleichtert den späteren Umstieg auf ein Nicht-GWT-Frontend bzw. macht diesen überhaupt erst möglich.

Tile-Requests

Tile-Requests hatten in der Vergangenheit vier Query-Parameter, wobei sich der interessante Teil im Parameter r verbarg. In diesem Parameter wurden die Informationen die für das Rendering einer Seite benötigt werden per GWT-RPC serialisiert. Wenn Sie Ihre Integration mit Last-Tests getestet haben, sind Sie bestimmt schon darüber gestolpert. Diese URLs sind nur sehr schwer les- und anpassbar (z.B. für Last-Tests). Zukünftig werden auch weitere Informationen per Query-Parameter übertragen. Der Teil der PageSegmentHandles ist nun als JSON kodiert.

Die URL von Tile-Requests (gerenderte Kacheln) sah bisher so aus:

https://webtoolkit.jadice.com/enterprise/jwt/tile?c=a024a898-762a-268f-762a-268f762a268f&v=a024a899-02dd-3354-02dd-335402dd3354&r=7*0*13**674D608AF30ABADAE374AEBB2241DD75*com.levigo.jadice.web.shared.model.internal.FlattenedRenderSpecificationData%2F2762891342*com.levigo.jadice.web.shared.model.settings.AnnotationRenderSettings%2F3358781670*Triangle*%5BI%2F2970817851*%5BLcom.levigo.jadice.web.shared.model.PageSegmentHandle%3B%2F270004123*com.levigo.jadice.web.demo.common.shared.service.sources.SplitFileUploadHandle%2F2259686330*62258f96-07d5-48b4-8904-3ab560099ec0*document*d5729ddb-3c1f-4998-8b97-1825a39806d7*%5BLjava.lang.String%3B%2F2600011424*ROT_000*1*2*3*4*0*0*96*5*6*4*0*0*255*255*7*1*8*9*0*10*0*11*12*0*1122*793*0*0*13*1*

Zukünftig sehen die Requests folgendermaßen aus:

https://webtoolkit.jadice.com/enterprise/jwt/tile?c=a024a898-762a-268f-762a-268f762a268f&v=a0248df6-9f13-a782-9f13-a7829f13a782&deviceResolution=96&zoomFactor=1&defaultLayerStateEnabled=false&rotation=ROT_000&filter=Triangle&roix=0&roiy=0&roiwidth=793&roiheight=1122&layerNames=%5B%5D&gradationCurvePoints=%5B0%2C0%2C255%2C255%5D&handles=%5B%7B%22type%22%3A%22URI%22%2C%22params%22%3A%7B%22uri%22%3A%22upload%3A%2F%2F11785933124963992093%22%2C%22password%22%3Anull%7D%2C%22documentLayer%22%3A%22document%22%2C%22pageIndex%22%3A0%2C%22pageSegmentUUID%22%3A%22f7e36745-2734-423a-b648-f250ca2bb94a%22%7D%5D

Die Handles sehen vor der serialisierung so aus:

[
   {
      "type":"URI",
      "params":{
         "uri":"upload://11785933124963992093",
         "password":null
      },
      "documentLayer":"document",
      "pageIndex":0,
      "pageSegmentUUID":"f7e36745-2734-423a-b648-f250ca2bb94a"
   }
]

Messaging statt RPC

Anstatt eines Remote-Service-Aufrufs versenden Client und Server nun Messages, die interpretiert werden. Diese sind als JSON formatiert und werden nicht mehr per GWT-RPC automatisch serialisiert. Die Messages werden über das Connection-Framework versendet. Dies hat jedoch zur Folge, dass manuelle Anpassungen am Code erforderlich sind. Diese Schritte werden weiter unten erläutert. Das deutlich einfachere Messaging ist im JSON-Format besser interpretier- und debugbar.


Anbindung an jadice flow (optional)

Über die optionale jadice flow Anbindung ist es möglich weitere, bisher nicht unterstützte Formate im jadice web toolkit anzeigen zu können. Dazu gehören u.a. HEIC/HEIF oder MS Office (docx, xlsx, eml, ...). Wenn Sie Interesse an diesem Feature haben, nehmen Sie gerne Kontakt mit uns auf.

Eine Liste der so anzeigbaren Formate ist hier zu finden: Unterstütze Formate für die Anzeige im jadice web toolkit - jadice knowledge base - levigo info center


TypeScript Viewer

Das Backend ermöglicht es parallel oder alternativ zum GWT Frontend das TypeScript Frontend zu verwenden. Hierfür existieren auch Bindings und Komponenten, für die Verwendung in Angular. Eine Integration in React oder Vue.js ist ebenfalls möglich.

Das Angular Frontend unterstützt noch nicht alle Funktionalitäten des GWT Frontends. Hier finden Sie eine Liste der Known Issues & Limitations TypeScript Viewer

Gegenüber GWT bietet der TypeScript Viewer einige erhebliche Vorteile, einige davon haben wir hier aufgeführt:

  • Schnellere Entwicklung durch besseres Tooling
  • Bessere Integration in Entwicklungsumgebungen, kein spezielles GWT-Plugin erforderlich
  • Deutlich reduzierte Build-Zeit und Bundle-Größe
  • Simplere und moderne API und Features
  • Zukunftssicherheit durch reines TypeScript im Kern
  • Accessibility von Beginn an

Wenn Sie Interesse an dem neuen Viewer haben, nehmen Sie gerne Kontakt mit uns auf. Ein Tutorial um einen solchen Viewer aufzubauen finden Sie hier.



Einfacher Lesemodus (optional, nur TypeScript Viewer)

Der einfache Lesemodus ist ein neuer Bestandteil des Produkts. Dabei wird der Inhalt des Dokuments als HTML aufbereitet. Insbesondere für Screenreader ist das sehr nützlich da so der Inhalt des Dokuments für beispielsweise blinde Anwender zugänglich ist.

Die Anzeige als HTML funktioniert vor allem mit PDF/UA-Dokumenten, für weitere Dokumenten-Typen bauen wir diese Funktion weiter aus.



URI-Sources

Der bisherige Weg, um ein Dokument zu Anzeige zu bringen, war die Implementierung eines DocumentDataProviders in Kombination mit einer Source und einem PageSegmentHandle. Dieser Weg ist hier beschrieben und wird für den GWT-Viewer nach wie vor unterstützt, im TypeScript-Viewer sind URIs aktuell der einzige Weg um Dokumente laden zu können.

In der Version 6 des jadice web toolkit gibt es nun eine vereinfachte Möglichkeit. Dazu muss vom Integrator lediglich ein URI-Schema definiert und ein entsprechender DocumentDataProvider implementiert werden.

Um dies an einem Beispiel zu veranschaulichen: Angenommen die Dokumente liegen in einem P8-Archiv. Folgende Schritte wären zu erledigen:

URI-DataProvider
@Component
public class P8SchemeDocumentDataProvider implements UriBasedDocumentDataProvider {

    private final String[] schemes = new String[] { "p8" };

    @Override
    public void read(Reader reader, UriSource source) throws JadiceException, IOException {
        String uri = source.getUri();
        String p8Id = uri.substring("p8://".length());
        // Do the magic to retrieve the document from P8 here
    }

    @Override
    public void recover(Reader reader, UriHandle handle) throws RecoverFailedException, JadiceException {
        // Do the magic to retrieve the document from P8 here
    }

    @Override
    public List<String> getSchemes() {
        return Arrays.asList(schemes);
    }
}

Laden des Dokuments (GWT-Frontend)
    final Reader r = new Reader();
    r.read(new UriSource("p8://LFHA89DH", null /** no password needed for PDF document */), new AsyncCallback<Document>() {
      @Override
      public void onSuccess(Document doc) {
        viewer.getPageView().setDocument(doc);
      }

      @Override
      public void onFailure(Throwable caught) {
        LOGGER.error("loading document failed", caught);
        Window.alert("Can't find the requested document.");
      }
    });


Laden des Dokuments (TypeScript-Frontend)
<jwt-multi-mode-viewer [source]="{uri: 'p8://LFHA89DH'}"/>

Eine eigene Source- und Handle-Implementierung wird hier nun nicht mehr benötigt.

HTTP-URIs

Standardmäßig wird ein DataProvider mitgeliefert, der Dokumente von einem HTTP Endpunkt anzeigen kann.

<jwt-multi-mode-viewer [source]="{uri: '<a href="http://www.digbib.org/Franz_Kafka_1883/Der_Prozess_.pdf" }"/>

Dies ist sehr komfortabel in der Entwicklung, kann aber dazu führen, dass ein manipulierter Client eine Anfrage an das Backend schickt und dieses daraufhin einen HTTP-Aufruf absetzt.

Um die Einstellung zu ändern:
In der application.yml folgendes einstellen:
webtoolkit:
  uri-provider-http-enabled: false



Java 21 Support

Das Backend ist nun vollständig mit Java 21 kompatibel. Für GWT-Client-Code ist das maximal unterstütze Language Level nach wie vor Java 11 (bei GWT 2.10).

Die offiziellen Release Notes von Oracle können hier eingesehen werden.



Neue Defaults

PNG statt WebP

In Version 5.10 des jadice web toolkit wurde eingeführt, dass Tiles standardmäßig als WebP-Images und nicht mehr als PNG an den Client geliefert werden. Dies bringt Vorteile im Bezug auf Geschwindigkeit und Dateigröße. Jedoch hat sich herausgestellt, dass dies im Containerumfeld weniger gut geeignet ist. Die Erzeugung von WebP-Bildern erfordert Off-Heap-Speicher, was im Monitoring nicht gut überwachbar und steuerbar ist. Die Erzeugung von PNG ermöglicht es jadice auch besser mit dem Cache umzugehen, zudem kann so die Memory-Lücke zwischen Container- und JVM-Speicherlimit deutlich kleiner ausfallen, ohne dass es zu OOM-Killed-Fehlern seitens des Containers kommt.

Der neue Standard ist nun "IMAGEIO".

Weitere Informationen zur Wahl des Tile-Compression-Types sind hier zu finden.

Um die Einstellung zu ändern:
In der application.yml folgendes einstellen:
webtoolkit:
  tile-compression-type: WEBP_LOSSY_HIGH_QUALITY
alternativ programmatisch
ConfigurationManager.getServerConfiguration().setTileCompressionType(ServerConfiguration.TileCompressionType.WEBP_LOSSY_HIGH_QUALITY);

Lenient-Lesemodus

Der neue Standard für das Lesen von PDFs ist nicht mehr "Strict" sondern "Lenient on Error". Dadurch sind in der Regel mehr Dokumente (fehlerhafte Dokumente) anzeigbar als vorher.

Es werden Heuristiken angewandt, die Fehler abfangen. Da nicht alle möglichen Fehler vorhersehbar sind, und die Reaktion auf solche Fehler nicht definiert ist, kann nicht bestimmt werden, ob die Anzeige korrekt ist.

Es kann gegebenenfalls Strukturprobleme geben, die von der Heuristik nicht korrekt behandelt werden.

Zu Risiken und Nebenwirkungen siehe PDF-Struktur Lesestrategien.

Um die Einstellung zu ändern:
In der application.yml folgendes einstellen:
webtoolkit:
  pdf-structure-read-strategy: STRICT
alternativ programmatisch
ConfigurationManager.getServerConfiguration().setPdfStructureReadStrategy(PDFStructureReaderSettings.PDFStructureReadStrategy.STRICT);

Page-Preloading (GWT-Frontend)

Es werden standardmäßig 2 Seiten vor-geladen.

Um die Einstellung zu ändern:
pageView.setPreloadingPageRange(new PreloadingPageRange(2));

Server Sent Events (SSE) standardmäßig deaktiviert (GWT-Frontend)

Im Standard verbindet sich der Client über Websocket mit dem Backend. Schlägt dies fehl, wird nun direkt zu Longpoll übergegangen. Wenn SSE verwendet werden soll, muss dies nun manuell aktiviert werden.

Siehe auch unten.

Um die Einstellung zu ändern:
ServerConnectionBuilder connectionBuilder = new ServerConnectionBuilder().setServerSentEventsEnabled(true);

Cache-Settings

Der standardmäßig verwendete Composite-Key-Cache hat nun die folgenden Default-Einstellungen:

CompositeKeyCache alte Werteneue Werte
jadice.viewer.cache.maxNumberOfCacheEntries3000

90000
jadice.viewer.cache.minimumExpiryAge

-1

60000 // lifetime for cache entries. (in milliseconds)
jadice.viewer.cache.sizeHighwaterMarkPercent

-1

25 // entspricht 2 GB bei 8 GB Heap

Diese Werte basieren auf unseren Erfahrungen, weiter Informationen können hier nachgelesen werden.

Font-Konfiguration

In Version 6 des jadice web toolkit wurde die Font-Konfiguration dahingehend angepasst, dass keine Fonts mehr geladen werden, die auf dem Betriebssystem installiert sind. Dies reduziert die Startzeit und oft auch die Anzahl von Warnmeldungen beim Server-Start aufgrund von Problemen beim Einlesen von System-Fonts. Die wahrscheinlich größte Zahl von PDF Dokumenten referenziert entweder Standard-14-Fonts oder hat die Fonts, die zur Anzeige benötigt werden, im Dokument eingebettet. Zudem sind bei einigen Linux-Distributionen (v.a. Distroless) ohnehin keine Fonts vorhanden oder die am ehesten gewünschten (Arial, ...) aus Lizenztechnischen Gründen ebenfalls nicht vorhanden.

Um die Einstellung zu ändern:
Details siehe hier.


Neues Versionsnummern-Schema

Bisher hatte das jadice web toolkit ein vier-stelliges Versionsnummer-Schema. Dieses hat Semantic Versioning um eine führende Stelle, welche der jadice Produktgeneration entsprach, erweitert. Ab Version 6 des jadice web toolkit entfällt die Produktgeneration, sodass ein drei-stelliges Semantic Versioning verwendet wird.

Früher: 5.12.1.2
Heute: 6.1.2


Step-by-Step Migration (Backend & GWT-Frontend)

Anpassung von geänderten Klassen

  • Globales Ersetzen von "import com.google.gwt.user.client.rpc.AsyncCallback;" durch "import com.levigo.jadice.web.client.reader.AsyncCallback;"
  • Die Exception com.google.gwt.user.client.rpc.SerializationException wird im Client-Code nicht mehr geworfen, entsprechende Catch-Blöcke können entfallen

RemoteLogging

Es gab bisher über das GWT-seitige RemoteLoggingServlet die Möglichkeit Log-Meldungen die beim Client (im Browser) auftraten, ins Server-Log schreiben zu lassen. Als GWT-freie Alternative bieten wir hierzu nun den folgenden Client-Aufruf:

com.levigo.jadice.web.client.util.Logutils.setDefaultRemoteHandler();

Umstellung des Connection-Frameworks auf JSON-Messaging

Genereller Ablauf

Das Messaging folgt dem folgenden Ablauf:

  1. Ein Client sendet einen Request an den Server (über das Connection-Framework, also Websocket)
  2. Der Server bearbeitet die Anfrage und sendet eine beliebige Anzahl an Antworten an den Client
  3. Der Server beendet die Konversation durch eine EOC-Nachricht
    1. alternativ kann der Client durch eine Control-Message die Konversation ebenfalls beenden

Eine Message besteht dabei aus den folgenden Bestandteilen:

  • MessageName
    • ein beliebiger, eindeutiger String
  • Conversation-ID 
    • eine fortlaufende Nummer, die sicherstellt, dass Nachrichten in der richtigen Reihenfolge verarbeitet oder gegebenenfalls nochmals übertragen werden
  • Payload
    • beliebiger JSON-Inhalt

Umstellung

Durch den Entfall von GWT-RPC und der Umstellung auf JSON sind nun (sofern keine URI-Sources verwendet werden) manuelle Anpassungen erforderlich.

In der Vergangenheit war es oft gängige Praxis folgenden Aufbau für einzelne Module zu verwenden:

  • Ordner "server"
    • Backend-Code
  • Ordner "client"
    • GWT-Frontend-Code
  • Ordner "shared"
    • Code, der von Client und Server verwendet wird, oftmals Sources, Handles, Server-Operation-Parameters und String-Konstanten

Diese Aufteilung ist zwar komfortabel, jedoch war seitens GWT schon seit jeher eine klare Trennung von Client- und Server-Code in Form unterschiedlicher Maven-Module (und nicht nur in unterschiedlichen packages) angeraten. Diese Empfehlung möchten wir an dieser Stelle auch klar aussprechen.

Die oben aufgeführte Aufteilung funktioniert auch weiterhin, im Zuge der Umstellung kommen nun jedoch einige DTO-Klassen in server und client hinzu, da Client-Code nun spezielle Client-DTOs benötigt und das Backend spezielle Backend-DTOs. Der Grund für das manuelle Erstellen von DTO-Objekten ist, dass auf dem Client kein Reflection zur Verfügung steht und TypeScript nicht mit (von GWT) serialisierten Java-Klassen umgehen kann. Speziell abstrakte Klassen sind ohne Reflection nicht einfach serialisierbar. Um die zu übertragenden Java-Klassen nach JSON (und zurück) zu serialisieren, werden DTO-Klassen benötigt, die nur die relevanten Informationen enthalten.

Info

Als Integrator müssen Sie sich um die Serialisierung/Deserialisierung der Objekte kümmern, die Sie integrationsseitig zwischen Server und Client übertragen. Wenn Sie auf URI-Sources umstellen, brauchen Sie gar nichts tun. Bei anderen DataProvidern müssen Sie mindestens für die Source- und Handle-Klassen DTOs anlegen. Wenn diese komplexe Objekte als Felder enthalten (was ausdrücklich nicht empfohlen wird), müssen für diese ebenfalls DTOs angelegt werden. 

Für alle Objekte, die für ServerOperations verwendet werden, müssen ebenfalls DTOs angelegt werden (siehe nächster Abschnitt).

Bei dieser "Fleißarbeit" im Client-Code können diese IntelliJ-Code-Templates hilfreich sein: levigo/useful-intellij-live-templates: Collection of importable live templates for IntelliJ IDEA (github.com)

Server-seitig empfiehlt sich der Einsatz von Lombok.

Mapper

Wenn nur einfache Datenstrukturen bei ServerOperations übertragen werden, ist als Integrator Ihr einziger Berührungspunkt mit den Mappern der Aufruf der entsprechenden Mapper bei der Umstellung von Source und Handle (nächster Abschnitt). Bei komplexeren Datenstrukturen, muss ggf. selbst Einfluss auf das Mapping genommen werden.

Für die Serialisierung/Deserialisierung wird jackson verwendet. Dieses nutzt Mapper, um Objekte zu JSON zu konvertieren. Im jadice web toolkit gibt das darauf basierende Mapper, fachlich gegliedert nach dem jeweiligen Einsatzgebiet. Dabei kann zwischen den folgenden Mapper-Arten unterschieden werden:

SimpleMapper

Bei einem SimpleMapper, werden Java-Klassen auf die entsprechenden DTOs gemappt.

PolymorphicMapper

In der Java-Welt gibt es bekanntlich ein Vererbungs-Modell, welches zum Teil sehr komplexe Datenstrukturen erlaubt. Diese Komplexität ist in der JavaScript-Welt eher unüblich und in einer JSON-Struktur weder sauber abbildbar noch erforderlich. Um dennoch vorhandene Strukturen einfach serialisieren zu können, kann ein PolymorphicMapper verwendet werden.

Erstellen von DTOs

From/To Pattern

Die DTO-Objekte werden lediglich zur Serialisierung/Deserialisierung benötigt. Im Code kann wie gewohnt mit den "original" Objekten gearbeitet werden. Wir empfehlen, die DTO-Klassen mit speziellen Methoden zu versehen, die dafür zuständig sind, ein Objekt in und von einem DTO zu konvertieren. Diese Methoden werden von den Mappern verwendet. Anbei folgendes Beispiel für ein DTO für die Klasse java.awt.Color (→ Beispiel-Use-case: Ein Color-Objekt wird als Teil einer ServerOperation vom Client an den Server übertragen)

Client-Seitiges DTO 

Diese DTOs werden nur im Client-Code verwendet. Die Klassen sind für die Verwendung im GWT-Code vorgesehen und haben daher eine Abhängigkeit auf gwt-user. Client-DTOs erweitern JavaScriptObject, eine GWT-JSNI-Klasse.

ColorDTO
import java.awt.Color;

import com.google.gwt.core.client.JavaScriptObject;

@SuppressWarnings("ProtectedMemberInFinalClass")
public final class ColorDTO extends JavaScriptObject {
    protected ColorDTO() {
    }

    public native int getRed()/*-{
        return this.r;
    }-*/;

    public native int getGreen()/*-{
        return this.g;
    }-*/;

    public native int getBlue()/*-{
        return this.b;
    }-*/;

    public native int getAlpha()/*-{
        return this.a;
    }-*/;

    public static native ColorDTO create(int r, int g, int b, int a)/*-{
        return {r: r, g: g, b: b, a: a};
    }-*/;

    public static ColorDTO from(Color color) {
        if (color == null) {
            return null;
        }

        return create(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha());
    }

    public static Color to(ColorDTO dto) {
        if (dto == null) {
            return null;
        }

        return new Color(dto.getRed(), dto.getGreen(), dto.getBlue(), dto.getAlpha());
    }
}

Server-Seitiges DTO 

ColorDTO
import java.awt.Color;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public final class ColorDTO {
    private int r;
    private int g;
    private int b;
    private int a;

    public static ColorDTO from(Color color) {
        if (color == null) {
            return null;
        }

        return new ColorDTO(
                color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha()
        );
    }

    public static Color to(ColorDTO dto) {
        if (dto == null) {
            return null;
        }

        return new Color(dto.getR(), dto.getG(), dto.getB(), dto.getA());
    }
}

Hinweis zu Datentypen

In Java gibt es verschiedene Datentypen, die in JavaScript so nicht existieren. So gibt es in Java beispielsweise Int, Float, Double, Long u.s.w. während es in JavaScript nur number gibt. Es empfiehlt sich in den DTO-Klassen mit primitiven Doubles und Integern zu Arbeiten, sowohl Server- als auch Client-seitig.

Achtung

Die Client- und Server-seitigen DTOs müssen hinsichtlich der Felder identisch sein.

Mapping registrieren

Damit das jadice web toolkit die DTOs serialisieren und übertragen kann, müssen die Mappings sowohl server- als auch clientseitig bekannt gemacht werden.

Client-seitig
com.levigo.jadice.web.client.messaging.mapper.SimplePolymorphicMapper.registerMapping(
                "COLOR", Color.class,
                (color) -> ColorDTO.from((Color) color),
                (json) -> ColorDTO.to((ColorDTO) json)
        );
Server-seitig
com.levigo.jadice.web.server.messaging.mapper.SimplePolymorphicMapper.registerMapping(
                "COLOR", Color.class,
                (color) -> json(ColorDTO.from((Color) color)),
                (json) -> ColorDTO.to(dto(json, ColorDTO.class))
        );

Im Produkt vorgenommene interne Umstellungen

Das Konzept der DTOs wurde auch intern für die Umstellung von GWT-RPC auf JSON-Messaging verwendet. Die entsprechenden Klassen befinden sich serverseitig hauptsächlich in com/levigo/jadice/web/server/messaging und clientseitig in com.levigo.jadice.web.client.messaging .

Diese ersetzen vor allem die zuvor von GWT verwendeten Custom Field Serializer.

Für das Versenden der Nachrichten wurde in com.levigo.jadice.web.transport.shared.messaging.DefaultMessageNames eine Liste von Konstanten definiert, um die vom Produkt genutzten Nachrichten zu unterscheiden.

com.levigo.jadice.web.shared.model.document.snapshot.DocumentSnapshot  ist im Wesentlichen der Ersatz für das alte com.levigo.jadice.web.shared.model.serveroperation.internal.SynchronizeObject.


Umstellung von Sources und Handles

Grundsätzlich ist das Vorgehen für Source und Handle dasselbe wie bei anderen Objekten (siehe vorheriger Abschnitt). Für die Sources und Handles werden ebenfalls je eine DTO-Klasse für den Client und eine für den Server benötigt.

Mapping registrieren (für Source und Handle)

Damit das jadice web toolkit die DTOs serialisieren und übertragen kann, müssen die Mappings sowohl Server- als auch Clientseitig bekannt gemacht werden.

Server-seitige Mappings
@Component
public class Mappings {
    @PostConstruct
    public void init() {
        SourceMapper.get().registerDirectMapping(ClassPathWithAnnoSource.TYPE, ClassPathWithAnnoSource.class);
        PageSegmentHandleMapper.get().registerMapping(
                ClassPathWithAnnoHandle.TYPE,
                ClassPathWithAnnoHandle.class,
                (handle) -> {
                    final ClassPathWithAnnoParamsDTO dto = ClassPathWithAnnoParamsDTO.from((ClassPathWithAnnoHandle) handle);
                    return new ObjectMapper().valueToTree(dto);
                },
                (json) -> {
                    final ClassPathWithAnnoParamsDTO dto = new ObjectMapper().treeToValue(json, ClassPathWithAnnoParamsDTO.class);
                    return ClassPathWithAnnoParamsDTO.toHandle(dto);
                }
        );
    }
}
Client-seitige Mappings
private void registerMappings() {
  SourceMapper.get().registerMapping(ClassPathWithAnnoSource.TYPE, ClassPathWithAnnoSource.class,
      (source) -> ClassPathWithAnnoParamsDTO.from((ClassPathWithAnnoSource) source),
      (json) -> ClassPathWithAnnoParamsDTO.toSource((ClassPathWithAnnoParamsDTO) json));
 
  PageSegmentHandleMapper.get().registerMapping(CompositeHandle.TYPE, CompositeHandle.class,
      (handle) -> CompositeParamsDTO.from((CompositeHandle) handle),
      (dto) -> CompositeParamsDTO.toHandle((CompositeParamsDTO) dto));
}

GWT ablösen

GWT.create ablösen (optional)

Grundsätzlich gilt, dass Sie natürlich weiterhin auf GWT-RPC setzen können. Um jedoch ein Backend ohne GWT zu bekommen, sollten Sie alle GWT.create-Aufrufe entfernen und stattdessen Messages über unser Connection-Framework versenden. Dies ist weiter unten beschrieben.

Dependencies entfernen

Im Backend können sämtliche GWT-Dependencies entfernt werden

EnableGWTSpringBootApplication

Die Annotation @EnableGWTSpringBootApplication kann im Server-Teil entfernt werden. Die Annotation @EnableJWTSpringBootApplication ersetzt diese.

Umstellung von Server-Operations

Das Server-Operation Konzept ermöglichte es bisher Integratoren, dokumentbezogene Aktionen asynchron serverseitig auszuführen. Anwendungsfälle sind beispielsweise das Speichern von clientseitigen Änderungen an Annotationen.

Dieses Konzept wurde jetzt auf das Messaging-Verfahren des Connection-Frameworks umgestellt. Die Idee ist, dass ein Client generisch eine Nachricht an den Server verschicken kann, unabhängig davon, ob das Dokument mit übertragen werden soll oder nicht. Der Server horcht auf Nachrichten und reagiert entsprechend (asynchron). Die Server-seitige Logik (im MessageListener) sollte unbedingt asynchron (über ein Future) ausgeführt werden und abbrechbar sein (→ einen CancelHandler bereitstellen). Stellen Sie bitte unbedingt (z.B. über einen finally-Block) sicher, dass die Konversation sauber beendet wird, z.B. durch ein sendSuccess oder ein sendFail.

Um eine Klasse zu einem MessageListener zu machen, wird die Klasse mit @MessageName annotiert. Um einen MessageListener zu registrieren wird dieser mit @Component annotiert. 

Client-seitig führt ein Command nun keinen invoke-Aufruf mit einem Observer mehr aus sondern es wird ein send-Aufruf mit einem MessageHandler ausgeführt.

Server-Seitig (alt)

SaveAnnotationsServerOperation
/**
 * <pre>
 * Copyright (c), levigo holding gmbh.
 *
 * This file is subject to the terms and conditions defined in
 * file 'LICENSE.txt', which is part of this source code package.
 * </pre>
 */
package com.levigo.jadice.web.demo.enterprise.server;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.jadice.util.log.Logger;
import org.jadice.util.log.LoggerFactory;
import org.springframework.stereotype.Component;

import com.levigo.jadice.document.Document;
import com.levigo.jadice.document.write.DefaultWriterControls;
import com.levigo.jadice.document.write.FormatWriter;
import com.levigo.jadice.format.annotation.JadiceAnnotationWriter;
import com.levigo.jadice.web.demo.enterprise.shared.serveroperation.SaveAnnotationParameters;
import com.levigo.jadice.web.demo.enterprise.shared.serveroperation.SaveCompletedMessage;
import com.levigo.jadice.web.demo.enterprise.shared.serveroperation.SaveStartedMessage;
import com.levigo.jadice.web.server.ServerOperation;
import com.levigo.jadice.web.shared.model.serveroperation.ServerOperationMessage;

@Component
public class SaveAnnotationServerOperation
    implements
        ServerOperation<SaveAnnotationParameters, ServerOperationMessage> {

  private static final Logger LOGGER = LoggerFactory.getLogger(SaveAnnotationServerOperation.class);

  private final FormatWriter writer = new JadiceAnnotationWriter();

  public SaveAnnotationServerOperation() {
  }

  @Override
  public void invoke(Request<SaveAnnotationParameters> request,
                     ResponseChannel<ServerOperationMessage> responseChannel)
      throws IOException {

    List<Document> documents = request.getDocuments();

    responseChannel.send(new SaveStartedMessage());

    List<String> annotationXmls = new ArrayList<>(documents.size());
    for (Document document : documents) {

      DefaultWriterControls controls = new DefaultWriterControls();
      try {
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        writer.write(document, os, controls);

        String annotationXml = os.toString();
        annotationXmls.add(annotationXml);
      } catch (Throwable e) {

        LOGGER.error("Error saving annotations for document");
        throw new RuntimeException("Could not save annotations");
      }
    }

    SaveCompletedMessage saveCompletedMessage = new SaveCompletedMessage();
    saveCompletedMessage.setResult(annotationXmls);
    responseChannel.send(saveCompletedMessage);
  }

}
 

(neu)

SaveAnnotationsMessageListener
/**
 * <pre>
 * Copyright (c), levigo holding gmbh.
 *
 * This file is subject to the terms and conditions defined in
 * file 'LICENSE.txt', which is part of this source code package.
 * </pre>
 */
package com.levigo.jadice.web.demo.enterprise.server.annotations;

import com.levigo.jadice.document.Document;
import com.levigo.jadice.document.write.DefaultWriterControls;
import com.levigo.jadice.document.write.FormatWriter;
import com.levigo.jadice.format.annotation.JadiceAnnotationWriter;
import com.levigo.jadice.web.demo.enterprise.shared.EnterpriseMessageNames;
import com.levigo.jadice.web.server.internal.util.SynchronizeObjectDeserializer;
import com.levigo.jadice.web.server.messaging.SynchronizeObjectDTO;
import com.levigo.jadice.web.shared.model.CancelHandler;
import com.levigo.jadice.web.transport.server.messaging.IncomingMessageContext;
import com.levigo.jadice.web.transport.server.messaging.MessageListener;
import com.levigo.jadice.web.transport.server.messaging.MessageName;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.ByteArrayOutputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

@Component // Auto-registrierung durch Spring Boot
@MessageName(EnterpriseMessageNames.SAVE_ANNOS) // Horchen auf alle Nachrichten mit diesem Namen
public class SaveAnnotationsMessageListener implements MessageListener<SynchronizeObjectDTO> {
    private ExecutorService executorService; // Logik soll in einem MessageListener immer cancelbar sein, daher wird der Code in einem Future über einen ExecutorService (Thread-pool) ausgeführt
    private SynchronizeObjectDeserializer deserializer; // Für JSON serialisierung (Jackson)
    private final FormatWriter writer = new JadiceAnnotationWriter();

    @Autowired
    public void setDeserializer(SynchronizeObjectDeserializer deserializer) {
        this.deserializer = deserializer;
    }

    @Autowired
    public void setExecutorService(ExecutorService executorService) {
        this.executorService = executorService;
    }

    @Override
    public CancelHandler consume(SynchronizeObjectDTO dto, IncomingMessageContext ctx) throws Exception {
        final Future<?> future = executorService.submit(() -> this.run(dto, ctx));
        return () -> future.cancel(false);
    }

    private void run(SynchronizeObjectDTO dto, IncomingMessageContext ctx) {
        try {
            final Document doc = deserializer.convert(ctx, dto);
            final DefaultWriterControls controls = new DefaultWriterControls();
            final ByteArrayOutputStream os = new ByteArrayOutputStream();

            writer.write(doc, os, controls);

            final String annotationXml = os.toString();
            ctx.reply(EnterpriseMessageNames.SAVE_ANNOS_COMPLETED, new SaveAnnotationCompletedDTO(annotationXml));
            ctx.sendSuccess();
        } catch (Exception e) {
            ctx.sendFail(e);
        }
    }
}
  

Client-Seitig (alt)

SaveAnnotationsCommand
private class ObserverImpl implements Observer<ServerOperationMessage> {

    @Override
    public void onNext(final ServerOperationMessage message) {
      if (message instanceof SaveCompletedMessage) {
        String result = Arrays.toString(((SaveCompletedMessage) message).getResult().toArray());
        showResultPopup(result, null, getPageView());
      }
    }

    @Override
    public void onError(final Throwable e) {
      inProgress = false;
      JadiceEventBus.get().fireEvent(new LoadingEvent(null, LoadingState.FINISHED, getDocument()));

      LOGGER.error("Error saving annotations");
    }

    @Override
    public void onCompleted() {
      resetChangedAnnotations(getDocument());
      inProgress = false;
      JadiceEventBus.get().fireEvent(new LoadingEvent(null, LoadingState.FINISHED, getDocument()));
      getContext().contextChanged();
    }
  }

@Override
  protected void execute() {
    inProgress = true;
    JadiceEventBus.get().fireEvent(new LoadingEvent(null, LoadingState.LOADING, getDocument()));
    invoke(new SaveAnnotationParameters(), new ObserverImpl());
  }



(neu)

SaveAnnotationsCommand
@Override
  protected void execute() {
    inProgress = true;

    JadiceEventBus.get().fireEvent(new LoadingEvent(null, LoadingState.LOADING, getDocument()));

    final Message message = Message.create(EnterpriseMessageNames.SAVE_ANNOS, getTransferableDocument());
    ServerConnection.get().send(message, false, resp -> {
        if (resp.getMessageName().equals(EnterpriseMessageNames.SAVE_ANNOS_COMPLETED)) {
            final SaveAnnotationsCompletedDTO dto = resp.getPayload().cast();
            final String xml = dto.getXml();
            showResultPopup(xml, null, getPageView());
        } else if (resp.getMessageName().equals(DefaultMessageNames.EOC)) {
            final ConversationEndDTO eoc = resp.getPayload().cast();
            if (eoc.wasSuccessful()) {
                resetChangedAnnotations(getDocument());
                inProgress = false;
                JadiceEventBus.get().fireEvent(new LoadingEvent(null, LoadingState.FINISHED, getDocument()));
                getContext().contextChanged();
            } else {
                inProgress = false;
                JadiceEventBus.get().fireEvent(new LoadingEvent(null, LoadingState.FINISHED, getDocument()));
                LOGGER.error("Error saving annotations");
            }
        }
    });
  }



In der Vergangenheit wurde automatisch das Document beim Aufruf einer ServerOperation mit übergeben. Dies ist nun nicht mehr der Fall. Das Document kann aber über getTransferableDocument() (verfügbar in allen Commands) sehr einfach abgerufen werden.


API-Änderungen und Umbenennungen von Klassen und Packages


Client

Alter PfadNeuer PfadWechsel ModulAnmerkung
com.levigo.jadice.web.client.Viewer
  • alte Datei am alten Ort
  • (Plus) JadiceViewer in src/ui/viewer.ts
  • alte Datei unverändert
  • (Plus) webtoolkit-client → @levigo/webtoolkit-ng-client

com.levigo.jadice.web.client.PageView;
  • alte Datei am alten Ort
  • (Minus) in der Typescript-Variante Bestandteil von JadiceViewer in src/ui/viewer.ts


com.levigo.jadice.web.client.ThumbnailView
  • alte Datei am alten Ort
  • (Plus)ThumbnailView in src/ui/web-components/thumbnail-view/thumbnail-view.ts

  • alte Datei unverändert
  • (Plus) webtoolkit-client → @levigo/webtoolkit-ng-client

com.levigo.jadice.web.client.reader.Reader
  • alte Datei am alten Ort
  • (Fehler) in der TypeScript-Variante als public API quasi nicht mehr nötig, s. Anmerkung.

siehe Tutorial, im Prinzip wird bei der Definition des Viewers in der app.component.html per data binding die Source übergeben und der Rest wird automatisch erledigt.

com.levigo.jadice.web.client.ToolManager;

sonstige Tools

  • alte Datei am alten Ort
  • (Plus) ToolManger in src/tool/tool-manager.ts
  • Tools selbst liegen im selben package (nicht alle Tools wurden migriert)
  • alte Datei unverändert
  • (Plus) webtoolkit-client → @levigo/webtoolkit-ng-client

com.jadice.web.util.icon.*

  • alte Datei am alten Ort
  • (Plus) neuer Style in eigenes Modul ausgelagert
  • alte Dateien unverändert
  • neues Modul mit eigener Versionierung: @levigo/jadice-web-icons/

com.jadice.web.util.log.client.Logger;

com.jadice.web.util.log.client.LoggerFactory;

  • (Plus) zusätzlich com.levigo.jadice.web.client.util.LogUtils in der gwt Welt


com.levigo.jadice.web.client.ui.style.Theme;

com.levigo.jadice.web.client.ui.style.UIStyler;


  • alte Datei am alten Ort
  • (Fehler) komplett ersetzt in der TypeScript-Variante
    • (Plus) /@levigo/webtoolkit-ng-client/assets/dark-theme.scss

  • alte Datei unverändert
  • (Plus) webtoolkit-client → @levigo/webtoolkit-ng-client
  • Theme einbinden in der Datei angular.json, siehe Tutorial (json path: /projects/jwv-getting-started/architect/build/options/styles)
  • Styling wurde generell überarbeitet und dezentraler gestaltet, sodass es keine Entsprechung zum UIStyler gibt. 

com.levigo.jadice.web.client.config.ClientConfigurationManager;

  • alte Datei am alten Ort
  • (Plus) ClientConfiguration in src/client-configuration.ts
  • alte Datei unverändert
  • (Plus) webtoolkit-client → @levigo/webtoolkit-ng-client

com.levigo.jadice.web.client.undo.DefaultUndoManager;

com.levigo.jadice.web.client.undo.DocumentUndoSupport;


  • alte Datei am alten Ort


com.levigo.jadice.web.client.util.action.*
  • alte Datei am alten Ort
  • (Plus) am nächsten dran: src/defaults/actions
  • alte Datei unverändert
  • (Plus) webtoolkit-client → @levigo/webtoolkit-ng-client

Annotationen
  • alte Dateien am alten Ort
  • (Plus) src/annotation
  • alte Dateien unverändert
  • (Plus) webtoolkit-client → @levigo/webtoolkit-ng-client
  • Grundlegende Änderungen, für eine erste Integration siehe die Methode 

    AppComponent.setupAnnotations

    im Tutorial.

com.levigo.jadice.web.client.commands.LocaleCommand und weiteres i18n
  • alte Datei am alten Ort
  • (Plus)(Fehler) in TypeScript Variante komplett anderes Konzept
  • alte Dateien unverändert
  • (Plus)(Fehler) neues Modul mit eigener Versionierung: @levigo/ngx-translate-support
  • Siehe Verwendung des I18NService in der app.component.ts des Tutorials

com.google.gwt.user.client.rpc.AsyncCallback

com.levigo.jadice.web.client.reader.AsyncCallback


Kam vorher aus gwt-user.jar

com.levigo.jadice.web.p8.integration.shared.ICNConstants

com.levigo.jadice.web.p8.integration.shared.P8Constants



com.levigo.jadice.web.demo.common.shared.service.sources.PasswordContainingSource

com.levigo.jadice.web.shared.PasswordContainingSource


War vorher Demo-Code, liegt nun im Produkt

com.levigo.jadice.web.client.commands.AbstractServerOperationCommand

com.levigo.jadice.web.client.commands.AbstractMessagingCapableDocumentCommand



com.levigo.jadice.web.demo.common.shared.export.ExportParameters.Type

com.levigo.jadice.web.shared.model.export.ExportType


War vorher Demo-Code, liegt nun im Produkt

Server

Alter PfadNeuer PfadWechsel ModulAnmerkung
com.levigo.jadice.web.server.ServerOperationcom.levigo.jadice.web.transport.client.messaging.MessageListener

webtoolkit-server-api → webtoolkit-connection 

Grundlegende Änderungen, wie Daten vom Client an den Server geschickt werden. 
DocumentDataProvider(Plus) com.levigo.jadice.web.server.UriBasedDocumentDataProviderIn webtoolkit-server-api enthalten Empfohlener DataProvider, der über URIs funktioniert.

API Änderungen bei jadice Core

Wenn Sie jadice Core APIs direkt verwenden sind ggf. weitere Änderungen erforderlich, die API-Änderungen sind hier dokumentiert.


Connection Framework

Server Sent Events deprecated

Der Verbindungsaufbau über SSE ist nun im Standard deaktiviert und auch der Backoff-Mechanismus unterstützt SSE nur, wenn dies manuell aktiviert wurde.

Weitere Informationen sind im Referenzhandbuch zu finden.


Export

Da ServerOperations generell entfernt wurden, muss hier ggf. eine Export-Server-Operation durch einen Export-Message-Listener ersetzt werden.


Änderungen in der Modulstruktur / Integration via Maven

-


Entfernte Klassen und Funktionalitäten

BereichFunktion / KlassenAnmerkung
Clientcom.google.gwt.user.client.rpc.SerializationExceptionDurch Entfernung von GWT-RPC entfallen
Server-API

com.levigo.jadice.web.server.ServerOperation

Durch MessageListener ersetzt