Archiv der Kategorie: Allgemein

Nacktes Thymeleaf

Oftmals nutzen die Beispiele im Internet Spring, oder — noch spezieller — Thymeleaf wird als Template-Engine in Spring MVC eingesetzt, daher jetzt nackt, so einfach es geht.

In unsere POM kommt:

<!-- https://mvnrepository.com/artifact/org.thymeleaf/thymeleaf -->
<dependency>
 <groupId>org.thymeleaf</groupId>
 <artifactId>thymeleaf</artifactId>
 <version>3.0.9.RELEASE</version>
</dependency>

Wir schreiben das Template demo.html:

<!DOCTYPE html>
<html>
<body>
<p>Hey <span data-th-text="${name}">CHRISTIAN</span></p>
</body>
</html>

Und dann kann ein Java-Programm name füllen:

import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.thymeleaf.templateresolver.FileTemplateResolver;

public class ThymeleafDemo {

 public static void main( String[] args ) {

  FileTemplateResolver resolver = new FileTemplateResolver();
  resolver.setCharacterEncoding( "UTF-8" );

  TemplateEngine templateEngine = new TemplateEngine();
  templateEngine.setTemplateResolver( resolver );

  Context ctx = new Context();
  ctx.setVariable( "name", "tutego" );
 
  String result = templateEngine.process( "demo.html", ctx );
  System.out.println( result );
 }
}

 

Aktuelle offene Java-Seminare Q2/Q3 2018

Spring Boot (›SPRINGBOOT‹)

22.–24. Mai 2018 (KW 21), 11.–13. Juni 2018 (KW 24), 16.–18. Juli (KW 29)

Java Grundlagen (›JAVA1‹)

14.–18. Mai 2018 (KW 20), 25.–29. Juni 2018 (KW 26), 6.–10. August 2018 (KW 32), 17.–21. September 2018 (KW 38), 5.–9. November 2018 (KW 45)

Java für Fortgeschrittene (›JAVA2‹)

28. Mai bis 1. Juni 2018 (KW 22), 9.–13. Juni 2018 (KW 28), 20.–24. August 2018 (KW 34), 8.–12. Oktober 2018 (KW 41), 26.–30. November 2018 (KW 48)

Java für C++/C#-Programmierer

23.–27. April 2018 (KW 17), 11.–15. Juni 2018 (KW 24), 23.–27. Juli 2018 (KW 30), 3.–7. September 2018 (KW 36), 22.–26. Oktober 2018 (KW 43)

Nebenläufige Programmierung mit Threads (›JAVACON‹)

7.–8. Mai (KW 19), 14.–15. Juni (KW 24), 19.–20. Juli (KW 29)

Iterator vom Stream besorgen

Sich einen Iterator von einem Stream geben zu lassen ist nützlich, weil dann der Zeitpunkt des Konsumierens vom Zeitpunkt des Stream-Aufbaus getrennt werden kann. Es ist nicht einfach mehrere Streams gleichzeitig abzulaufen, doch mit Iteratoren funktioniert das. Zudem lässt sich in Stream mit einer Generator-Funktion einfach aufbauen, in Iterator aber nicht.

Beispiel: Aus zwei Streams sollen jeweils alterrnierend das nächste Element konsumiert und ausgegeben werden.

Iterator<Integer> iterator1 = Stream.of( 1, 3, 5 ).iterator();

Iterator<Integer> iterator2 = Stream.of( 2, 4, 6, 7, 8 ).iterator();




while ( iterator1.hasNext() || iterator2.hasNext() ) {

  if ( iterator1.hasNext() )

    System.out.println( iterator1.next() );

  if ( iterator2.hasNext() )

    System.out.println( iterator2.next() );

}

Iterator ist ein Datentyp, der häufig in der Rückgabe verwendet wird, seltener als Parametertyp. Mit stream.iterator() ist es aber möglich, die Daten vom Stream genau an solchen Stellen zu übergeben. Einen Stream in einen Iterator zu konvertieren, um diesen dann mit hasNext()/next() abzulaufen, ist wenig sinnvoll, hierfür bietet sich genauso gut forEach(…) auf dem Stream an.

Stream ist nicht Iterable

Der Typ Stream bietet eine Methode iterator(), erweitert jedoch die Schnittstelle Iterable nicht. In der Javadoc ist bei iterator() die Bemerkung „terminale Operation“ vermerkt, denn der Iterator saugt den Stream leer, sodass ein zweiter iterator()-Aufruf auf einem Stream nicht möglich ist. Bei Klassen, die Iterable implementieren, muss ein Aufruf von iterator() beliebig oft möglich sein. Bei Streams ist das nicht gegeben, da die Streams selbst nicht für die Daten stehen wie eine Collection, die daher Iterable ist.

Iterator in Stream umwandeln

Ist auf der anderen Seite ein Iterator gegeben, lässt sich dieser nicht direkt in einen Stream bringen. Iteratoren können wie Streams unendlich sein, und es gibt keinen Weg, die Daten eines Iterators als Quelle zu benutzen. Natürlich ist es möglich, den Iterator abzulaufen und daraus einen neuen Stream aufzubauen, dann muss der Iterator aber endlich sein.

Beispiel: Die Methode ImageIO.getImageReadersBySuffix(String) liefert einen Iterator von ImageReader-Objekten – sie sollen über einen Strom zugänglich sein:

Builder<ImageReader> builder = Stream.builder();
 for ( Iterator<ImageReader> iter = ImageIO.getImageReadersBySuffix( "jpg" );
       iter.hasNext(); )
   builder.add( iter.next() );
 Stream<ImageReader> stream = builder.build();
 System.out.println( stream.count() );    // 1

Einen anderen Weg geht StreamSupport.

Ist eine alte Enumeration gegeben, hilft Collections.list(enumeration).stream(), denn list(…) liefert eine ArrayList mit allen Einträgen; list(Iterator) gibt es da hingegen nicht.

iterator() von BaseStream und PrimitiveIterator.OfXXX

Die Schnittstelle Stream deklariert keine abstrakte iterator()-Methode, sondern bekommt die Methode vom Obertyp BaseStream vererbt. BaseStream ist insgesamt Basistyp von:

  • Stream<T>, DoubleStream, IntStream, LongStream

Alle diese drei Typen haben damit iterator()-Methdoden, doch die Rückgaben sind unterschiedlich:

Typ iterator()-Methdoden und Rückgabe
Stream<T> Iterator<T> iterator()
DoubleStream PrimitiveIterator.OfDouble iterator()
IntStream PrimitiveIterator.OfInt iterator()
LongStream PrimitiveIterator.OfInt iterator()

Unterschiedliche Rückgaben der Iteratoren

PrimitiveIterator ist eine Schnittstelle aus dem Paket java.util mit eben drei inneren statischen Schnittstellen: OfDouble, OfInt und OfLong, die PrimitiveIterator erweiten. Jeder dieser drei inneren Typen hat eigene, aber symmetrische Methoden:

  • default void forEachRemaining(Consumer<? super WrapperTyp> action)
  • default void forEachRemaining(WrapperTypConsumer action)
  • default WrapperTyp next()
  • PrimitiverTyp nextPrimitiverTyp()

Statt WrapperTyp und PrimitiverTyp ist dann Double, Integer, Long und double, int, double einzusetzen.

Beispiel: Ein vom Stream abgeleiter Iterator besorgt Zahlen. Diese werden in einem anderen Stream eingesetzt.

PrimitiveIterator.OfInt counter =

  IntStream.iterate( 1, Math::incrementExact ).iterator();

Stream.of( "Telegram", "WhatsApp", "Facebook Messenger", "Insta" )

      .map( s -> counter.nextInt() + ". " + s )

      .forEach( System.out::println );

Java sollte „Inkubator“-Features bekommen.

Das bedeutet, es gibt Syntaxerweiterungen, aber die sind erst mal zum Testen.

http://openjdk.java.net/jeps/12

An incubating language or VM feature is a new feature of the Java SE Platform that is fully specified, fully implemented, and yet impermanent. It is available in a JDK feature release to provoke developer feedback based on real world use; this may lead to it becoming permanent in a future Java SE Platform.

In der Mailliste fragen die Eclipse-Bauer schon nach, ob das verpflichtend ist … es wäre blöd, wenn man sich bei den JDT viel Mühe gibt, und dann wird das doch nicht übernommen: http://mail.openjdk.java.net/pipermail/jdk-dev/2018-February/000642.html

JEP-12 ist allerdings noch nicht bestätigt.

 

JEP Draft für „Raw String Literals“ in Java

In der Diskussion ist der „Back-Tick“, sodass es so aussehen würde:

File Paths Example

Traditional String Literals Raw String Literals
Runtime.getRuntime().exec("\"C:\\Program Files\\foo\" bar");
Runtime.getRuntime().exec(`"C:\Program Files\foo" bar"`);

Multi-line Example

Traditional String Literals Raw String Literals
String html = "<html>\n"
              "    <body>\n" +
              "		    <p>Hello World.</p>\n" +
              "    </body>\n" +
              "</html>\n";
String html = `<html>
                   <body>
                       <p>Hello World.</p>
                   </body>
               </html>
              `;

Regular Expression Example

Traditional String Literals Raw String Literals
System.out.println("this".matches("\\w\\w\\w\\w"));
System.out.println("this".matches(`\w\w\w\w`));

Output:

true

Polyglot Example

Traditional String Literals Raw String Literals
String script = "function hello() {\n" +
                "   print(\'\"Hello World\"\');\n" +
                "}\n" +
                "\n" +
                "hello();\n";
ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
Object obj = engine.eval(script);
String script = `function hello() {
                    print('"Hello World"');
                 }
hello();
            `

ScriptEngine engine = new ScriptEngineManager().getEngineByName(„js“); Object obj = engine.eval(script);

Output:

"Hello World"

Database Example

Traditional String Literals Raw String Literals
String query = "SELECT EMP_ID, LAST_NAME\n" +
               "FROM EMPLOYEE_TBL\n" +
               "WHERE CITY = 'INDIANAPOLIS'\n" +
               "ORDER BY EMP_ID, LAST_NAME;\n";
String query = `SELECT EMP_ID, LAST_NAME
                FROM EMPLOYEE_TB
                WHERE CITY = 'INDIANAPOLIS'
                ORDER BY EMP_ID, LAST_NAME;
               `;

Ich bin gespannt, ob das in Java 10 kommen wird. Ich dachte schon, dass Java 10 Release wird eher klein, doch zusammen mit den var-Variablen sind das zwei kleine, aber bedeutende Sprachänderungen.

Details unter http://openjdk.java.net/jeps/8196004.

Aktuelles zu Java 10

Nach Java 9 wird uns Java 10 beglücken. Die Zeitleiste sieht so aus:

2017/12/14 Rampdown Phase One
2018/01/11 All Tests Run
2018/01/18 Rampdown Phase Two
2018/02/22 Final Release Candidate
2018/03/20 General Availability

Java 10 ist laut http://openjdk.java.net/projects/jdk/10/ ab heute in der „Rampdown Phase One“ (http://openjdk.java.net/projects/jdk8/milestones#Rampdown_start):

  • Rampdown — Phases in which increasing levels of scrutiny are applied to incoming changes. In phase 1, only P1-P3 bugs can be fixed. In phase 2 only showstopper bugs can be fixed.

Neuerungen dir wir für Java 10 erwarten können, beschrieben im JEP:

286: Local-Variable Type Inference
296: Consolidate the JDK Forest into a Single Repository
304: Garbage-Collector Interface
307: Parallel Full GC for G1
310: Application Class-Data Sharing
312: Thread-Local Handshakes
313: Remove the Native-Header Generation Tool (javah)
314: Additional Unicode Language-Tag Extensions
316: Heap Allocation on Alternative Memory Devices
317: Experimental Java-Based JIT Compiler
319: Root Certificates
322: Time-Based Release Versioning

Es wird auch Java 10, Java 11, usw. heißen und nicht, wie vorher angedacht, Java 18, etc. nach Jahreszahlen. Aus dem http://openjdk.java.net/jeps/322:

  • $FEATURE is incremented every six months: The March 2018 release is JDK 10, the September 2018 release is JDK 11, and so forth.

 

Inselraus: Zugriff auf SMB-Server mit jCIFS

Microsoft Windows nutzt zur Datei- und Verzeichnisfreigabe, zur Freigabe von Druckern und Kommunikationsschnittstellen das Protokoll SMB (Server Message Block). Es ist weit verbreitet, und jede aktuelle Windows-Version lässt sich als Client und Server konfigurieren – gleichzeitig gibt es unter Unix das populäre Samba, einen SMB-Server unter Open Source, der von Andrew Tridgell und Kollegen entwickelt wurde.

Mithilfe der jCIFS-SMB-Bibliothek (http://jcifs.samba.org/) kann ein Java-Programm auf Datei- und Verzeichnisfreigaben zugreifen und Freigaben auflisten. jCIFS ist eine erweiterte Implementierung von CIFS und unterstützt Unicode, Batching, verschlüsselte Authentifizierung, Transactions, das Remote Access Protocol (RAP) und Weiteres. Die Bibliothek steht unter der LGPL.

Die Klassen jcifs.smb.SmbFile, SmbFileInputStream und SmbFileOutputStream verhalten sich ähnlich wie java.io.File, FileInputStream und FileOutputStream. Sie werden mit einem Dateipfad (URL) parametrisiert, der mit smb:// beginnt. Um eine Datei zu beziehen, muss vorher der Server spezifiziert werden. Dazu dienen Eigenschaften wie WINS. Sie werden mit Config.setProperty(„wins“, „IP-Adresse“); gesetzt.

Beispiel: Lies eine Datei aus, und kopiere sie um:

InputStream in =
   new SmbFileInputStream( "smb://user:passwd@host/c/My Documents/doc.txt" );
 Path target = …
 Files.copy( in, target );

Inselraus: Anwendungen für FilterReader und FilterWriter

Unsere nächste Klasse bringt uns etwas näher an das HTML-Format heran. Wir wollen eine Klasse HTMLWriter entwerfen, die FilterWriter erweitert und Textausgaben in HTML konvertiert. In HTML werden Tags eingeführt, die vom Browser erkannt und besonders behandelt werden. Findet etwa der Browser im HTML-Text eine Zeile der Form <strong>Dick</strong>, so stellt er den Inhalt „Dick“ in fetter Schrift dar, da das <strong>-Element den Zeichensatz umstellt. Alle Tags werden in spitzen Klammern geschrieben. Daraus ergibt sich, dass HTML einige spezielle Zeichenfolgen (Entities genannt) verwendet. Wenn diese Zeichen auf der HTML-Seite dargestellt werden, muss dies durch spezielle Zeichensequenzen geschehen:

  • < wird zu &lt;
  • > wird zu &gt;
  • & wird zu &amp;

Kommen diese Zeichen im Quelltext vor, so muss unser HTMLWriter diese Zeichen durch die entsprechende Sequenz ersetzen. Andere Zeichen sollen nicht ersetzt werden.

Den Browsern ist die Struktur der Zeilen in einer HTML-Datei egal. Sie formatieren wiederum nach speziellen Tags. Zeilenvorschübe etwa werden mit <br/> eingeleitet. Unser HTMLWriter soll zwei leere Zeilen durch das Zeilenvorschub-Element <br/> markieren.

HTML-Dokument schreiben

Alle sauberen HTML-Dateien haben einen wohldefinierten Anfang und ein wohldefiniertes Ende. Das folgende kleine HTML-Dokument ist wohlgeformt und zeigt, was unser Programm später erzeugen soll:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
                       "http://www.w3.org/TR/html4/strict.dtd">
 <html><head><title>Superkreativer Titel</title></head>
 <body><p>
 Und eine Menge von Sonderzeichen: &lt; und &gt; und &amp;
 Zweite Zeile
 <br/>
 Leerzeile
 Keine Leerzeile danach
 </p></body></html>

Der Titel der Seite sollte im Konstruktor übergeben werden können. Hier ist nun das Programm für den HTMLWriter:

package com.tutego.insel.io.stream;
 
 import java.io.*;
 
 class HTMLWriter extends FilterWriter {
   
   private boolean newLine;
 
   /**
    * Creates a new filtered HTML writer with a title for the web page.
    *
    * @param out  a Writer object to provide the underlying stream.
    * @throws IOException if the header cannot be written
    */
   public HTMLWriter( Writer out, String title ) throws IOException {
     super( out );
 
     out.write( "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"" +
            " \"http://www.w3.org/TR/html4/strict.dtd\">\n"  );
     out.write( "<html><head><title>" + title + "</title></head>\n<body><p>\n" );
   }
 
   /**
    * Creates a new filtered HTML writer with no title for the web page.
    *
    * @param out  a Writer object to provide the underlying stream.
    */
   public HTMLWriter( Writer out ) {
     super( out );
   }
 
   /**
    * Writes a single character.
    */
   @Override
   public void write( int c ) throws IOException {
     switch ( c ) {
       case '<':
         out.write( "&lt;" );
         newLine = false;
         break;
       case '>':
         out.write( "&gt;" );
         newLine = false;
         break;
       case '&':
         out.write( "&amp;" );
         newLine = false;
         break;
       case '\n':
         if ( newLine ) {
           out.write( "<br/>\n" );
           newLine = false;
         }
         else
           out.write( "\n" );
         newLine = true;
         break;
       case '\r':
         break; // ignore
 
       default :
         out.write( c );
         newLine = false;
     }
   }
 
   /**
    * Writes a portion of an array of characters.
    *
    * @param  cbuf Buffer of characters to be written
    * @param  off  Offset from which to start reading characters
    * @param  len  Number of characters to be written
    * @exception   IOException If an I/O error occurs
    */
   @Override
   public void write( char[] cbuf, int off, int len ) throws IOException {
     for ( int i = off; i < len; i++ )
       write( cbuf[ i ] );
   }
 
   /**
    * Writes a portion of a string.
    *
    * @param  str  String to be written.
    * @param  off  Offset from which to start reading characters
    * @param  len  Number of characters to be written
    * @exception   IOException If an I/O error occurs
    */
   @Override
   public void write( String str, int off, int len ) throws IOException {
     for ( int i = off; i < len; i++ )
       write( str.charAt( i ) );
   }
 
   /**
    * Closes the stream.
    *
    * @throws IOException If the prolog can not be written or the underlying stream 
    * not be closed
    */
   @Override
   public void close() throws IOException {
     try {
       out.write( "</p></body></html>" );
     }
     finally {
       out.close();  // Ignoriere, falls out.close() und out.write() knallt
     }
   }
 }

Ein Demo-Programm soll die aufbereiteten Daten in einen StringWriter schreiben:

StringWriter sw = new StringWriter();

try ( HTMLWriter html = new HTMLWriter( sw, "Superkreativer Titel" );

      PrintWriter pw = new PrintWriter( html ) ) {

  pw.println( "Und eine Menge von Sonderzeichen: < und > und &" );

  pw.println( "Zweite Zeile" );

  pw.println();

  pw.println( "Leerzeile" );

  pw.println( "Keine Leerzeile danach" );

}

System.out.println( sw );

HTML-Tags mit einem speziellen Filter überlesen

Unser nächstes Beispiel ist eine Klasse, die den FilterReader so erweitert, dass HTML-Tags überlesen werden. Die Klasse FilterReader deklariert den notwendigen Konstruktor zur Annahme des Reader, der die wirklichen Daten liefert, und überschreibt zwei read(…)-Methoden. Die read()-Methode ohne Parameter – die ein int für ein gelesenes Zeichen zurückgibt – legt einfach ein 1 Zeichen großes Feld an und ruft dann die zweite überschriebene read(char[], int, int)-Methode auf, die die Daten in ein Feld liest. Da dieser Methode neben dem Feld auch noch die Größe übergeben werden kann, müssen wirklich so viele Zeichen gelesen werden. Es reicht einfach nicht aus, die übergebene Anzahl von Zeichen vom tiefer liegenden Reader zu lesen, sondern hier müssen wir beachten, dass eingestreute Tags nicht zählen. Die Zeichenkette <p>Hallo<p> ist ja nur fünf Zeichen lang und nicht elf!

 package com.tutego.insel.io.stream;
 
 import java.io.*;
 
 public class HTMLReader extends FilterReader {
   private boolean inTag = false;
 
   public HTMLReader( Reader in ) {
     super( in );
   }
 
   @Override
   public int read() throws IOException {
     char[] buf = new char[ 1 ];
     return read( buf, 0, 1 ) == –1 ? –1 : buf[ 0 ];
   }
 
   @Override
   public int read( char[] cbuf, int off, int len ) throws IOException {
     int numchars = 0;
 
     while ( numchars == 0 ) {
       numchars = in.read( cbuf, off, len );
 
       if ( numchars == –1 ) // EOF?
         return –1;
 
       int last = off;
 
       for ( int i = off; i < off + numchars; i++ ) {
         if ( ! inTag ) {
           if ( cbuf[ i ] == '<' )
             inTag = true;
           else
             cbuf[ last++ ] = cbuf[ i ];
         }
         else if ( cbuf[ i ] == '>' )
           inTag = false;
       }
       numchars = last – off;
     }
     return numchars;
   }
 }

Ein Beispielprogramm soll die Daten aus einem StringReader ziehen. Der HTMLReader bekommt diesen StringReader und wird selbst von Scanner genutzt, damit wir die komfortable nextLine()-Methode nutzen können. Da hier keine externen Ressourcen vorkommen, müssen wir nichts schließen, und ein try mit Ressourcen kann entfallen.

String s = "<html>Hallo! <b>Ganz schön fett.</b> "
            + "Ah, wieder normal.</html>";
 
 Reader sr = new StringReader( s );
 Reader hr = new HTMLReader( sr );
 Scanner scanner = new Scanner( hr );
 while ( scanner.hasNextLine() )
   System.out.println( scanner.nextLine() );

Es produziert dann die einfache Ausgabe:

Hallo! Ganz schön fett. Ah, wieder normal.

Inselraus: Ein LowerCaseWriter

Wir wollen im Folgenden einen Filter schreiben, der alle in den Strom geschriebenen Zeichen in Kleinbuchstaben umwandelt. Drei Dinge sind für einen eigenen FilterWriter nötig:

  • Die Klasse leitet sich von FilterWriter
  • Unser Konstruktor nimmt als Parameter ein Writer-Objekt und ruft mit super(out) den Konstruktor der Oberklasse auf, also FilterWriter. Die Oberklasse speichert das übergebene Argument in der geschützten Objektvariablen out, sodass die Unterklassen darauf zugreifen können.
  • Wir überlagern die drei write(…)-Methoden und eventuell noch close() und flush(). Unsere write(…)-Methoden führen dann die Filteroperationen aus und geben die wahren Daten an den Writer
class LowerCaseWriter extends FilterWriter {
 
   public LowerCaseWriter( Writer writer ) {
     super( writer );
   }
 
   @Override
   public void write( int c ) throws IOException {
     out.write( Character.toLowerCase((char)c) );
   }
 
   @Override
   public void write( char[] cbuf, int off, int len ) throws IOException {
     write( String.valueOf( cbuf ), off, len );
   }
 
   @Override
   public void write( String s, int off, int len ) throws IOException {
     out.write( s.toLowerCase(), off, len );
   }
 }

Und die Nutzung sieht dann so aus:

StringWriter sw = new StringWriter();
PrintWriter  pw = new PrintWriter( new LowerCaseWriter( sw ) );
pw.println( "Eine Zeile für klein und groß" );
System.out.println( sw.toString() );

JAX-RS mit Jersey, Teil 2 (RESTful Web-Services)

Content-Handler, Marshaller und verschiedene MIME-Typen

JAX-RS erlaubt grundsätzlich alle MIME-Typen, und die Daten selbst können auf verschiedene Java-Datentypen übertragen werden. So ist es egal, ob bei Textdokumenten zum Beispiel der Rückgabetyp String oder OutputStream ist; selbst ein File-Objekt lässt sich zurückgeben. Für einen Parametertyp – Parameter werden gleich vorgestellt – gilt das Gleiche: JAX-RS ist hier recht flexibel und kann über einen InputStream oder Writer einen String entgegennehmen. (Reicht das nicht, können so genannte Provider angemeldet werden.)

Bei XML-Dokumenten kommt hinzu, dass JAX-RS wunderbar mit JAXB zusammenspielt.

XML mit JAXB

Dazu ein Beispiel für einen Dienst hinter der URL http://localhost:8080/api/dating/serverinfo, der eine Serverinformation im XML-Format liefert. Das XML wird automatisch von JAXB generiert.

@GET
 @Path( "serverinfo" )
 @Produces( MediaType.TEXT_XML )
 public ServerInfo serverinfo() {
   ServerInfo info = new ServerInfo();
   info.server = System.getProperty( "os.name" )+" "+System.getProperty( "os.version" );
   return info;
 }

@XmlRootElement
 class ServerInfo {
   public String server;
 }

Die Klasse ServerInfo ist eine JAXB-annotierte Klasse. In der eigenen JAX-RS-Methode serverinfo() wird dieses ServerInfo-Objekt aufgebaut, das Attribut gesetzt und dann zurückgegeben; der Rückgabetyp ist also nicht String wie vorher, sondern explizit ServerInfo. Dass der MIME-Typ XML ist, sagt @Produces(MediaType.TEXT_XML). Und noch eine Annotation nutzt das Beispiel: @Path. Lokal an der Methode bedeutet es, dass der angegebene Pfad zusätzlich zur Pfadangabe an der Klasse gilt. Also ergibt sich der komplette Pfad aus: Basispfad + „dating“ + „/“ + „serverinfo“. Wir können http://localhost:8080/api/dating/serverinfo im Browser eingeben.

JSON-Serialisierung *

Ist der Client eines REST-Aufrufs ein JavaScript-Programm in einem Webbrowser, ist es in der Regel praktischer, statt XML das Datenformat JSON zu verwenden. JAX-RS bindet drei Möglichkeiten zum Senden von JSON:

  1. JSON-Übersetzungdes Java-Objekts über die JSON-Bibliothek („POJO based JSON binding support“). Es lassen sich so ziemlich alle Java-Objekte abbilden.
  2. JAXB-basierte JSON-Übersetz Die JSON-Bibliothek liest die JAXB-annotierten Objekte aus und führt anhand der Metadaten die Umsetzung durch. JAXB wird hier nicht nur für XML verwendet, sondern auch für JSON; natürlich ergeben nicht alle Eigenschaften einen Sinn.
  3. Automatisch ohne ein Mapping arbeitet eine Low-Level-API. Sie gibt maximale Flexibilität, aber erfordert viel Handarbeit. Seit Java EE 7 ist die API im Standard, aber nicht Teil der Java SE.

Schauen wir uns die zweite Lösung an. Jersey unterstützt von Haus aus Jackson, MOXy und Jettison als JSON-Objekt-Mapper. Um MOXy einzusetzen müssen weitere Java-Archive in den Klassenpfad aufgenommen werden. Wir können die Abhängigkeiten über Maven beschreiben, dann ist folgendes in der pom.xml aufzunehmen:

<dependency>

<groupId>org.glassfish.jersey.media</groupId>

<artifactId>jersey-media-moxy</artifactId>

<version>2.25</version>

</dependency>

Oder zu Fuß müssen die folgenden JAR-Dateien geladen und dann im Klassenpfad aufgenommen werden:

  • https://mvnrepository.com/artifact/org.glassfish.jersey.media/jersey-media-moxy/
  • https://mvnrepository.com/artifact/org.glassfish.jersey.ext/jersey-entity-filtering/
  • https://mvnrepository.com/artifact/org.eclipse.persistence/org.eclipse.persistence.moxy/
  • https://mvnrepository.com/artifact/org.eclipse.persistence/org.eclipse.persistence.core/
  • https://mvnrepository.com/artifact/org.glassfish/javax.json/
    • Um von XML auf JSON zu wechseln muss bei den REST-Methoden lediglich der APPLICATION_JSON eingesetzt werden:
@GET

@Path( "jsonserverinfo" )

@Produces( MediaType.APPLICATION_JSON )

public ServerInfo jsonserverinfo() {

  return serverinfo();

}

Das reicht schon aus, und der Server sendet ein JSON-serialisiertes Objekt.

Alles Verhandlungssache

Die JAX-RS-API bietet mit dem MIME-Typ noch eine Besonderheit, dass der Server unterschiedliche Formate liefern kann, je nachdem, was der Client verarbeiten möchte oder kann. Der Server macht das mit @Produces klar, denn dort kann eine Liste von MIME-Typen stehen. Soll der Server XML und JSON generieren können, schreiben wir:

@GET

@Path( "jsonxmlserverinfo" )

@Produces( { MediaType.TEXT_XML, MediaType.APPLICATION_JSON } )

public ServerInfo jsonxmlserverinfo() {

  return serverinfo();

}

Kommt der Client mit dem Wunsch nach XML, bekommt er XML, möchte er JSON, bekommt er JSON. Die Jersey-Client-API teilt über request(String) bzw. request(MediaType… types) mit, was ihr Wunschtyp ist. (Dieser Typwunsch ist eine Eigenschaft von HTTP und nennt sich Content Negotiation.)

WebTarget wr1 = ClientBuilder.newClient().target( "http://localhost:8080/api" );

Builder b1 = wr1.path( "dating" ).path( "jsonxmlserverinfo" )

                .request( MediaType.APPLICATION_JSON );

System.out.println( b1.get( ServerInfo.class ).getServer() );  // Windows 10 10.0




WebTarget wr2 = ClientBuilder.newClient().target( "http://localhost:8080/api" );

Builder b2 = wr2.path( "dating" ).path( "jsonxmlserverinfo" )

                .request( MediaType.TEXT_XML );

System.out.println( b2.get( ServerInfo.class ).getServer() );  // Windows 10 10.0




WebTarget wr3 = ClientBuilder.newClient().target( "http://localhost:8080/api" );

Builder b3 = wr3.path( "dating" ).path( "jsonxmlserverinfo" )

                .request( MediaType.TEXT_PLAIN );

try {

  System.out.println( b3.get( ServerInfo.class ).getServer() );

}

catch ( Exception e ) {

  System.out.println( e );

  // javax.ws.rs.NotAcceptableException: HTTP 406 Not Acceptable

}

Passt die Anfrage auf den Typ von @Produces, ist alles prima, ohne Übereinstimmung gibt es einen Fehler. Bei der letzten Zeile gibt es eine Ausnahme („javax.ws.rs.NotAcceptableException: HTTP 406 Not Acceptable“), da JSON und XML eben nicht purer Text sind.

REST-Parameter

Im Abschnitt „Wie sieht ein REST-URI aus?“ in Abschnitt 15.2.1 wurde ein Beispiel vorgestellt, wie Pfadangaben aussehen, wenn sie einen RESTful Service bilden:

http://www.tutego.de/blog/javainsel/category/java-7/page/2/

Als Schlüssel-Wert-Paare lassen sich festhalten: category=java-7 und page=2. Der Server wird die URL auseinanderpflücken und genau die Blog-Einträge liefern, die zur Kategorie „java-7“ gehören und sich auf der zweiten Seite befinden.

Bisher sah unser REST-Service auf dem Endpunkt /api/dating/ so aus, dass einfach ein String zurückgegeben wird. Üblicherweise gibt es aber unterschiedliche URLs, die Operationen wie „finde alle“ oder „finde alle mit der Einschränkung X“ abbilden. Bei unseren Dating-Dienst wollen wir dem Client drei Varianten zur Abfrage anbieten (mit Beispiel):

  • /api/dating/: alle Nutzer
  • /api/dating/gender/nasi: alle Nutzer mit dem Geschlecht „nasi“
  • /api/dating/gender/nasi/age/18-28: alle „nasischen“-Nutzer im Alter von 18 bis 28

Das erste Beispiel macht deutlich, dass hier ohne explizite Angabe weiterer Einschränkungskriterien alle Nachrichten erfragt werden sollen, während mit zunehmend längerer URL weitere Einschränkungen dazukommen.

Parameter in JAX-RS kennzeichnen

Die JAX-RS-API erlaubt es, dass Parameter (wie eine ID oder ein Such-String) leicht eingefangen werden können. Für die drei möglichen URLs entstehen zum Beispiel drei überladene Methoden:

@GET @Produces( MediaType.TEXT_PLAIN )
 public String meet() ...
 


@GET

@Path( "gender/{gender}" )

@Produces( MediaType.TEXT_PLAIN )

public String meet( @PathParam( "gender" ) String gender ) {

  return String.format( "Geschlecht = %s", gender );

}




@GET

@Produces( MediaType.TEXT_PLAIN )

@Path( "gender/{gender}/age/{age}" )

public String meet( @PathParam( "gender" ) String gender,

                    @PathParam( "age" ) String age ) {

  return String.format( "Geschlecht = %s, Altersbereich = %s", gender, age );

}

Die bekannte @Path-Annotation enthält nicht einfach nur einen statischen Pfad, sondern beliebig viele Platzhalter in geschweiften Klammern. Der Name des Platzhalters taucht in der Methode wieder auf, nämlich dann, wenn er mit @PathParam an einen Parameter gebunden wird. Jersey parst für uns die URL und füllt die Parametervariablen passend auf bzw. ruft die richtige Methode auf. Da die JAX-RS-Implementierung den Wert füllt, nennt sich das auch JAX-RS-Injizierung.

URL-Endung Aufgerufene Methode
/api/dating/ meet()
/api/dating/gender/nasi meet( String gender )
/api/dating/gender/nasi/age/18-28 meet( String gender, String age )

Welche URL zu welcher Methode führt

Die Implementierungen der Methoden würden jetzt an einen Daten-Service gehen und die selektierten Datensätze zurückgeben. Das zeigt das Beispiel nicht, da dies eine andere Baustelle ist.

Tipp: Wenn Parameter falsch sind, kann eine Methode eine besondere Ausnahme vom Typ javax.ws.rs.WebApplicationException (dies ist eine RuntimeException) erzeugen. Im Konstruktor von javax.ws.rs.WebApplicationException lässt sich ein Statuscode als int oder als Aufzählung vom Typ Response.Status übergeben, etwa new WebApplicationException(Response.Status. EXPECTATION_FAILED).

REST-Services mit Parametern über die Jersey-Client-API aufrufen

Wenn die URLs in dem Format schlüssel1/wert1/schlüssel2/wert2 aufgebaut sind, dann ist ein Aufbau einfach durch Kaskadierung der path(…)-Methoden umzusetzen:

System.out.println( ClientBuilder.newClient().target( "http://localhost:8080/api" )

  .path( "dating" ).path( "gender" ).path( "nasi" )

  .request().get( String.class ) ); // Geschlecht = nasi




System.out.println( ClientBuilder.newClient().target( "http://localhost:8080/api/dating" )

  .path( "gender" ).path( "nasi" ).path( "age" ).path( "18-28" )

  .request().get( String.class ) ); // Geschlecht = nasi, Altersbereich = 18-28

Multiwerte

Schlüssel-Wert-Paare lassen sich auch auf anderen Wegen übermitteln statt nur auf dem Weg über schlüssel1/wert1/schlüssel2/wert2. Besonders im Web und für Formularparameter ist die Kodierung über schlüssel1=wert1&schlüssel2=wert2 üblich. Auch das kann in JAX-RS und mit der Jersey-Client-API abgebildet werden:

  • Anstatt Parameter mit @PathParam zu annotieren, kommt bei Multiwerten @QueryParam zum Einsatz.
  • Anstatt mit path(String) zu arbeiten, wird bei dem Jersey-Client mit queryParam(String key, Object… value)

Hinweis

Es gibt eine Reihe von Dingen, die in Methoden per Annotation übermittelt werden können, und nicht nur @PathParam und @QueryParam. Dazu kommen noch Dinge wie @HeaderParam für den HTTP-Request-Header, @CookieParam für Cookies, @Context für Informationsobjekte und weitere Objekte.

PUT-Anforderungen und das Senden von Daten

Zum Senden von Daten an einen REST-Service ist die HTTP-PUT-Methode gedacht. Die Implementierung einer Java-Methode kann so aussehen:

@PUT

@Path( "message/{user}" )

@Consumes( MediaType.TEXT_PLAIN )

public Response postMessage( @PathParam( "user" ) String user, String message ) {

  System.out.printf( "%s sendet '%s'%n", user, message );

  return Response.noContent().build();

}

Zunächst gilt, dass statt @GET ein @PUT die Methode annotiert. @Consumes hält den MIME-Typ dieser gesendeten Daten fest. Ein zusätzlicher @PathParam fängt die Benutzerkennung ein, die dann mit der gesendeten PUT-Nachricht auf der Konsole ausgegeben wird.

Diese beiden Annotationen @PUT und @Consumes sind also nötig. Eine Rückgabe in dem Sinne hat die Methode nicht, und es ist umstritten, ob ein REST-PUT überhaupt neben dem Statuscode etwas zurückgeben soll. Daher ist die Rückgabe ein spezielles JAX-RS-Objekt vom Typ Response, das hier für 204, „No Content“, steht. Ein 204 kommt auch immer dann automatisch zurück, wenn eine Methode void als Rückgabe deklariert.

PUT/POST/DELETE-Sendungen mit der Jersey-Client-API absetzen

Ein Invocation.Builder bietet neben get(…) auch die anderen Java-Methoden für HTTP-Methoden, also delete(…), post(…), options(…), … und eben auch put(…) zum Schreiben.

Client client = ClientBuilder.newClient();

WebTarget target = client.target( "http://localhost:8080/api" );

Response response = target.path( "dating" ).path( "message" )

                          .path( "Ha" ).request().put( Entity.text("Hey Ha!") );

System.out.println( response );

Die put(…)-Methode erwartet als Argument den Typ Entity, und zum Objektaufbau gibt es diverse statische Methoden in Entity. Es ist Entity.text(„Hey Chris“) eine Abkürzung für Entity.entity(„Hey Chris“, MediaType.TEXT_PLAIN).

Versionierung einer REST-API

Die REST-Schnittstelle ist ein externer Endpunkt einer Software und unterliegt den gleichen Modifikationen wie übliche Software: Schnittstellen ändern sich, die ausgetauschten Objekte bekommen zusätzliche Felder, usw. Ändert sich die Schnittstelle, erzeugt das eine neue Version. Alte APIs sollte ein Server eine Zeit lang beibehalten, damit nicht von einem Tag auf dem anderen eine vielleicht große Anzahl von Klienten mit der älteren Schnittstelle kommunikationslos sind.

Prinzipiell gibt es drei Ansätze bei der Versionierung von REST-APIs; die ersten beiden Varianten nutzen die Möglichkeiten der URL bzw. des HTTP:

  1. Änderung der URL, dass etwa eine Versionskennung mit eingebaut wird. Zu finden ist das bei vielen großen Unternehmen, und zu erkennen etwa an der Versionsnummer bei https://api.twitter.com/1/ https://www.googleapis.com/youtube/v3/, https://api.pinterest.com/v1/, https://api.instagram.com/v1/.
  2. In einem HTTP-Header, wie Accepts. Dort lässt sich die Version in den Dateityp hineinkodieren. Normalerweise ist der Header mit einem MIME-Typ wie text/plain, text/html belegt, doch der RFC4288 sieht in Sektion 3.2 einen „Vendor Tree“ mit dem Präfix vor, sodass sich damit ein eigener Media-Typ inklusive Version formulieren lässt. Das kann so aussehen: application/vnd.tutego.app.api+json;version=2.1 oder application/vnd.tutego.app.api-v2.1+json.
  3. In den Rumpf einer Nachricht lässt sich ebenfalls eine Version einkodieren, zum Beispiel wenn JSON übermittelt wird: {„version“: „2.1“,…}. Nachteilig ist, dass URLs ohne Parameter und ohne Körper keinen Platz für eine Versionsnummer lassen.

Alle Lösungen lassen sich prinzipiell mit JAX-RS umsetzen, wobei die Lösung 1 am Einfachsten ist.