Inselraus: Swing/Auswahlmenü mit Separator

Standardmäßig unterstützt die JList und auch JComboBox keine Separatoren, doch die Unterteilung in Segmente ist in der Praxis sehr nützlich. Microsoft Word und PowerPoint benutzen sie zum Beispiel, um die zuletzt vom Benutzer ausgewählten Zeichensätze prominent oben in der Liste zu haben (Excel dagegen tut dies nicht).

Abbildung

Abbildung 9.42: Separator in Words Zeichensatzauswahlliste

Wir wollen diese Möglichkeit nachbilden und dabei noch einiges über Modelle und Renderer lernen.

Abbildung

Abbildung 9.43: Eine eigene Variante mit JSeparator in JComboBox

Bei der Umsetzung gibt es unterschiedliche Varianten, die sich außer in der technischen Implementierung darin unterscheiden, ob das Modell eine Markierung für den Separator enthält oder nicht. Wir stellen beide Ansätze vor und beginnen mit der ersten Variante, also damit, einem Zellen-Renderer Positionen mitzugeben, die sagen, wo ein Trennstrich zu zeichnen ist.

package com.tutego.insel.ui.list;

import javax.swing.*;

public class JComboBoxWithSeparator1 {
  public static void main( String[] args ) throws Exception {
    UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
    JFrame frame = new JFrame();
    frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
    String[] items = { "Cambria", "Arial", "Verdana", "Times" };
    JComboBox<String> comboBox = new JComboBox<>( items );
    ListCellRenderer<String> renderer = new SeparatorAwareListCellRenderer1<>(
                                          comboBox.getRenderer(), 0 );
    comboBox.setRenderer( renderer );
    frame.add( comboBox );
    frame.pack();
    frame.setVisible( true );
  }
}

Die eigene Klasse SeparatorAwareListCellRenderer1 ist ein ListCellRenderer, den die JComboBox zur Darstellung der Komponenten nutzt. Im Konstruktor des Renderers geben wir den Original-Renderer mit – es kann ein bestimmter Renderer schon vorinstalliert sein, den wollen wir dekorieren – sowie eine variable Argumentliste von Positionen. Das Beispiel übergibt nur 0, da nach dem ersten Element (Index = 0) ein Trennzeichen zu setzen sein soll, sodass Cambria und Arial abgetrennt sind.

package com.tutego.insel.ui.list;

import java.awt.*;
import java.util.Arrays;

import javax.swing.*;

public class SeparatorAwareListCellRenderer1<E> implements ListCellRenderer<E>
{
  private final ListCellRenderer<? super E> delegate;
  private final int[] indexes;
  private final JPanel panel = new JPanel( new BorderLayout() );

  public SeparatorAwareListCellRenderer1( ListCellRenderer<? super E> delegate,
                                          int... indexes )
  {
    Arrays.sort( indexes );
    this.delegate = delegate;
    this.indexes  = indexes;
  }

  @Override
  public Component getListCellRendererComponent( JList<? extends E> list, E value,
                                                 int index, boolean isSelected,
                                                 boolean cellHasFocus )
  {
    panel.removeAll();
    panel.add( delegate.getListCellRendererComponent( list, value, index,
                                                      isSelected, cellHasFocus ) );
    if ( Arrays.binarySearch( indexes, index ) >= 0 )
      panel.add( new JSeparator(), BorderLayout.PAGE_END );

    return panel;
  }
}

Die Implementierung basiert auf der Idee, jede Komponente in einen Container (JPanel) zu setzen und in diesem Container dann je nach Bedarf ein JSeparatorunten an diesem Container anzufügen. Statt JPanel mit einem JSeparator auszustatten, kann ebenfalls auch ein Border unten gezeichnet werden. Die AnweisungArrays.binarySearch(indexes, index) >= 0 ist als »contains« zu verstehen, also als ein Test, ob der Index im Feld ist – leider gibt es so eine Methode nicht in der Java API. Wenn der Index im Feld ist, soll der Separator unter der Komponente erscheinen. Dass sich eine Trennlinie auch am Anfang befinden kann, berücksichtigt die Lösung nicht; diese Variante bleibt als Übungsaufgabe für die Leser.

Diese Lösung ist einfach und funktioniert gut, denn vorhandene Renderer werden weiterverwendet, was sehr wichtig ist, denn größere Swing-Anwendungen nutzen viele eigene Renderer, etwa um Icons und Text zusammenzufassen. Ein Nachteil ist, dass der Separator zu einen Element gehört, und wenn das Element etwa in der Liste ausgewählt wird, steht der Separator mit in der Selektion und ist nicht abgetrennt (das ist aber bei Word genauso).

Während die vorgestellte Variante in der Praxis gut funktioniert, wollen wir uns noch mit einer alternativen Umsetzung beschäftigen. Sie ist deutlich komplexer und auch nicht so flexibel. Die Lösung basiert auf der Idee, dass die Modelldaten eine Markierung für den Separator enthalten – die folgende Lösung nutzt null dafür. DaJList oder JComboBox eine null problemlos verträgt, ist die Basis schnell umgesetzt:

JFrame frame = new JFrame();
frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
String[] items = { "Cambria", null, "Arial", "Cambria", "Verdana", "Times" };
JComboBox<String> comboBox = new JComboBox<String>( items );
frame.add( comboBox );
frame.pack();
frame.setVisible( true );

Wenn wir das Programm starten, zeigt es dort, wo das Model eine null liefert, einfach nichts an. Die Selektion dieses null-Elements ist auch möglich und führt zu keiner Ausnahme.

Damit Swing das null-Element nicht als Leereintrag anzeigt, verpassen wir unserer JComboBox einen Zellen-Renderer. Der soll immer dann, wenn das Element nullist, ein Exemplar von JSeparator zeichnen. Vom Design ist es das beste, wenn der Zell-Renderer selbst die null-Elemente darstellt, aber das Zeichnen der Nicht-null-Elemente an einen anderen Zell-Renderer abgibt, anstatt diese etwa durch einen BasicComboBoxRenderer (ein JLabel, das ListCellRendererimplementiert) selbst zu renderen – das würde die Flexibilität der Lösung massiv einschränken.

package com.tutego.insel.ui.list;

import java.awt.Component;
import javax.swing.*;

public class SeparatorAwareListCellRenderer2<E> implements ListCellRenderer<E>
{
  private final ListCellRenderer<? super E> delegate;

  public SeparatorAwareListCellRenderer2( ListCellRenderer<? super E> delegate )
  {
    this.delegate = delegate;
  }

  @Override
  public Component getListCellRendererComponent( JList<? extends E> list, E value,
                                                 int index, boolean isSelected,
                                                 boolean cellHasFocus )
  {
    if ( value == null )
      return new JSeparator();

    return delegate.getListCellRendererComponent( list, value, index, isSelected, cellHasFocus );
  }
}

Die eigene Klasse SeparatorAwareListCellRenderer2 bekommt einen anderen ListCellRenderer übergeben und liefert die Komponenten dieses Delegate-Renderers immer dann, wenn das Element ungleich null ist. Ist es dagegen gleich null, liefert getListCellRendererComponent() ein Exemplar des JSeparators.

Im Beispielprogramm muss der Renderer nun angemeldet werden, und dazu ist Folgendes zu ergänzen:

comboBox.setRenderer(
  new SeparatorAwareListCellRenderer2<String>(comboBox.getRenderer()) );

Nach dem Start des Programms ist das Ergebnis schon viel besser: Dort, wo vorher die JComboBox eine leere Zeile darstellte, ist nun ein Strich.

Die Lösung ist jedoch nur ein Etappensieg, denn die Navigation mit der Tastatur durch die Liste zeigt eine Schwachstelle: Das null-Element lässt sich auswählen und erscheint auch als Linie im Editor/Textfeld. Beheben lässt sich das Problem nicht mit dem Renderer, denn der ist nur mit der Darstellung in der Liste beauftragt. Hier muss in die Interna der Swing-Komponente eingegriffen werden. Jede Swing-Komponente hat ein korrespondierendes UI-Delegate, das für das Verhalten und die Darstellung der Komponente verantwortlich ist. Für die JComboBox sind das Unterklassen von ComboBoxUI, und zwei Methoden sind besonders interessant:selectNextPossibleValue() und selectPreviousPossibleValue().

Da jede UI-Implementierung ihr eigenes Look and Feel (LAF) mitbringt, müssen wir hier eigentlich einen Dekorator bauen und jede Methode bis auf die beiden genannten an das Original weiterleiten, doch das ist jetzt zu viel Arbeit, und so nehmen wir die Basisklasse WindowsComboBoxUI als Basisklasse, denn unser Beispiel nutzt das Windows-LAF. In der Unterklasse implementieren wir eigene Versionen der Methoden selectNextPossibleValue() undselectPreviousPossibleValue(), die so lange die Liste nach oben/unten laufen müssen, bis sie ein Element ungleich null finden.

package com.tutego.insel.ui.list;

import com.sun.java.swing.plaf.windows.WindowsComboBoxUI;

public class SeparatorAwareComboBoxUI extends WindowsComboBoxUI
{
  @Override
  protected void selectNextPossibleValue()
  {
    for ( int index = comboBox.getSelectedIndex() + 1;
          index < comboBox.getItemCount();
          index++ )
      if ( comboBox.getItemAt( index ) != null )
      {
        comboBox.setSelectedIndex( index );
        break;
      }
  }

  @Override
  protected void selectPreviousPossibleValue()
  {
    for ( int index = comboBox.getSelectedIndex() - 1;
          index >= 0;
          index-- )
      if ( comboBox.getItemAt( index ) != null )
      {
        comboBox.setSelectedIndex( index );
        break;
      }
  }
}

Das UI-Objekt muss ebenfalls im main()-Programm angemeldet werden:

comboBox.setUI( new SeparatorAwareComboBoxUI() );

Enthält die Liste null-Elemente, überspringt die Tastennavigation über die Cursor-Taste diese. Doch auch mit diesem Teilstück fehlt ein weiteres Detail: Mit einem Klick lässt sich die Linie doch noch auswählen. Das ist keine Frage des Renderers und auch keine Frage der Tastaturnavigation – es muss untersagt werden, dass bei der Aktivierung ein null-Element zum Editor kommen kann. Hier ist die Methode setSelectedItem() von JComboBox entscheidend. Denn jedes Element, das selektiert wird – und dadurch auch in das Textfeld kommt –, geht durch die Methode hindurch. Wenn wir die Methode überschreiben und bei null-Elementen einfach nichts tun, wird auch das null-Element nicht selektiert, und im Textfeld bleibt das letzte Element stehen.

Damit auch spezielle Implementierungen von JComboBox von diesem Verhalten profitieren können, müssen wir wieder einen Dekorator schreiben, doch das kostet zu viel Mühe, und so überschreibt eine einfache Unterklasse die setSelectedItem()-Methode. (Im Prinzip wäre auch eine überschriebene Methode vonsetSelectedIndex() sinnvoll, denn das könnte eine programmierte Aktivierung von null vermeiden.)

package com.tutego.insel.ui.list;

import javax.swing.*;

public class SeparatorAwareJComboBox<E> extends JComboBox<E>
{
  @SafeVarargs
  public SeparatorAwareJComboBox( E... items )
  {
    super( items );
  }

  @Override
  public void setSelectedItem( Object anObject )
  {
    if ( anObject != null )
      super.setSelectedItem( anObject );
  }
}

Im Hauptprogramm muss nur diese spezielle Klasse zum Einsatz kommen und so folgt:

package com.tutego.insel.ui.list;

import javax.swing.*;

public class JComboBoxWithSeparator2
{
  public static void main( String[] args ) throws Exception
  {
    UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
//    UIManager.setLookAndFeel( new NimbusLookAndFeel() );

    JFrame frame = new JFrame();
    frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
    String[] items = { "Cambria", null, "Arial", "Verdana", "Times" };
    JComboBox<String> comboBox = new SeparatorAwareJComboBox<>( items );
    comboBox.setRenderer( new SeparatorAwareListCellRenderer2<>(comboBox.getRenderer()) );
    comboBox.setUI( new SeparatorAwareComboBoxUI() );
    comboBox.setSelectedIndex( 1 );
    frame.add( comboBox );
    frame.pack();
    frame.setVisible( true );
  }
}

Inselraus: Variablen mit Xor vertauschen

Eine besonders trickreiche Idee für das Vertauschen von Variableninhalten arbeitet mit dem Xor-Operator und benötigt keine temporäre Zwischenvariable. Die Zeilen zum Vertauschen von x und y lauten wie folgt:

int x = 12, 
  
    y = 49;
x ^= y; // x = x ^ y = 001100bin ^ 110001bin = 111101bin
y ^= x; // y = y ^ x = 110001bin ^ 111101bin = 001100bin
x ^= y; // x = x ^ y = 111101bin ^ 001100bin = 110001bin
System.out.println( x + " " + y );  // Ausgabe ist: 49 12

Der Trick funktioniert, da wir mit Xor etwas »hinein- und herausrechnen« können. Zuerst rechnet die erste Zeile das y in das x. Wenn wir anschließend die Zuweisung an das y machen, dann ist das der letzte schreibende Zugriff auf y, also muss hier schon das vertauschte Ergebnis stehen. Das stimmt auch, denn expandieren wir die zweite Zeile, steht dort: »y ^ x wird zugewiesen an y«, und dies ist y ^ (x ^ y). Der letzte Ausdruck verkürzt sich zu y = x, da aus der Definition des Xor-Operators für einen Wert a hervorgeht: a ^ a = 0. Die Zuweisung hätten wir zwar gleich so schreiben können, aber dann wäre der Wert von y verloren gegangen. Der steckt aber noch in x aus der ersten Zuweisung. Betrachten wir daher die letzte Zeile x ^ y: y hat den Startwert von x, doch in x steckt ein Xor-y. Daher ergibt x ^ y den Wert x ^ x ^ y, und der verkürzt sich zu y. Demnach haben wir den Inhalt der Variablen vertauscht. Im Übrigen können wir für die drei Xor-Zeilen alternativ schreiben:

y ^= x ^= y;   // Auswertung automatisch y ^= (x ^= y) 
  
x ^= y;

Da liegt es doch nahe, die Ausdrücke weiter abzukürzen zu x ^= y ^= x ^= y. Doch leider ist das falsch (es kommt für x immer null heraus). Motivierten Lesern bleibt dies als Denksportaufgabe überlassen.

GMail-URLs generieren

private static String createGMailUrl( String to, String cc, String subject, String body )
{
  StringBuilder result = new StringBuilder( "https://mail.google.com/mail/?view=cm&tf=0" );
  
  if ( to != null && ! to.isEmpty() )
    result.append( "&to=" ).append( to );
  if ( cc != null && ! cc.isEmpty() )
    result.append( "&cc=" ).append( cc );
  if ( subject != null && ! subject.isEmpty() ) {
    try {
      result.append( "&su=" ).append( encode( subject ) );
    }
    catch ( UnsupportedEncodingException e ) {
      e.printStackTrace();
    }
  }
  if ( body != null && ! body.isEmpty() ) {
    try {
      result.append( "&body=" ).append( encode( body ) );
    }
    catch ( UnsupportedEncodingException e ) {
      e.printStackTrace();
    }
  }
  return result.toString();
}
private static String encode( String param ) throws UnsupportedEncodingException
{    
  byte[] utf8 = param.getBytes( "utf-8" );
  StringBuilder result = new StringBuilder( param.length() * 3 );
  for ( int i = 0; i < utf8.length; i++ )
  {
    String c = Integer.toString( utf8[ i ] & 0xFF, 16 );
    result.append( '%' ).append( c.length() == 1 ? "0" + c : c );
  }
  return result.toString();
}

Wanted: Native English speaker for proofreadig my ebook about Google Guava I/O

Seit etwa einem Jahr schreibe ich unregelmäßig an Tutorials zu Google Guava. Das Paket com.google.common.io ist nahezu komplett dokumentiert und Schritt für Schritt taste ich mich an die fast 300 Typen der Google Bibliothek ran. Der I/O Teil fasst etwa 50 Seiten und ich möchte dieses Bündel als ebook veröffentlichen. Da mir mein englisch Geschriebenes holprig und nicht flüssig vorkommt, suche ich einen Java-affinen englische Muttersprachler, der/die etwas über Google Guava IO lernen möchte bzw. im Idealfall selbst Erfahrung mit der Lib gesammelt hat und Details einstreuen kann. Interessenten sollten sich bitte unter ullenboom<at>gmail<dot>com melden. Danke.

Nach meiner jetzigen Planung werden es drei ebooks:

  • I/O and Networks
  • Basic Utilities and Java SE Extensions
  • Common Collection

Die Dokumentation zu com.google.common.base und com.google.common.primitives ist zu 80 % abgeschlossen, sodass ich denke, das zweite ebook auch noch in diesem Jahr fertigstellen zu können.

JDOM 2.0.1 freigegeben

http://www.jdom.org/news/index.html. Von der Webseite:

04.28.2012: JDOM 2.0.1 Released!

JDOM 2.0.1 is here!

JDOM 2.0.1 Introduces official support for Android! See the JDOM and Android page. JDOM 2.0.1 also fixes a bug in the ‚Compact‘ output of XML.

Get JDOM 2.0.0 Here! or from the maven-central repository here: Group: org.jdom, Articact: jdom

04.08.2012: JDOM 2.0.0 Released!

JDOM 2.0.0 is here!

JDOM 2.0.0 brings JDOM in to the world of Generics and other Java language items introduced with Java 5. As a result, JDOM 2.0.0 requires Java 5 or later, but is only fully supported on Java 6 and later.

Get JDOM 2.0.0 Here! or from the maven-central repository here: Group: org.jdom, Articact: jdom

In der Insel habe ich meine Programme nun auf JDOM 2 gebracht – ohne große Probleme. Nur die Generics muss ich in der Doku dokumentieren. Das einzige, was ich umschreiben musste, was das Kapitel über XPath, dazu gleich ein eigener Beitrag.

Inselraus: identityHashCode() ist für eine System-eindeutige Objekt-IDs nicht geeignet

Stellen wir uns vor, wir hätten eine Objekthierarchie im Speicher, die zum Beispiel jeder Socke einen Besitzer zuspricht. Wenn wir im Speicher Assoziationen abbilden, dann sollen diese Verweise auch noch nach dem Tod des Programms überleben. Eine Lösung ist die Serialisierung, eine andere eine Objekt-Datenbank oder aber auch eine XML-Datei. Doch überlegen wir selbst, wo bei der Abbildung auf eine Datenbank oder eine Datei das Problem besteht. Zunächst stehen ganz unterschiedliche Objekte mit ihren Eigenschaften im Speicher. Das Speichern der Zustände ist kein Problem, denn nur die Attribute müssten abgespeichert werden. Doch wenn ein Objekt auf ein anderes verweist, muss dieser Verweis gesichert werden. Aber in Java ist ein Verweis durch eine Referenz gegeben, und was sollte es da zu speichern geben? Eine Lösung für das Problem ist, jedem Objekt im Speicher einen Zähler zu geben und beim Speichen etwa zu sagen: »Der Besitzer 2 kennt Socke 5«.

Der Identifizierer für die Objekte muss eindeutig sein, und wir können überlegen, System.identityHashCode() zu nutzen. In der Implementierung der virtuellen Maschine von Oracle geht in den Wert von identityHashCode() die Information über den wahren Ort des Objekts im Speicher ein. Bei einer 64-Bit-Implementierung würden auch 32 Bit abgeschnitten, und die Eindeutigkeit wäre somit automatisch nicht mehr gewährleistet. Ein weiteres Problem besteht darin, dass zwar die Implementierung von Oracles identityHashCode() auf die eindeutige Objektspeicheradresse abbildet, aber dass das nicht jeder Hersteller so machen muss. Damit ist identityHashCode() nicht überall gesichert unterschiedlich. Zudem ist es prinzipiell denkbar, dass die Speicherverwaltung die Objekte verschiebt. Was sollte identityHashCode() dann machen? Wenn die neue Speicheradresse dahinter steckt, würde sich der Hashcode ändern, und das darf nicht sein. Es käme ebenfalls zu einem Problem, wenn mehr als Integer.MAX_INTEGER viele Objekte im Speicher stünden. (Doch wenn wir uns die große Zahl 2^32 = 4.294.967.296 vor Augen halten, dann es ist unwahrscheinlich, dass sich mehr als 4 Milliarden Objekte im Speicher tummeln. Zudem bräuchten wir 4 Gigabyte Speicher, wenn jedes Objekt auch nur 1 Byte kosten würde.)

Es ist gar nicht so schwierig, zwei unterschiedliche Objekte mit gleichen identityHashCode()-Resultat zu bekommen. Wir erzeugen ein paar String-Objekte und testen, jedes mit jedem, ob identityHashCode() den gleichen Wert ergibt:

String[] strings = new String[5000];
for ( int i = 0; i < strings.length; i++ )
  strings[i] = Integer.toString( i );
int cnt = 0;
for ( int i = 0; i < strings.length; i++ ) {
  for ( int j = i + 1; j < strings.length; j++ ) {
    int id1 = System.identityHashCode( strings[i] );
    int id2 = System.identityHashCode( strings[j] );
    if ( id1 == id2 ) {
      out.println( "Zwei Objekte mit identityHashCode() = " + id1 );
      out.println( " Objekt 1: \"" + strings[i] + "\"" );
      out.println( " Objekt 2: \"" + strings[j] + "\"" );
      out.println( " Object1.hashCode(): " + strings[i].hashCode() );
      out.println( " Object2.hashCode(): " + strings[j].hashCode() );
      out.println( " Object1.equals(Object2): " + strings[i].equals( strings[j] ) );
      cnt++;
    }
  }
}
System.out.println( cnt + " Objekte mit gleichem identityHashCode() gefunden." );

Ein Durchlauf bringt schnell Ergebnisse wie:

Zwei Objekte mit identityHashCode() = 9578500
Objekt 1: "541"
Objekt 2: "2066"
Object1.hashCode(): 52594
Object2.hashCode(): 1537406
Object1.equals(Object2): false
Zwei Objekte mit identityHashCode() = 14850080
Objekt 1: "2085"
Objekt 2: "2365"
Object1.hashCode(): 1537467
Object2.hashCode(): 1540288
Object1.equals(Object2): false
2 Objekte mit gleichem identityHashCode() gefunden.

Das Ergebnis ist also, dass identityHashCode() nicht sicher bei der Vergabe von Identifizierern ist. Um wirklich allen Problemen aus dem Weg zu gehen, ist ein Zählerobjekt oder eine ID über zum Beispiel die Klasse java.util.UUID nötig.

JavaFX 2.1 ist fertig

Download unter http://www.oracle.com/technetwork/java/javafx/downloads/index.html. Aus den http://docs.oracle.com/javafx/release-documentation.html:

  • Media H.264 and AAC support

  • Mac OS X support

    Applications must be packaged for the desktop, Web and Web Start applications are not yet supported.

  • LCD text

  • UI enhancements, including controls for Combo Box, Stacked Chart, and application-wide menu bar

  • Webview to support JavaScript to Java method calls

In GWT global auf Tastenkürzel reagieren

Es setzt Strg + F den Fokus auf das Suchfeld:

Event.addNativePreviewHandler( new NativePreviewHandler() {
  @Override public void onPreviewNativeEvent( NativePreviewEvent event ) {
    NativeEvent ne = event.getNativeEvent();
    if ( event.getTypeInt() == Event.ONKEYDOWN && ne.getCtrlKey() && ne.getKeyCode() == ‚F‘) {
      ne.preventDefault();
      searchTextBox.setFocus( true );
    }
  }
} );

Osterrätsel (C, nicht Java)

Linus Torwalds stellt in seinem G+ Stream zwei C-Ausdrücke vor:

/* Modified Carl Chatfield G+ version for 32-bit */
long a = (mask-256) >> 23;
long b = mask & 1;
return a + b + 1;

/* Jan Achrenius on G+ for 64-bit case */
return mask*0x0001020304050608 >> 56;

Die Frage für Rätselfreunde ist (ohne den G+-Beitrag gelesen zu haben): Was berechnen die Ausdrücke?

Jerry: ein jQuery in Java

Jerry ist eine Open-Source-Lib, die eine von jQuery bekannte Funktionalität in Java abbildet. Beispiele von der Webseite: http://jodd.org/doc/jerry/index.html.

import static jodd.lagarto.dom.jerry.Jerry.jerry;

...

Jerry doc = jerry(html);

doc.$("div#jodd p.neat").css("color", "red").addClass("ohmy");

und:

doc.$("select option:selected").each(new JerryFunction() {

public boolean onNode(Jerry $this, int index) {

str.append($this.text()).append(' ');

return true;

}

});

Statt aus einem String kann die Eingabe auch direkt aus der Datei kommen:

File file = new File(SystemUtil.getTempDir(), "allmusic.html");

NetUtil.downloadFile("http://allmusic.com", file);

// create Jerry, i.e. document context

Jerry doc = Jerry.jerry(FileUtil.readString(file));

// parse

doc.$("div#new_releases div.list_item").each(new JerryFunction() {

public boolean onNode(Jerry $this, int index) {

System.out.println("-----");

System.out.println($this.$("div.album_title").text());

System.out.println($this.$("div.album_artist").text().trim());

return true;

}

});

Apache POI 3.8 ist fertig

Nach genau einem Jahr Arbeit an dem Release 3.8 ist die neue Version von Apache POI veröffentlicht. Die Änderungen gibt es unter http://poi.apache.org/changes.html, Download unter http://poi.apache.org/download.html. Links zu den Component APIs: