2012-01-24

Hallo Welt

Sie erinnern sich sicher, dass ich die Euphorie des Java Magazins bzgl. JavaFX 2.0 nicht 100% geteilt habe. Das bedeutet aber natürlich nicht, dass man sich diese Technologie nicht ansehen sollte – im Gegenteil. Heute also ein klein wenig Abwechslung. Kein Swing, kein Android, sondern RIA.

Oracle bietet JavaFX in den unterschiedlichsten Paketen an, Huckepack mit dem JDK 7, als Bundle mit NetBeans oder als “einfaches” SDK. Um nur etwas mit JavaFX zu spielen, sollten Sie diese Variante wählen. Laden Sie von Oracles Download-Seite das JavaFX SDK herunter und folgen den wenigen Schritten des Installationsassistenten.

JavaFX SDK 2.0 Installationsassistent
JavaFX SDK 2.0 Installationsassistent

Nachdem die Installation abgeschlossen wurde, finden Sie unter C:\Program Files\Oracle (bzw. den von Ihnen gewählten Pfad) die beiden Verzeichnisse JavaFX 2.0 Runtime und JavaFX 2.0 SDK. Uns interessiert das SDK, insbesondere der Unterordner rt. Er enthält bin und lib. Letzteres beherbergt die Datei jfxrt.jar, ersteres einige .dlls. Um eine JavaFX 2.0-Anwendung zu bauen, müssen Sie nur das .jar-Archiv dem Klassenpfad hinzufügen und sicherstellen, dass die nativen Bibliotheken geladen werden können.

Unter Eclipse machen Sie diese Einstellungen auf der Registerkarte Libraries der Seite Java Build Path des Projekteigenschaften-Dialogs eines ganz normalen Java-Projekts. Sie erreichen sie durch Rechtsklick auf die Projektwurzel im Package Explorer und Auswahl des Menüpunks Build Path • Configure Build Path. Klicken Sie auf Add External JARs und suchen jfxrt.jar. Markieren Sie Native library location und klicken anschließend auf Edit. Mit External Folder öffnen Sie die Ordnerauswahl, in der Sie das weiter oben angesprochene bin-Verzeichnis mit den nativen Bibliotheken suchen und auswählen müssen. In einem letzten Schritt sollten Sie noch das Verzeichnis mit der API-Dokumentation setzen. Klicken Sie hierzu auf Javadoc location und anschließend erneut auf Edit. Sie benötigen den Unterordner api des doc-Verzeichnisses. Bei mir lautet der Pfad C:\Program Files\Oracle\JavaFX 2.0 SDK\docs\api.

Der Projekteigenschaften unter Eclipse mit dem Java Build Path
Der Projekteigenschaften unter Eclipse mit dem Java Build Path

Fügen Sie Ihrem auf diese Weise konfigurierten Java-Projekt die folgende Klasse HelloWorld hinzu:

import javafx.application.Application;
import javafx.geometry.VPos;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.effect.Reflection;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.StrokeType;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Stage;

public class HalloWelt extends Application {

  public static void main(String[] args) {
    launch(args);
  }

  @Override
  public void start(Stage primaryStage) throws Exception {
    primaryStage.setTitle("Hallo Welt");
    Group group = new Group();
    Scene scene = new Scene(group);
    // anzuzeigender Text...
    Text text = new Text("Hallo Welt!");
    text.setTextOrigin(VPos.TOP);
    // Font
    text.setFont(new Font(72));
    // anzeigen
    group.getChildren().add(text);
    primaryStage.setScene(scene);
    primaryStage.show();
  }
}

Wenn Sie das Programm starten, erscheint folgendes Fenster:

Hallo Welt-Basisversion
Hallo Welt-Basisversion

Fügen Sie vor dem Kommentar // anzeigen die folgenden Zeilen ein:

    // Effekt
    text.setEffect(new Reflection());

Hallo Welt mit einer Spiegelung als Effekt
Hallo Welt mit einer Spiegelung als Effekt

Die folgenden Zeilen produzieren eine blaue Umrisslinie. Fügen Sie Sie nach dem Aufruf von setEffect() ein.

    // Linie
    text.setStrokeType(StrokeType.OUTSIDE);
    text.setStroke(Color.BLUE);
    text.setStrokeWidth(2);

Hallo Welt mit zusätzlicher blauer Umrisslinie
Hallo Welt mit zusätzlicher blauer Umrisslinie

Zu guter Letzt füllen wir die Schrift – fügen Sie das folgende Fragment nach dem Aufruf von setStrokeWidth() ein.

    // Füllung
    text.setFill(new LinearGradient(0f, 1f, 1f, 0f, true,
        CycleMethod.NO_CYCLE, new Stop[] {
            new Stop(0, Color.web("#f8bd55")),
            new Stop(0.14, Color.web("#c0fe56")),
            new Stop(0.28, Color.web("#5dfbc1")),
            new Stop(0.43, Color.web("#64c2f8")),
            new Stop(0.57, Color.web("#be4af7")),
            new Stop(0.71, Color.web("#ed5fc2")),
            new Stop(0.85, Color.web("#ef504c")),
            new Stop(1, Color.web("#f2660f")), }));

Hallo Welt ganz bunt
Hallo Welt ganz bunt

2012-01-22

Ultimate Swing, Teil 18

Im vorangehenden Beitrag hatte ich Ihnen das Business Delegate Muster nahegelegt und eine Beispiel-Implementierung gezeigt. Es gibt Ihnen die Möglichkeit, die Präsentationsschicht fachlich anders zu strukturieren, als es die Geschäftslogik (das Backend) vielleicht vorsieht. Dies ist unter anderem der Fall, wenn Ihre Anwendung eine Art Mashup realisiert, also die Ergebnisse verschiedener Serviceaufrufe kombiniert.

Achtung: Es ist natürlich nicht originäre Aufgabe der Präsentationsschicht, Services zu orchestrieren. In einer Enterpriseanwendung würde selbstverständlich das Backend dies übernehmen. Mein Szenario geht davon aus, dass Sie keine klassische verteilte Anwendung bauen möchten, sondern “nur” externe Dienste nutzen (also eigentlich einen Fat Client coden).

Fazit: Der Business Delegate kommt aus der Enterprise-Welt, kann uns aber auch bei klassischen Desktop-Anwendungen helfen, sofern diese fremde Services nutzen. Dann fungiert er für uns als eine Art Übersetzer, der die Daten für das eigene Programm anpasst.

Sind Sie dem Link etwas weiter oben gefolgt? Falls nicht, sollten Sie dies nun nachholen und nach der Lektüre wieder hier aufschlagen.
Smiley

Wie Sie nun wissen, nutzt der Business Delegate weitere core J2EE Patterns. Falls dieses Buch noch nicht in Ihrem Regal steht, sollten Sie es auf jeden Fall kaufen. Zugegebenermaßen ist das Werk nicht mehr taufrisch, aber es ist auch heute noch eine Quelle der Inspiration. Einige Muster haben an Bedeutung verloren oder werden heute überhaupt nicht mehr eingesetzt, andere sind aktuell wie immer. Zum Beispiel der Service Locator. Er verbirgt die Implementierung der Suche nach Services und Komponenten und stellt (zum Beispiel einem Business Delegate) eine einheitliche Zugriffsschicht auf Services zur Verfügung. Die folgende Implementierung ist insofern ein klein wenig untypisch, als Sie keine JEE-typischen Lookup-Strategien entdecken werden. Das Vorgehen (zum Beispiel Realisierung als Singleton) ist aber identisch.

public class ServiceLocator {

  private static final ServiceLocator INSTANCE = new ServiceLocator();

  private CategoryService categoryService;
  private TasksService tasksService;

  private ServiceLocator() {
    categoryService = null;
    tasksService = null;
  }

  public static ServiceLocator getInstance() {
    return INSTANCE;
  }

  public synchronized CategoryService getCategoryService() {
    if (categoryService == null) {
      categoryService = new LocalCategoryService();
    }
    return categoryService;
  }

  public synchronized TasksService getTasksService() {
    if (tasksService == null) {
      tasksService = new LocalTasksService();
    }
    return tasksService;
  }
}

Die Nutzung des Service Locators durch den Business Delegate CategoryDelegate sieht folgendermaßen aus:

  private CategoryService categoryService;

  public CategoryDelegate() {
    categoryService = ServiceLocator.getInstance().getCategoryService();
  }

Über die Vorteile der Nutzung eines Service Locators ist viel geschrieben worden. Das für mich überzeugendste Argument, ihn zu nutzen, ist die leichte Austauschbarkeit der Serviceimplementierung. Wie Sie gesehen haben, liefert meine Implementierung beim Aufruf von getCategoryService() ein Objekt des Typs LocalCategoryService. Wenn wir später auf Google Konten umsteigen, wird hier einfach ein anderes Objekt geliefert. Wichtig ist nur, dass alle Implementierungen eine gemeinsame Schnittstelle haben. Diese wird üblicherweise durch ein Interface beschrieben, beispielsweise CategoryService. Gerade bei echten verteilten Anwendungen ist diese Austauschbarkeit ein Segen – solange die Serviceimplementierung selbst noch im Gange ist, kann die Präsentationsschicht gegen Mocks arbeiten.

2012-01-19

Ultimate Swing, Teil 17

Kaum ist der Urlaub beendet, werden die Abstände zwischen zwei Posts wieder länger… Immerhin hatten Sie auf diese Weise Gelegenheit, wieder etwas aufzuholen. Haben Sie noch Spaß an der Serie? Zu erzählen gibt es noch sehr viel.

Zum Beispiel über Schnittstellen. Wie Sie wissen, greift Notes und Tasks derzeit auf das lokale Dateisystem zu und liest und schreibt XML-Dateien. Später werden wir das aber gegen einen Zugriff auf Google Konten austauschen. Damit dies keinen Einfluss auf die Implementierung hat, orientieren wir uns an bewährten Mustern aus der Java EE-Programmierung.

Bei mehrschichtigen Anwendungen erfolgen die Methodenaufrufe zum Speichern und Auslesen von Daten über mehrere Schichten hinweg. Um die Präsentationsschicht von der Geschäftslogik zu entkoppeln, wird gerne das Business Delegate Muster verwendet. Eine zu starke Kopplung zwischen den beiden Schichten kann nämlich zu verschiedenen Problemen führen. Beispielsweise erschwert sie die Wartung oder Weiterentwicklung der verschiedenen Schichten. Außerdem sind einzelne Komponenten schwerer austauschbar. Schließlich ist die Präsentationsschicht labil gegenüber größeren Veränderungen in der Geschäftslogikschicht:  diese führen oft zu ungewollten Anpassungen in der Präsentationsschicht. Um das zu vermeiden, kapselt ein Business Delegate interne Details der Implementierung der Geschäftslogik (z. B. JNDI-Lookup, RMI oder Zugriff auf EJBs) und verbirgt sie vor der Präsentationsschicht. Frei übersetzt heißt dies: egal ob wir auf eine lokale Eigenimplementierung oder Webservices zugreifen – die Sicht der Präsentationsschicht bleibt die gleiche.

Der in Zusammenhang mit dem Business Delegate oft genannte Vorteil, dass die Präsentationsschicht leichter ausgetauscht werden kann, interessiert uns hier natürlich nicht. Wir wollen ja stattdessen das Backend austauschen. Uns geht es um die Stabilität der Schnittstelle. Der Business Delegate übernimmt für uns die Rolle eines Übersetzers, der die Welt des Backends für unsere Präsentationsschicht übersetzt.

Hier eine bewusst freie Umsetzung des Musters. Sie stellt die drei Methoden save(), delete() und getAll() zur Verfügung.

public class TasksDelegate {

  private TasksService tasksService;

  public TasksDelegate() {
    tasksService = ServiceLocator.getInstance().getTasksService();
  }

  public Task[] getAll() {
    return tasksService.getAll();
  }

  public boolean save(Task task) throws IOException {
    return tasksService.save(task);
  }

  public boolean delete(Task task) {
    return tasksService.delete(task);
  }
}

Ist Ihnen aufgefallen, dass die Implementierung weitere Muster bemüht? Bleiben Sie dran.

UPDATE: Ich muss Ihnen unbedingt noch einen Teaser zeigen.

Neue Funktion: alphabetisch oder nach Fälligkeitsdatum sortieren
Neue Funktion: alphabetisch oder nach Fälligkeitsdatum sortieren

2012-01-10

Ultimate Swing, Teil 16

Heute ist wieder Quelltext-Tag. Ich habe die letzten Urlaubstage genutzt und noch ein paar Funktionen hinzugefügt. Man kann nun erledigte Aufgaben ausblenden. Wie das aussieht, zeigt der folgende Screenshot:

Erledigte Aufgaben können ausgeblendet werden
Erledigte Aufgaben können ausgeblendet werden

Wenn Sie sich die Größe der Archive ansehen, fällt sofort auf, dass mittlerweile eine ganze Menge zusammen gekommen ist. In den folgenden Posts werden wir uns etwas Gedanken über die Architektur machen.

Quelltexte zu TKNotesAndTasks vom 10.01.2012

2012-01-08

Ultimate Swing, Teil 15

Im vorangehenden Post hatte ich angedeutet, dass man Aufgaben mit einem Fälligkeitsdatum versehen kann. Als kleinen Appetithappen hier schon einmal ein paar Screenshots:

Hauptfenster von Notes and Tasks
Hauptfenster von Notes and Tasks

Auswählen des Fälligkeitsdatums
Auswählen des Fälligkeitsdatums

Das Fälligkeitsdatum wird – sofern es gesetzt wurde – unterhalb der Aufgabenbeschreibung ausgegeben. Um die Listenansicht noch besser zu strukturieren, werden Kategorien ab sofort unterstrichen.

Die Kalenderkomponente kann übrigens eine ganze Menge. Ich habe hier von Microsoft inspirieren lassen. Ausführliche Infos finden Sie in einem älteren Post von mir.

Unnötige Fehler

Was ist an folgendem Quelltextfragment falsch?

-->
  private static final ResourceBundle b = ResourceBundle
      .getBundle("com.thomaskuenneth.swing.datepicker.resources.DatePicker");

So richtig falsch (ich mag solche Formulierungen) ist daran gar nichts. Dennoch ist es Code, den Sie so nicht schreiben sollten. Zum einen wurde die Variable b kleingeschrieben. Das ist unschön, weil sie den Charakter einer Konstanten hat. Zum anderen – und das ist der wirklich wichtige Teil – ist das Fragment wartungsunfreundlich. Ich bin darüber gestolpert, als ich die Datumsauswahl meiner TKSwingComponents in Notes and Tasks übernehmen wollte. Um sie in die Schuhschachtel legen zu können, musste ich die Paketstruktur der Datumsauswahl anpassen. Das führte natürlich dazu, dass die benötigten Ressourcen nicht mehr gefunden wurden.

-->
  private static final ResourceBundle B = ResourceBundle
      .getBundle(DatePicker.class.getPackage().getName() + ".resources."
          + DatePicker.class.getSimpleName());

Die Ressourcen werden in dieser zweiten Fassung an einem anderen Ort gesucht. resources liegt nicht mehr unterhalb von datepicker, sondern im Paket der Klasse DatePicker. Das Ablegen von Ressourcen in einem entsprechenden Unterverzeichnis ist natürlich willkürlich. Nach einem automatisierten Refactoring funktioniert aber alles weiterhin ohne Probleme.

2012-01-05

Java auf dem Desktop

Das Java Magazin titelt in der Ausgabe  2.2012:
Java Desktop wachgeküsst
Meine spontane Reaktion, ich gebe es zu, war “Glauben die das wirklich?” Der Auslöser für die Wiedergeburt soll JavaFX 2.0 sein. Und die ehrlichen Bemühungen, die Oracle in diese Technik investiert. Dabei ist es vollkommen unerheblich, ob JavaFX ein nur wertiger Nachfolger von Swing ist und ob man damit tolle Apps bauen kann, oder ob damit eine neue Generation von UI Toolkits eingeläutet wird. Entscheidend allein ist, ob sich überhaupt noch jemand für klassische Desktop-Anwendungen interessiert. Ich will nun wirklich kein Wegbereiter für eine Webifizierung des Desktops sein. Ich glaube fest daran, dass “echte” Anwendungen weiterhin eine Berechtigung haben. Welcher Technologiestapel für deren Entwicklung verwendet wird, ist natürlich eine ganz andere Geschichte.
Lehnen wir uns also einmal zurück und sehen uns den Desktop im Jahr 2012 etwas genauer an. Da ist zunächst Apple. Cupertino wird zwar Mac OS X und iOS Stück für Stück einander annähern, aber bis auf weiteres setzt man umfassend auf klassische Mac-Apps. Immerhin hat Apple einen eigenen Mac App Store eröffnet und nutzt diesen sogar als Vertriebskanal für das Betriebssystem. Java wird indes mehr und mehr zu einer Randerscheinung auf dem Mac. Zum einen hinkt die Bereitstellung aktueller Java-basierter Technologien (zum Beispiel Java SE 7, aber auch JavaFX 2.0) den anderen Welten hinterher, zum anderen dürfen (zumindest Stand Januar 2012) keine Java-Anwendungen über den Cupertino-Store vertrieben werden. Selbst wenn in der Vergangenheit der Mac für Entwickler von Java-Anwendungen interessant war, so dürften die skizzierten Rahmenbedingungen die Plattform auf Dauer weniger attraktiv machen.
Die nächste bedeutende Plattform, Linux, ist zumindest was den Desktop angeht, von Heterogenität geprägt. Die auf dem Mac bis jetzt mögliche nahtlose Integration von Java-Anwendungen in das Wirtssystem war hier leider nie Realität. Auch wenn es bislang keinen bedeutenden Linux App Store gibt, so ist doch das Konzept einer zentralen Distributionsplattform viel älter (…als Apples App Stores…) – zumindest dann, wenn es ein Programm in das Repository einer der großen Distributionen schafft.
Jetzt zu Windows. Mit Windows 8 steht nicht nur Nutzern, sondern auch Entwicklern, ein Paradigmenwechsel ins Haus. Wie genau sich Microsoft die Oberfläche vorstellt, ließ die Windows 8-Preview nur erahnen. Ob die Welt dann wirklich nur noch aus kleinen bunten Kacheln besteht, wird sich zeigen. Offiziell verkündet wurde auch der neue Store, den Redmond als zentralen Vertriebsweg für Programme etablieren möchte. Ob man darüber später auch ein Office kaufen kann, bleibt bis auf weiteres unklar. Ebenso, ob sich Office bald dem Metro Style angleichen kann. Für Java-Entwickler ergeben sich daraus eine ganze Reihe von Fragen:
  • Wie passen die JRE und ggf. JavaFX in das neue Vertriebskonzept?
  • Finden Swing und ggf. JavaFX Zugang zu dem neuen UI von Windows 8 oder sind sie in der alten Desktopwelt von Windows 7 gefangen?
  • Darf man Java-Apps über den Microsoft Store vertreiben?
Oracle wirbt damit, dass Java auf mehreren Milliarden Geräten läuft. Es ist schade, dass man Suns Project Vector nicht weiter verfolgt hat. Das Java Warehouse hätte viel wahrscheinlicher zu einer Wiederbelebung von Java auf dem Desktop geführt, als das sicher gut gemeinte JavaFX 2.0.

Ultimate Swing, Teil 14

Diesmal ist wieder Quelltexttag… Der folgende Screenshot entstand mit den Quelltexten aus dem am Ende des Posts verlinkten Archiv. Vielleicht fragen Sie sich, warum ich das Programm mit einer englischen Benutzeroberfläche versehe. Internationalisierung ist ein Schlüsselmerkmal moderner Anwendungen. In einer späteren Folge werden wir uns ansehen, wie im Detail die automatische Versorgung von Swing-Komponenten mit Texten in der Sprache des Anwenders funktioniert. Dabei werden wir auch feststellen, dass Übersetzungen ohne unmittelbare Änderungen an Klassen erstellt werden können. Unabhängig davon, wie viele Übersetzungen Sie anbieten möchten, sollten Sie sich aber für eine Standardsprache entscheiden. Natürlich kann dies auch Deutsch sein. Ich rate allerdings zu Englisch, weil Sie auf diese Weise viel mehr Nutzer erreichen, für deren Sprache Sie keine explizite Übersetzung anbieten.

Screenshot von Notes and Tasks
Screenshot von Notes and Tasks

Quelltexte zu TKNotesAndTasks vom 05.01.2012

2012-01-03

Gadgets, Gadgets, Gadgets

Noch ist die Berichterstattung über neue Gadgets, die wir dieses Jahr erwarten dürfen, verhalten. Das wird aber nicht mehr lange so bleiben. Denn vom 10. bis 13. Januar findet die CES statt. Gespannt sein darf man auf die Beta von Windows 8. Neugierig bin ich auch, ob der Tablet-Boom weiter anhält.
Das nächste Ereignis wird dann wohl Ende Januar stattfinden. Es häufen sich die Meldungen über Gerüchte (Himmel, das ist eine Formulierung :-)), dass Apple wohl bald Einladungen versenden werde. Ob Cupertino dann (schon) ein iPad 3 oder doch ein Fernsehgerät präsentiert, wird sicher bis zur letzten Minute offen bleiben. Auch der Februar wird nicht ohne Gadgetmania auskommen, denn vom 27. Februar bis 1. März treffen sich in Barcelona wieder die Mobilgrößen zu ihrem jährlichen Stelldichein.
Dass ich mir an dieser Stelle etwas mehr Ruhe wünschen würde, habe ich ja schon geschrieben. So kommen wird es aber sicher nicht. Microsoft wird sicher bald eine neue Version von Windows Phone vorstellen. Spannend ist in diesem Zusammenhang die Frage, wann ein richtiges Windows (8) auf Smartphones läuft. Und Android? In Barcelona wird man sicher viele Android 4-Geräte sehen. Ob Google dann auch schon den Ring frei für Jelly Bean gibt, muss sich zeigen. Ich hoffe inständig, sie lassen sich zumindest bis zur nächsten IO Zeit. Die findet vom 27. bis 29. Juni statt.
...genießen Sie die ruhige Zeit.

2012-01-02

Ultimate Swing, Teil 13

Im 10. Teil habe ich das Thema fachliche Transaktionen gestreift. Wie man technisch damit umgeht, hatte ich seinerzeit offen gelassen. In diesem Post wollen wir darauf zurückkommen. Sehen Sie sich als Einstieg bitte das folgende Codefragment an:

          tasksView.clearSelection();
          for (Task task : tasks) {
            new DeleteTaskWorker(task, tasksModel, tasksDelegate)
                .execute();
          }
          updateCategories();

In einer Schleife werden Worker instantiiert und ausgeführt. Das Löschen besteht aus einem Serviceaufruf sowie im Erfolgsfall der Manipulation eines Bedienelements (Entfernen aus der Liste). Die Methode updateCategories() zählt, wie oft Kategorien in Aufgaben verwendet werden. Das darf sie natürlich erst, wenn alle Löschoperationen abgeschlossen wurden. Dumm nur, dass obiger Quelltext dies nicht garantiert… Die Lösung des Problems besteht darin, solange zu warten, bis alle Worker ihre Arbeit abgeschlossen haben, und erst danach die Methode aufzurufen.

Wie nicht anders zu erwarten, hält Java (mindestens) eine Lösung in den unendlichen Weiten seiner Klassenbibliothek parat: java.util.concurrent.CountDownLatch. Diese Synchronisierungshilfe erlaubt einem oder mehreren Threads so lange zu warten, bis eine bestimmte Anzahl an Operationen in anderen Threads vollständig abgearbeitet wurde. Hierzu wird ein CountDownLatch mit der Anzahl der Operationen initialisiert. Durch den Aufruf der Methode await() wartet der Aufrufer-Thread auf die Abarbeitung der Aufgaben. Korrespondierende Worker rufen hierzu am Ende ihrer Verarbeitung countDown() auf.

2012-01-01

Ultimate Swing, Teil 12

Ich hoffe, Sie sind gut in das Jahr 2012 gestartet und haben weiterhin Lust auf Swing. Wie an anderer Stelle bereits gesagt wird es natürlich auch weiterhin andere Themen, zum Beispiel rund um Android und Mobile Computing geben.

Das Speichern von (neu eingegebenen) Kategorien ist bisher recht schlampig implementiert:

            // FIXME: refactor code to SwingWorker
            Category cat = DTOFactory.createCategory(text);
            if (categoriesModel.addCategory(cat)) {
              if (categoryDelegate.save(cat)) {
                break;
              }
            }
            Application.showErrorMessage(MessageFormat.format(
                Application
                    .getString("string.error_new_category"),
                text));

Analog zum Löschen von Kategorien lässt sich dies sehr schön in einen Worker auslagern.

            Category c = DTOFactory.createCategory(text);
            new SaveCategoryWorker(c, categoriesModel,
                categoryDelegate).execute();

Ich finde, das sieht wesentlich aufgeräumter aus. Hier die Implementierung des Workers:

public class SaveCategoryWorker extends SwingWorker<Boolean, Void> {

  private final Category c;
  private final CategoriesModel m;
  private final CategoryDelegate d;

  public SaveCategoryWorker(Category c, CategoriesModel m, CategoryDelegate d) {
    this.c = c;
    this.m = m;
    this.d = d;
  }

  @Override
  protected Boolean doInBackground() throws Exception {
    boolean result = d.save(c);
    return new Boolean(result);
  }

  @Override
  protected void done() {
    Boolean result = Boolean.FALSE;
    try {
      result = get();
    } catch (InterruptedException e) {
      handleException(e);
    } catch (ExecutionException e) {
      handleException(e.getCause());
    } finally {
      if (Boolean.TRUE.equals(result)) {
        m.addCategory(c);
      }
    }
  }

  private void handleException(Throwable t) {
    Application.showErrorMessage(t.getLocalizedMessage());
    t.printStackTrace();
  }
}

Wenn Sie dies mit dem Worker aus dem vorangehenden Post vergleichen, stellen Sie viele Gemeinsamkeiten fest. Der Konstruktor und die Methode handleException() sind identisch. Es liegt nahe, beides in eine abstrakte Elternklasse auszulagern.

public abstract class AbstractCategoryWorker extends SwingWorker<Boolean, Void> {

  protected final Category c;
  protected final CategoriesModel m;
  protected final CategoryDelegate d;

  public AbstractCategoryWorker(Category c, CategoriesModel m,
      CategoryDelegate d) {
    this.c = c;
    this.m = m;
    this.d = d;
  }

  public abstract void doneIfTrue();

  public abstract String getStringForErrorLine1();

  @Override
  protected void done() {
    Boolean result = Boolean.FALSE;
    try {
      result = get();
    } catch (InterruptedException e) {
      handleException(e);
    } catch (ExecutionException e) {
      handleException(e.getCause());
    } finally {
      if (Boolean.TRUE.equals(result)) {
        doneIfTrue();
      }
    }
  }

  private void handleException(Throwable t) {
    String msg = t.getLocalizedMessage();
    if ((msg == null) || (msg.length() < 1)) {
      msg = t.getClass().getName();
    }
    Application.showErrorMessage(MessageFormat.format("{0}\n{1}",
        getStringForErrorLine1(), msg));
    t.printStackTrace();
  }
}

Die Methode handleException() gibt den Klassennamen der Ausnahme aus, wenn getLocalizedMessage() einen Leerstring oder null liefert. Dies war in früheren Versionen meiner Methode noch nicht so (robust) implementiert. Außerdem ist die Fehlermeldung nun zweizeilig. Die erste Zeile wird durch Aufruf der Methode getStringForErrorLine1() ermittelt. Zum Abschluss die deutlich abgespeckte Fassung der Klasse SaveCategoryWorker:

public class SaveCategoryWorker extends AbstractCategoryWorker {

  public SaveCategoryWorker(Category c, CategoriesModel m, CategoryDelegate d) {
    super(c, m, d);
  }

  @Override
  protected Boolean doInBackground() throws Exception {
    boolean result = d.save(c);
    return new Boolean(result);
  }

  @Override
  public void doneIfTrue() {
    m.addCategory(c);
  }

  @Override
  public String getStringForErrorLine1() {
    return MessageFormat.format(
        Application.getString("string.error_new_category"),
        c.getDescr());
  }
}