Zwischenprojekt: KVMPortSwitcher

Meine Entwicklungsstrategie für private Projekte folgt meist einer Stack-Strategie: je älter die Idee, desto tiefer vergraben, ständig kommt neues Zeugs oben drauf, und in seltenen Fällen findet das jüngste Projekt direkt zur Reife. Und so geschah es mit dem jüngsten Spross meines bunten Straußes an Java-Projekten: KVMPortSwitcher. Wer nur den technischen Sermon lesen und den Code sehen will, darf direkt zum Projekt auf GitHub abbiegen.

Um was geht es? Seit einiger Zeit besitze ich einen “TESmart 16-port HDMI KVM Switch” (das exakte Modell gibt es offenbar nicht mehr, aber hier ist der Nachfolger zu finden). TESmart ist der einzige mir bekannte Hersteller, der KVM-Switches mit 8 Ports und mehr zu einigermaßen bezahlbaren Preisen anbietet und trotzdem problemlos funktioniert – meine Anforderungen damals waren “4K, HDMI, 8 Ports oder mehr, keine Spezialkabel”. Dadurch schrumpfte die Zahl der zur Auswahl stehenden Geräte nach ausführlicher Recherche auf “1”.

Nun gehört alles mit 8 Ports und mehr offenbar üblicherweise der Profi-Fraktion an, und so hat dieser Switch zusätzlich einen Ethernet-Anschluss, mit dem man ihn ins LAN integrieren kann, um ihn so über IP steuern zu können. Warum würde man das wollen? Zum einen zwecks Konfiguration des Geräts. Aber vor allem zur Steuerung des ausgewählten Ports. Denn die anderen Wege, den Port zu wechseln, sind mit verschiedenen Problemen und Nicklichkeiten verbunden, die einem ganz schön auf die Nerven gehen können.

Man kann die Knöpfe an der Front des KVM-Switches verwenden. Prinzipiell keine schlechte Idee, aber beim 16-Port-Modell braucht man dabei für die Geräte 10-16 zwei Tastendrücke. Oder sogar drei, denn wenn der Display-Timeout zugeschlagen hat, dient der erste Tastendruck gar nicht dem Umschalten, sondern dem Aktivieren. Also zwei oder drei Tastendrücke, und man muss natürlich die Hand von der Tastatur nehmen.

Wie wäre es mit der IR-Fernbedienung? Anders, aber nicht besser. Wenn man sie denn mal gefunden hat (vorsichtshalber steht nur “Remote Control” drauf, damit man sie ja nicht klar zuordnen kann), kann man zwar die Ports 2-9 durch einen Tastendruck auswählen, die Ports 1 und 10-16 hingegen nicht – was besonders bei “1” sehr dumm ist, da drückt man auf “1” und wundert sich, dass nix passiert, bis dem Gerät klar wird, dass keine Eingabe mehr erfolgt. Wer jetzt denkt, er könne durch zwei Tastendrücke “0” und “1” das Gerät überlisten, sieht sich getäuscht. auch “1” gefolgt von “OK” hilft nicht. Immerhin braucht es den “Aktivierungstastendruck” wie bei den Front-Panel-Buttons nicht.

Wie wäre es mit der eingebauten Möglichkeit, über die Tastatur den Port zu wechseln? Im Prinzip eine gute Idee, aber: es funktioniert nur mit den im Gerät hartcodierten Shortcuts, und die sind eher komisch, aber natürlich so gewählt, dass sie möglichst nicht mit anderen gängigen Tastatursteuerungen kollidieren: zwei Mal Scroll Lock (aka “Rollen”), dann 1-n. Man erkennt direkt das Problem für die 16-Port-Variante: bei Rollen-Rollen-1 gibt es die von der Fernbedienung bekannte Gedenksekunde, bis klar ist, dass man wirklich “1” meint und nicht “10” oder “13” und nur langsam tippt. Und noch ein Haken: das funktioniert natürlich nur, wenn die Tastatur im dafür vorgesehenen spezifischen USB-Port steckt, der diese Logik enthält – dieser Port ist aber nicht kompatibel mit allen Tastaturen dieser Welt, und so muss man manchmal auf den “nackten” USB-Port ausweichen. Und schon ist es Essig mit der Hotkey-Funktionalität.

Insgesamt gibt es also ausreichend Gründe, alternative Möglichkeiten zum Umschalten der Ports zu suchen. Eine davon bietet TESmart selbst an mit einem kleinen Tool, das eine IP-Verbindung aufbauen kann, dadurch den aktiven Port feststellt und dann per Mausclick das Umschalten erlaubt. Schön, aber natürlich eher keine Verbesserung zu den anderen drei Varianten. Tatsächlich dauert es doch recht lange, bis das Tool endlich den “Connect” hergestellt hat – keine Ahnung, warum. Mehrere Sekunden jedenfalls.

Nun muss man TESmart zugutehalten, dass sie das Protokoll über die IP-Schnittstelle zur Ansteuerung der Funktionalität öffentlich dokumentiert haben – leider muss man solche Dinge ja positiv vermerken, da es sich nicht um eine Selbstverständlichkeit handelt. Jedenfalls ist es denkbar einfach: IP-Verbindung herstellen, ein paar Bytes senden, fertig ist die Laube.

Ich hatte also so ein Projekt schon länger im Hinterkopf – meine Brot-und-Butter-Implementierungssprache Java schien da aber ungeeignet, weil man dort keine systemweiten Keyboard-Shortcuts beim Betriebssystem registrieren kann, und eine weitere grafische Oberfläche, wo man per Click den KVM-Switch bedienen kann, wollte ich nun auch nicht bauen. Zufällig bin ich bei meinen Streifzügen durch das Internet dann aber über eine Bibliothek namens JNativeHook gestolpert, die für alle gängigen Betriebssysteme und CPU-Architekturen native code zusammen mit etwas Java-Glue-Code zur Verfügung stellt, um solche globalen Hotkey-Listener zu implementieren. Kurz getestet, tut, also frisch ans Werk.

Der Code an sich und die UI war natürlich trivial. Kurz noch ein TrayIcon dazugeschraubt (denn für eine Anwendung, die normalerweise im Hintergrund läuft, ist es immer geschickt, noch einen Haken zur Kontrolle derselben zu haben), ausprobiert. Funktionierte – meistens. Denn es gab ein interessantes Phänomen, dass der NativeKey-Event-Dispatcher manchmal, wenn der Port umgeschaltet wurde, munter weiter denselben Event wieder und wieder feuerte. Ich bin dem noch nicht auf den Grund gegangen, aber habe einen klassischen Workaround gefunden: asynchrone Verarbeitung, der Listener löst nicht direkt den Port-Switch aus, sondern scheduled einen extra Thread, der kurz wartet und dann erst umschaltet. Das scheint das Problem zuverlässig gelöst zu haben.

Wer bis hierhin durchgehalten hat, darf jetzt zum GitHub-Projekt abbiegen. Es gibt auch ein runnable jar nebst inkludiertem jnativehook-Jar als Release 0.1.0 zum Download bei GitHub, Java 8 wird zur Ausführung benötigt. Wer also zufällig einen TESmart HDMI-KVM-Switch sein Eigen nennt und ihn in sein Heimnetz integriert (hat), kann direkt loslegen.

Auf der GitHub-Projektseite kann man auch nachlesen, was noch die TODOs sind. Konfigurierbare Port-Namen (bei 16 Ports kann man schon mal den Überblick verlieren), konfigurierbare Keyboard-Shortcuts, i18n (bis jetzt ist alles hartcodiert englisch). Und vielleicht auch noch für die old-school-Generation die Steuermöglichkeit über die serielle Schnittstelle des TESmart-KVM-Switches?

Java. Swing. Dem Wahnsinn nahe.

Seit Swing 1.0.2, als das Package noch com.sun.java.swing hieß und Java 1.1 gerade der heiße Scheiß war, programmiere ich UIs mit Java und Swing. Es ist so eine Art Hassliebe. Nach wie vor halte ich es für den einfachsten und besten Weg, Cross-Platform-Anwendungen zu schreiben, die mehr oder weniger problemlos unter Windows und Linux und MacOS funktionieren. Und an sonnigen Tagen auch unter AIX und Solaris.

Aber manchmal…manchmal…graue Haare, Stirnrunzeln beim Debugging, dem Wahnsinn nahe. Ich baue gerade an einem kleinen Projekt dessen tieferer Sinn oder Unsinn hier nichts zur Sache tun soll. Jedenfalls wird dort als Anzeigekomponente die allseits beliebe JEditorPane mit HTML-Inhalt verwendet. Das ist von jeher etwas kitzelig weil ein etwas merkwürdiges HTML-3.2-Sub-und-Superset-mit-etwas-CSS unterstützt wird, und man sollte sehr genau wissen welches HTML man reinsteckt und was unfallfrei gerendert werden kann. Immer spannend war das Thema Grafiken. Im Prinzip sollte es ja ein img-Tag mit passender URL dahinter tun. Blabla.class.getResource(“/schnickschnack.png”) liefert ja schon eine lokale File-URL zurück, was kann da schon schiefgehen.

Eine ganze Menge. Und was ist, wenn man gar keine Datei hat, sondern ein beliebiges Image in-memory? Weil das so ist, haben schlaue Menschen einen Weg ersonnen, wie man der JEditorPane – oder besser gesagt dem HTMLDocument, das über den HTMLEditorKit beim ContentType “text/html” bekanntlich entsteht – ein beliebiges Image unterjubeln kann, indem man es in einen “imageCache” unter der im img-Tag hinterlegten URL steckt.

Hier ein Link zu Sourcecode, der illustriert, wie das prinzipiell geht. Sehr heikel, weil es sich auf ein Implementierungsdetail verlässt, aber Swing ist bekanntlich tot…äh…stabil, und so wird das wohl auch in Zukunft problemlos funktionieren. Dachte ich.

Wenn man nun aber obigen Sourcecode mal in der IDE der Wahl compiliert und zur Ausführung bringt, erlebt man leider eine Überraschung: mit Java 7 funktioniert das noch problemlos, mit Java 8 und neuer aber nicht. Ein beherztes Debugging an der richtigen Stelle – javax.swing.text.html.ImageView.loadImage – brachte keinen Aufschluss – das Image wird korrekt aus dem Cache geholt, seine Größe wird inspiziert, es wird layoutet, und dann…wird nichts angezeigt.

Ich will es nicht zu spannend machen, denn es gibt einen Workaround. Auch wenn ich gerade überhaupt nicht verstehe, warum der was ändert, weil genau das, was er eventuell ändern könnte, im Debugging nicht nach dem Problem ausgesehen hat.

Wenn man die Zeile

"<img src=\""+localImageSrc+"\">\n" +

ändert in

"<img src=\""+localImageSrc+"\" width=100 height=50>\n" +

dann funktioniert es plötzlich prima von Java 7 bis einschließlich Java 16.

Wer auch immer das im Detail analysieren will – Freiwillige vor.

SwingHelpViewer – ein einfacher Online-Hilfe-Viewer

Seit einiger Zeit schraube ich an diversen Anwendungen auf Basis von Java Swing, die wirklich demnächst die Release-Reife erreichen sollten. Die meisten aus der Reihe “für RISC OS, aber nicht unter RISC OS lauffähig” (da Java-basiert, und es unter RISC OS keine kompetente JVM gibt). Hier kann man ein paar wenige Details zu diesen geplanten Anwendungen nachlesen.

Wie dem auch sei, solche klassischen FatClient-Anwendungen brauchen typischerweise eine Online-Hilfe. Allein um dem Benutzer mitzuteilen, welche 3rd-Party-Bibliotheken genutzt werden und unter welcher Lizenz diese stehen.

Standard unter Java ist für solche Zwecke JavaHelp, vor Urzeiten von Sun entwickelt, aber seit fast so langer Zeit auch nicht weiter gepflegt. Diverse ungefixte Bugs, unklare Lizenzsituation (alle Sourcefiles reden von GPLv2, irgendwo steht “GPLv2 with Classpath Exception”, und es gibt auch Hinweise, dass es dual lizenziert ist sowohl unter GPLv2 als auch unter CDDL), merkwürdige indexbasierte Suche die außer Englisch recht wenig versteht, unflexibel in der Integration. Aber durchaus mit coolen Features für die damalige Zeit, inklusive einer JSP-basierten Server-Variante.

Allein die Lizenzsituation bewog mich, schnell mal was Eigenes zu bauen. Mit dem typischen “wie schwer kann das schon sein” auf den Lippen begann ich während eines langweiligen Fußballspiels (es könnte der VfB Stuttgart involviert gewesen sein), schnell mal einen Hilfe-Viewer ähnlich des JavaHelp-Viewers zu bauen. Ungefähr 10 Stunden später hatte ich einen Viewer, der ein JavaHelp-HelpSet (.hs-Datei) parsen und sowohl TOC als auch Index eines solchen HelpSets erfolgreich darstellen. Es war also tatsächlich nicht so schwer, am längsten hat noch das XML-Gefrickel gedauert, weil ich mir in den Kopf gesetzt hatte, ganz ohne 3rd-Party-Dependencies auszukommen, und das DOM-Zeugs in Java SE ist halt sehr mit-der-Hand-am-Arm.

Code aufräumen, Icons malen und jetzt seit neuestem eine einfache Volltextsuche dazuzubauen hat nochmal ein paar Stunden gekostet, alles in allem schätze ich mal 3 PT an Gesamtaufwand. Swing ist wohl doch nicht so schlecht wie viele seit etwa 1999 nicht müde werden zu behaupten.

Das Ergebnis dieser Bemühungen kann man auf GitHub inspizieren. Und damit es wirklich von jedem auf welche Weise auch immer verwendet werden kann, habe ich als Lizenz die “Unlicense” ausgewählt. Ist bestimmt auch wieder inkompatibel mit irgendwas, aber es ist die freieste Lizenz, die ich gefunden habe.

Wird es für den SwingHelpViewer eine Weiterentwicklung geben? Natürlich liegen einige Erweiterungen auf der Hand (indexbasierte Suche z.B. über Apache Lucene, ein Bookmark-System, Unterstützung für weitere JavaHelp-Features wie “merged help sets” und Direkthilfe-Action und Hilfe-Cursor und so, Unterstützung für andere Hilfedaten wie CHM, HLP oder Eclipse Help, Einbindung anderer Anzeigekomponenten die mehr von HTML und CSS verstehen als JEditorPane), aber der jetzige Zustand dürfte für meine Zwecke wohl reichen.

Wer die Herausforderung sucht, könnte z.B. mal schnell eine Java 1.2-kompatible Version des Codes bauen. Im Moment läuft alles ab Java 7, wenn man den Diamond Operator rauswirft müsste Java 5 gehen, wenn man die Generics rauswirft Java 1.4, und wenn man das XML-Parsing anders löst (wer erinnert sich noch an crimson.jar…), dürfte man bei Java 1.2 angekommen sein. Der Code ist sehr “old-school”, und Swing ist ja im Prinzip seit Java 1.2 unverändert.

Was GitHub angeht, ist SwingHelpViewer mein erstes dort publiziertes Projekt. Wenn ich die jetzige Geschwindigkeit beibehalte, werden meine über die Jahre entstandenen programmiertechnischen Ergüsse spätestens bis 2050 komplett auf GitHub gelandet sein.

Eine Swing-Komponente – HexDumpView

Nach dem großen Erfolg der JVM-Memory-Indicator-Komponente und dem sensationellen Layout-Manager nun eine weitere unverzichtbare Komponente in jeder grafischen Oberfläche, die aus unverständlichen Gründen nicht schon standardmäßig in Swing ausgeliefert wird: die Anzeige eines klassischen Hexdumps. In der ebenso klassischen 8 + 16 + 16-Darstellung: 8-stellige Adresse, danach 16 Bytes in Hex-Darstellung, danach 16 ASCII-Zeichen als direkte Übersetzung der Bytes. Mit möglichst simpler API: hier ist das Byte-Array, mach’ mir die HexDump-View als Swing-Komponente dazu, die ich zur Nutzung nur noch in eine JScrollPane reinsetzen muss. Natürlich superschnell und superspeichersparend.

Testkandidat war eine 800KiB-Datei (ein adf-Floppyimage, aber das tut nichts zur Sache, hat aber mit dem Ursprung des Wunsches nach dieser Komponente zu tun – ich hoffe, da kann ich demnächst Vollzug melden, dann voraussichtlich nebenan im RISC OS-Blog), resultierend in einem rund 50000 (genau 51200) Zeilen umfassenden Hexdump.

Wie ist es implementiert? Da gab es natürlich eine Menge Varianten, die mir spontan in den Sinn kamen und die wie immer mit unterschiedlichen Kompromissen behaftet sind, die ich kurz anreißen will.

Variante 1: Erzeugen einer String-Repräsentation der Binärdaten und Nutzung einer “normalen” JTextArea oder JEditorPane. Geht schnell, ist unproblematisch in der Realisierung, alle typischen Dinge einer Textkomponente sind direkt verfügbar (auch wenn in diesem Falle das Copy&Paste vermutlich nicht ganz das ist, was der Anwender erwartet). Aber speicher- und laufzeittechnisch sicher suboptimal – die Konvertierung in eine lange Zeichenkette, obwohl man diese ja bei größeren Datensätzen niemals gleichzeitig auf dem Schirm sieht, ist doch verhältnismäßig teuer, und die Wahrscheinlichkeit, dass sowohl das Quell-Byte-Array als auch die Zeichenkette im Speicher verbleiben müssen, ist ja doch recht groß (vermutlich will der Verwender seine Quelldaten ja nicht wegwerfen, sondern noch weiter verarbeiten), und pro Byte kostet die String-Repräsentation ja zusätzlich etwa 14 Bytes (73 Zeichen pro 16 Bytes.

Variante 1a: Erzeugen einer String-Repräsentation der Binärdaten und wrappen in ein java.swing.text.Document als Unterfütterung einer JTextArea/JEditorPane. Spontan würde ich denken, dass so ein reines ReadOnly-Document etwas besser bei Speicher- und Laufzeitverhalten ist als ein Wald- und Wiesen-Document aus dem Swing-Bauchladen.

Variante 2: Erzeugen einer String-Repräsentation der Binärdaten und direkter Redraw in paintComponent. Habe ich getestet, für mäßig große Binärdaten funktioniert das erschreckenderweise recht gut (in meinem Falle waren das rund 50000 Zeilen des Hexdumps), was glaube ich eher zeigt, wie schnell die Maschinen heutzutage sind, und nicht, wie man ein solches Problem angehen sollte. Speichertechnisch ist das etwas günstiger als eine JTextArea oder JEditorPane weil man die ganze unnütze Funktionalität weglässt, aber das Hauptproblem der Erzeugung der großen Zeichenkette bleibt.

Variante 2a: wie 2, aber mit optimiertem Redraw (paintComponent) – nur die Zeilen zeichnen, die laut Clipping Rectangle tatsächlich gezeichnet werden. Das ist geschwindigkeitstechnisch schon erheblich besser als Variante 2, weil eben nur sagen wir 100 Zeilen gezeichnet werden statt 50000, und deshalb aus der vollständigen String-Repräsentation auch nur 100 Substring-Operationen anstatt 50000 durchgeführt werden müssen. Ich habe allerdings nicht analysiert, ob denn den Substring-Operationen im Vergleich zum dann weggeclippten drawString wirklich die teurere Operation ist – einigen wir uns auf “hilft beides nicht”.

Variante 3: kein Erzeugen der vollständigen String-Repräsentation, sondern Erzeugung der zu malenden Textzeile “on demand” aus den Quelldaten, dem Byte-Array. Dabei Inspektion des Clipping Rectangles, um nur die notwendigen Operationen durchzuführen.

Nachfolgend beispielhaft die Implementierungsvariante 3 (die anderen sind zu trivial :-)), als ganz nacktes Java – keine externen Abhängigkeiten, und vermutlich würde der Code so schon unter Java 1.1 mit Swing 1.1 funktionieren. Es gibt hier natürlich Lücken (es sind nur rund 200 Zeilen Code, für eine vollständige JComponent, inklusive JavaDoc!), die selbstverständlich nur als Aufforderung zur Übung an den Leser zu verstehen sind. Encoding-Unterstützung für den Textbereich, Implementierung eines Carets und einer Selektionsmöglichkeit, Unterstützung für nicht-Monospaced-Schriften, on-demand-loading der Binärdaten aus einer Datei größer als MaxHeap während der Benutzer scrollt, suchen nach Text und Bytes und Adressen, Darstellung in Byte- und Word-Form, hinzufügen der Zeilennummer…

Auch eine Variante 4 – ein Document das allein durch die Quelldaten unterfüttert ist und on-the-fly den Text für die JTextComponent generiert, wäre eine interessante Forschungsaufgabe.

/*
 * (c) hubersn Software
 * www.hubersn.com
 *
 * Use wherever you like, change whatever you want. It's free!
 */
package com.hubersn.playground.swing;

import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Rectangle;

import javax.swing.JComponent;

/**
 * Component showing a hexdump.
 */
public class HexDumpView extends JComponent {

  /** Number of characters in one line of a hexdump: 8 characters address, 16 2-character-bytes, 16 characters, spaces. */
  private static final int LINE_LENGTH = 8 + 1 + 3 * 16 + 16;

  private byte[] data;

  private int lineCount = -1;

  private FontMetrics fm = null;

  private int lineHeight = 0;

  private int lineAscent = 0;

  private int characterWidth;

  private int calculatedWidth;

  private int calculatedHeight;

  /**
   * Creates a hexdump view based on the given data with a monospaced 12pt font.
   *
   * @param data hexdump data.
   */
  public HexDumpView(final byte[] data) {
    setFont(new Font("Monospaced", Font.PLAIN, 12));
    setData(data);
  }

  /**
   * Sets the data to be visualized by this hexdump view.
   *
   * @param data hexdump data.
   */
  public void setData(final byte[] data) {
    this.data = data;
    refresh();
  }

  /**
   * Sets the font to be used for the hexdump - note that only a monospaced font will yield sensible results.
   */
  @Override
  public void setFont(final Font font) {
    super.setFont(font);
    this.fm = null;
    refresh();
  }

  private void refresh() {
    if (this.data == null) {
      return;
    }
    calculateSizes();
    revalidate();
  }

  private int calculateLineCount() {
    if (this.data == null) {
      return 0;
    }
    int lc = this.data.length / 16;
    if (this.data.length % 16 > 0) {
      lc++;
    }
    return lc;
  }

  private void calculateSizes() {
    if (this.fm == null) {
      this.fm = getFontMetrics(getFont());
      this.lineHeight = this.fm.getHeight();
      this.lineAscent = this.fm.getAscent();
      this.characterWidth = this.fm.stringWidth("M");
    }
    this.lineCount = calculateLineCount();
    this.calculatedWidth = getInsets().left + getInsets().right;
    this.calculatedWidth += LINE_LENGTH * this.characterWidth;
    this.calculatedHeight = getInsets().top + getInsets().bottom;
    this.calculatedHeight += this.lineCount * this.lineHeight + this.lineAscent;
  }

  @Override
  public Dimension getPreferredSize() {
    return new Dimension(this.calculatedWidth, this.calculatedHeight);
  }

  @Override
  protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    if (this.data == null) {
      return;
    }
    Rectangle clipBounds = g.getClipBounds();
    Insets insets = getInsets();
    int y = insets.top + this.lineAscent;
    for (int i = 0; i < this.lineCount; i++) {
      // optimized redraw - check if clipping bounds contains line
      if (shouldDraw(clipBounds, y)) {
        String str = getHexDumpLine(this.data, 0, i * 16);
        g.drawString(str, insets.left, y);
      }
      y += this.lineHeight;
    }
  }

  private boolean shouldDraw(Rectangle r, int y) {
    return !(y + this.lineHeight < r.y || y - this.lineHeight > r.y + r.height);
  }

  //
  // -- utility methods for hex(dump) representation
  //

  /**
   * Converts a value between 0 and 15 into the corresponding hex character - throws an
   * IllegalArgumentException on illegal input values.
   *
   * @param value value to convert.
   * @param useUpperCase use upper case letters A-F?
   * @return hex character representing value.
   */
  private static char valueToHexChar(final long value) {
    if (value < 0 || value > 15) {
      throw new IllegalArgumentException("Value " + value + " out of range - must be 0-15");
    }
    if (value < 10) return (char) ('0' + value);

    return (char) ('A' + (value - 10));
  }

  /**
   * Converts the given value to a number of hex characters specified in nibble count value.
   *
   * @param value
   * @param nibbleCount amount of nibbles (characters) to return.
   * @param useUpperCase use upper case hex characters.
   * @return hex representation of value.
   */
  private static String valueToHexChars(final long value, final int nibbleCount) {
    char[] hexChars = new char[nibbleCount];
    for (int i = 0; i < nibbleCount; i++) {
      hexChars[i] = valueToHexChar(0b1111 & (value >> (4 * (nibbleCount - 1 - i))));
    }
    return new String(hexChars);
  }

  /**
   * Returns a line (not terminated) for a hexdump output - either 16 bytes starting from offset or less if data has less than 16 bytes available.
   *
   * @param data data for dump.
   * @param startAddress start address of data.
   * @param offset offset into data.
   * @return hexdump line.
   */
  private static String getHexDumpLine(final byte[] data, final long startAddress, final int offset) {
    StringBuilder sb = new StringBuilder(LINE_LENGTH);
    sb.append(valueToHexChars(startAddress + offset, 8));
    sb.append(' ');
    for (int lineOffset = 0; lineOffset < 16; lineOffset++) {
      if (offset + lineOffset < data.length) {
        sb.append(valueToHexChars(getUnsigned(data[offset + lineOffset]), 2));
        sb.append(' ');
      } else {
        sb.append("   ");
      }
    }
    for (int lineOffset = 0; lineOffset < 16; lineOffset++) {
      if (offset + lineOffset < data.length) {
        sb.append(getPrintableCharacter(getUnsigned(data[offset + lineOffset])));
      }
    }
    return sb.toString();
  }

  private static int getUnsigned(final byte b) {
    if (b < 0) {
      return ((int) b + 256);
    }
    return b;
  }

  private static char getPrintableCharacter(final int byteValue) {
    if (byteValue < 32 || byteValue > 255 || (byteValue > 128 && byteValue < 160)) {
      return '.';
    }
    return (char) byteValue;
  }

}

Ein Swing-Layout-Manager – OneRowSameSizeLayoutManager

Jeder Java-Entwickler, der schon mal seriös mit Swing eine Oberfläche gebaut hat, hat schon einmal mit dem Layout-Manager seiner Wahl gekämpft. Die standardmäßig im JDK ausgelieferten Layout-Manager sind eher gewöhnungsbedürftig bis leistungsschwach, mindestens aber umständlich. GroupLayout für komplexe Dinge und BorderLayout für Basislayouts sind gerade noch so verwendbar. Beliebte 3rd-party-Layout-Manager sind sicherlich MigLayout (oder MiG Layout? Man kann sich nicht entscheiden…), FormLayout, TableLayout und DesignGridLayout (Jahre nach dem Ableben von java.net wohl nur noch über Maven Central beziehbar).

Für besondere Bedürfnisse ist es manchmal eine gute Idee, einen einfachen LayoutManager selbst zu bauen. Ich habe das mal beispielhaft getan für den Anwendungsfall “Layout einer typischen Reihe von Buttons unten in einem Dialog”. Im Gegensatz zum merkwürdigen Beispiel im Swing-Tutorial von Oracle namens “DiagonalLayout” hoffe ich jedenfalls, dass mein Beispiel etwas mehr Nützlichkeit ausstrahlt. Der Quellcode ist mehr auf Lesbarkeit und Nachvollziehbarkeit denn auf Eleganz getrimmt. An einem schmissigeren Namen arbeite ich noch…

Irgendwann werde ich noch mal den “Packer”, einen Layout-Manager von Tcl/Tk, für Swing nachbauen. M.E. war der “Packer” in seinem Verhalten und der Vorhersagbarkeit des programmierten Resultats absolut untadelig, etwas was ich von den meisten Swing-LayoutManagern nicht sagen kann. Aber vielleicht ist meine Erinnerung an den Packer auch rosarot verklärt – ist schon über zwei Jahrzehnte her seit meinem ersten und letzten Kontakt.

/*
 * (c) hubersn Software
 * www.hubersn.com
 */
package com.hubersn.playground.swing.layout;

import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.LayoutManager;

import javax.swing.SwingConstants;

/**
 * A layout manager supporting a row of components kept at the same width and height with specifiable alignment and gap size between the
 * components. It has its own insets to avoid having to work with empty borders.
 */
public class OneRowSameSizeLayout implements LayoutManager {

  /** Constant for default left alignment (identical to SwingConstants.LEFT). */
  public static final int ALIGN_LEFT = SwingConstants.LEFT;
  /** Constant for default centre alignment (identical to SwingConstants.CENTER). */
  public static final int ALIGN_CENTER = SwingConstants.CENTER;
  /** Constant for default right alignment (identical to SwingConstants.RIGHT). */
  public static final int ALIGN_RIGHT = SwingConstants.RIGHT;

  // calculated overall minimum/preferred size
  private int minWidth = 0;
  private int minHeight = 0;
  private int preferredWidth = 0;
  private int preferredHeight = 0;
  // calculated maximum component preferred sizes
  private int maxPreferredWidth = 0;
  private int maxPreferredHeight = 0;

  private Insets insets;

  private int componentGap = 5;

  private int alignment;

  /**
   * Creates a new layout with right alignment, 4 pixels gap and 8 pixels insets all around.
   */
  public OneRowSameSizeLayout() {
    this(ALIGN_RIGHT, 4, new Insets(8, 8, 8, 8));
  }

  /**
   * Creates a new layout with given alignment, gap between components and insets.
   * 
   * @param alignment component alignment - ALIGN_LEFT, ALIGN_CENTER or ALIGN_RIGHT.
   * @param componentGap gap in pixels between components.
   * @param insets insets to use additional to possible container border - if null, all insets are 0.
   */
  public OneRowSameSizeLayout(final int alignment, final int componentGap, final Insets insets) {
    setAlignment(alignment);
    this.componentGap = componentGap;
    this.insets = insets;
    if (insets == null) {
      this.insets = new Insets(0, 0, 0, 0);
    }
  }

  /**
   * Sets the component alignment for this layout.
   * 
   * @param alignment component alignment - ALIGN_LEFT, ALIGN_CENTER or ALIGN_RIGHT.
   */
  public void setAlignment(final int alignment) {
    this.alignment = alignment;
  }

  @Override
  public void addLayoutComponent(String name, Component comp) {
    // nothing to do - we are not interested in user-given layout constraints.
  }

  @Override
  public void removeLayoutComponent(Component comp) {
    // nothing to do.
  }

  private void calculateSizes(Container parent) {
    final int numberOfComponents = parent.getComponentCount();

    this.preferredWidth = this.insets.left + this.insets.right;
    this.preferredHeight = this.insets.top + this.insets.bottom;
    this.minWidth = 0;
    this.minHeight = 0;

    final int numberOfVisibleComponents = getNumberOfVisibleComponents(parent);
    for (int i = 0; i < numberOfComponents; i++) {
      final Component c = parent.getComponent(i);
      if (c.isVisible()) {
        final Dimension d = c.getPreferredSize();
        this.maxPreferredWidth = Math.max(this.maxPreferredWidth, d.width);
        this.maxPreferredHeight = Math.max(this.maxPreferredHeight, d.height);
        this.minWidth += c.getMinimumSize().width;
      }
    }

    this.preferredHeight += this.maxPreferredHeight;
    this.minHeight = this.maxPreferredHeight;

    if (numberOfVisibleComponents > 0) {
      this.preferredWidth += (this.maxPreferredWidth * numberOfVisibleComponents);
      this.preferredWidth += this.componentGap * (numberOfVisibleComponents - 1);
    }
  }

  @Override
  public Dimension preferredLayoutSize(Container parent) {
    final Dimension dim = new Dimension(0, 0);

    calculateSizes(parent);

    final Insets parentInsets = parent.getInsets();
    dim.width = this.preferredWidth + parentInsets.left + parentInsets.right;
    dim.height = this.preferredHeight + parentInsets.top + parentInsets.bottom;

    return dim;
  }

  @Override
  public Dimension minimumLayoutSize(Container parent) {
    final Dimension dim = new Dimension(0, 0);

    final Insets parentInsets = parent.getInsets();
    dim.width = this.minWidth + parentInsets.left + parentInsets.right;
    dim.height = this.minHeight + parentInsets.top + parentInsets.bottom;

    return dim;
  }

  @Override
  public void layoutContainer(Container parent) {
    final int numberOfVisibleComponents = getNumberOfVisibleComponents(parent);
    if (numberOfVisibleComponents == 0) {
      return;
    }

    calculateSizes(parent);

    final Insets parentInsets = parent.getInsets();

    // will all fit into parent? Which sizes to reduce?
    // width sizing strategy:
    // first, shrink gap and insets to 0
    // then, shrink button width and height
    // height sizing strategy:
    // shrink insets to 0
    final int parentWidth = parent.getWidth() - (parentInsets.left + parentInsets.right);
    final int parentHeight = parent.getHeight() - (parentInsets.top + parentInsets.bottom);
    final int heightToUse = Math.min(parentHeight, this.maxPreferredHeight);
    // width shrinking - toUse values are used for final layout bounds setting step
    int widthToUse = this.maxPreferredWidth;
    int buttonGapToUse = this.componentGap;
    int leftInsetToUse = this.insets.left;
    int rightInsetToUse = this.insets.right;

    int amountToReduceWidth = this.preferredWidth - parentWidth;
    if (amountToReduceWidth > 0) {
      // we need to save some pixels...
      // check for left/right insets
      if (leftInsetToUse + rightInsetToUse >= amountToReduceWidth) {
        int reduceInsetBy = amountToReduceWidth / 2;
        leftInsetToUse -= reduceInsetBy;
        amountToReduceWidth = 0;
      } else {
        leftInsetToUse = 0;
        amountToReduceWidth -= (this.insets.left + this.insets.right);
      }
      if (amountToReduceWidth > 0) {
        // check for button gaps
        if (numberOfVisibleComponents > 1 && (numberOfVisibleComponents - 1) * buttonGapToUse >= amountToReduceWidth) {
          buttonGapToUse -= amountToReduceWidth / (numberOfVisibleComponents - 1);
          amountToReduceWidth = 0;
        } else {
          buttonGapToUse = 0;
          amountToReduceWidth -= (numberOfVisibleComponents - 1) * this.componentGap;
        }
        // now shrink the button size itself as a last resort
        if (amountToReduceWidth > 0) {
          widthToUse = parentWidth / numberOfVisibleComponents;
        }
      }
    }

    // height shrinking - toUse values are used for final layout bounds setting step
    int topInsetToUse = this.insets.top;
    int amountToReduceHeight = this.preferredHeight - parentHeight;
    if (amountToReduceHeight > 0) {
      // we need to save some pixels...
      // check for top/bottom insets
      if (topInsetToUse + this.insets.bottom >= amountToReduceHeight) {
        int reduceInsetBy = amountToReduceHeight / 2;
        topInsetToUse -= reduceInsetBy;
      } else {
        topInsetToUse = 0;
      }
    }

    // finally size and place the components
    int x = parentInsets.left + leftInsetToUse;
    // alignment is only relevant if we have enough space
    if (parentWidth > this.preferredWidth) {
      if (this.alignment == ALIGN_CENTER) {
        x += (parentWidth - this.preferredWidth) / 2;
      } else if (this.alignment == ALIGN_RIGHT) {
        x += parentWidth - this.preferredWidth;
      }
    }

    final int y = parentInsets.top + topInsetToUse;

    final int numberOfComponents = parent.getComponentCount();
    for (int i = 0; i < numberOfComponents; i++) {
      Component c = parent.getComponent(i);
      if (c.isVisible()) {
        c.setBounds(x, y, widthToUse, heightToUse);
        x += widthToUse + buttonGapToUse;
      }
    }
  }

  private static int getNumberOfVisibleComponents(final Container parent) {
    final int numberOfComponents = parent.getComponentCount();
    int numberOfVisibleComponents = 0;

    for (int i = 0; i < numberOfComponents; i++) {
      Component c = parent.getComponent(i);
      if (c.isVisible()) {
        numberOfVisibleComponents++;
      }
    }

    return numberOfVisibleComponents;
  }
}

Eine Swing-Komponente – JVM-Memory-Indicator

Ich entwickle grafische Oberflächen in Java Swing seit der Zeit, als man Swing noch separat herunterladen musste und es noch im Package com.sun.java.swing wohnte. JDK 1.1. In JDK 1.0.2 hat man sich kurz AWT angeschaut, gelacht und es zu den Akten gelegt. Swing hingegen war ein seriöser Versuch eines leistungsfähigen und trotzdem plattformübergreifenden UI-Toolkits.

Drei Dinge jenseits von Java SE braucht der Swing-Entwickler bis heute: Komponenten, Layout-Manager und ggf. ein Application Framework. Neulich war ich auf der Suche nach einer kleinen schmalen Komponente für eins meiner zahlreichen Swing-basierten Inhouse-Tools, um den Zustand der JVM-Speicherverwaltung zu signalisieren, ähnlich wie Eclipse es standardmäßig anbietet wenn man es aktiviert unter “Window -> Preferences -> Global -> Show heap status”.

Als erfahrener Googler hätte ich erwartet, eher ein Problem mit zuviel als zuwenig Auswahl zu haben. Aber Pustekuchen: letztlich fand ich nur eine Frage auf StackOverflow (witzigerweise wenige Stunden nach meiner Antwort als off-topic geschlossen – und das bei einer Frage von 2009!) nach einer solchen Komponente, die lediglich zwei komplett falsche Antworten der Kategorie “Thema verfehlt” nach sich zog. Also habe ich mir mal eine Stunde Zeit genommen, um mit minimalem Aufwand eine solche Komponente zu bauen. Als Basis habe ich JProgressBar verwendet. Das Ergebnis ist natürlich weit weg von der wünschenswerten Flexibilität einer solchen Komponente, wenn sie in unterschiedlichen Kontexten wiederverwendbar sein soll, aber als ein schönes einfaches Beispiel taugt es dennoch. Die Komponente ist lizenztechnisch frei verwendbar allüberall. Eigene Anpassungen ausdrücklich erwünscht. i18n-Unterstützung, MiB vs. MB, Text und Tooltip über extern vorgebbare Texte mit Platzhaltern realisieren, ein “Trash”-Icon zum Auslösen des GC anstatt der nichtoffensichtlichen Doppelclick-Variante, eine “Mark”-Funktionalität ähnlich Eclipse – die Erweiterungsideen sind beinahe endlos.

/*
 * (c) hubersn Software
 * www.hubersn.com
 * 
 * Use wherever you like, change whatever you want. It's free!
 */
package com.hubersn.playground.swing;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.JProgressBar;
import javax.swing.Timer;

/**
 * JVM Memory Indicator based on JProgressBar.
 */
public class ProgressBarBasedJVMMemoryIndicator extends JProgressBar {

  private static final long serialVersionUID = 1L;

  private static final int TIMER_INTERVAL_IN_MS = 1000;

  /**
   * Creates a new instance of ProgressBarBasedJVMMemoryIndicator.
   */
  public ProgressBarBasedJVMMemoryIndicator() {
    super(0, 100);
    setStringPainted(true);
    setString("");
    addMouseListener(new MouseAdapter() {
      @Override
      public void mouseClicked(final MouseEvent mev) {
        if (mev.getClickCount() == 2) {
          System.gc();
          update();
        }
      }
    });
    final Timer t = new Timer(TIMER_INTERVAL_IN_MS, new ActionListener() {
      @Override
      public void actionPerformed(final ActionEvent aev) {
        update();
      }
    });
    t.start();
    update();
  }

  private void update() {
    final Runtime jvmRuntime = Runtime.getRuntime();
    final long totalMemory = jvmRuntime.totalMemory();
    final long maxMemory = jvmRuntime.maxMemory();
    final long usedMemory = totalMemory - jvmRuntime.freeMemory();

    final long MEBIBYTE_FACTOR = 1024 * 1024;
    final long totalMemoryInMebibytes = totalMemory / MEBIBYTE_FACTOR;
    final long maxMemoryInMebibytes = maxMemory / MEBIBYTE_FACTOR;
    final long usedMemoryInMebibytes = usedMemory / MEBIBYTE_FACTOR;
    final int usedPercentage = (int) ((100 * usedMemory) / totalMemory);
    final String textToShow = usedMemoryInMebibytes + "MiB of " + totalMemoryInMebibytes + "MiB";
    final String toolTipToShow = "Heap size: " + usedMemoryInMebibytes + "MiB of total: " + totalMemoryInMebibytes + "MiB max: "
        + maxMemoryInMebibytes + "MiB - double-click to run Garbage Collector";

    setValue(usedPercentage);
    setString(textToShow);
    setToolTipText(toolTipToShow);
  }
}

Aus mir unerfindlichen Gründen habe ich danach angefangen, eine superflexible, i18n-fähige, effiziente, JComponent-basierte Variante davon zu bauen. Hat auch nicht viel länger gedauert. Soll “demnächst” im Rahmen meiner ultimativen Swing-Bibliothek das Licht der Welt erblicken – das soll diese Bibliothek aber schon seit ungefähr 2001…

Swing-Rätsel – JTree-Zeilenhöhe

Manchmal hat man Probleme schon vor so langer Zeit gelöst, dass man sich bei erneutem Auftreten nicht mehr dran erinnert. Weder an das Problem, noch an die Lösung.

Es geht um Java Swing. Die Basics. Man nehme einen JTree. Da gibt es eine Methode namens “setRowHeight” wo man die zu rendernde Zeilenhöhe setzen kann. Wie so oft gibt es hier auch eine “magic number”, die besonderes Verhalten aktiviert. Hier ist es die 0 (oder negative Werte) – damit instruiert man den Tree, doch bitte den Renderer zu fragen, wie hoch die Zeile denn nun wirklich werden soll. Man sollte meinen, dass das auch ein wirklich guter Default wäre.

Nun gibt es ja in Swing zwei Varianten, wie so ein Default in Aktion tritt. Entweder simple Hartcodierung, oder durch die UI-Defaults des aktiven Look&Feel. Wer weiß auswendig, wie der hartcodierte Wert ist und welches der Standard-Look&Feels dann welchen Wert setzt? Bonuspunkte für denjenigen, der das auch noch schlüssig erklären kann.

Amüsante Randnotiz (Achtung, Spoiler! Teile der Antwort auf obige Frage inside!): hier ein Bug aus Zeiten von Java 1.4 (gab es das wirklich schon 2002?), der sich ebenfalls mit diesem Problem befasst.

Das Ende des Java-Plugins

Ich habe mich ja schon zu Anfangszeiten dieses Blogs als Fan der Java Applet-Technologie geoutet. Daran hat sich eigentlich nichts geändert. Nach wie vor halte ich Applets für den elegantesten Weg, vernünftige Benutzeroberflächen in den Browser zu bringen und gleichzeitig problemlos mit derselben Codebasis die Freunde des FatClients zu bedienen. Kluge Menschen haben behauptet, die Idee, GUIs in den Browser zu bringen, hätte die Qualität von Benutzeroberflächen 20 Jahre zurückgeworfen. Inzwischen dürften es wohl eher 25 Jahre sein, denn die Qualität von gängigen Oberflächen der 90er, sei es RISC OS oder MacOS oder die diversen GEM-Varianten, ist bis heute nicht erreicht. Optisch vielleicht schon, aber ergonomisch und performancetechnisch auf keinen Fall.

Nun hat Oracle verkündet (und schon vorher anklingen lassen, siehe etwa hier), man werde das Java-Plugin, das für die Ausführung von Applets im Browser zuständig ist, mit Java 9 als “deprecated” erklären und es in einer späteren Version aus Java entfernen. Als Grund wird genannt, dass mobile Browser ja noch nie Plugins unterstützt hätten und diverse Browser entweder die Plugin-Unterstützung schon entfernt hätten (Chrome), dies vorhätten (Firefox) oder noch nie eine gehabt hätten (MS Edge). Dazu die Security-Problematik.

So weit, so traurig. Insbesondere den Security-Aspekt konnte ich noch nie nachvollziehen – man kann das Java-Plugin ja so konfigurieren, dass es nur Applets mit bestimmter Signatur überhaupt ausführt, insofern ist diese Technologie wohl kaum unsicherer als die Ausführung beliebigen anderen Codes auf der Maschine. Dass es ab und zu in der Sandbox (auch klaffende) Security-Lücken gab, ist ja kaum ein Alleinstellungsmerkmal von Plugins – auch die Browser selbst sind ja wandelnde Sicherheitslöcher, selbst bei so simplen Dingen wie Grafikdecodierung. Das hat man halt davon, wenn man in Pseudo-Hochsprachen wie C entwickelt. Aber ich schweife ab.

Letztlich ist das alles “water under the bridge”, und man sollte nach vorne schauen. Dazu sollte man zunächst herausfinden, in welchem Zeitrahmen man nun Abschied von den Applets nehmen muss.

Entscheidend dafür ist zunächst der verwendete Browser. Bei IE11 kann man sich noch etwas Zeit lassen – Januar 2023 endet der Support von Microsoft für den IE11, zumindest unter Windows 8. Bei Firefox muss man sich schon eher sputen, Ende 2016 dürfte es vorbei sein. Es gibt aber durchaus noch Browser, die die NSAPI für Plugins unterstützen und noch nicht abgekündigt haben – Konqueror, QupZilla oder Midori (allesamt WebKit-basiert). Wie lange das anhält, wird man sehen – nicht so weit verbreitete Browser folgen ja häufig mit kurzer Ankündigungsfrist dem Mainstream und bieten selten lange Support-Garantien.

Wie sieht es auf der Java-Seite aus? September 2017 endet voraussichtlich die Verfügbarkeit öffentlicher Java 8-Updates, basierend auf dem Oracle-Releasezyklus wird das Ende der öffentlichen Java 9-Updates also frühestens September 2019 sein. Danach kann man Oracle etwas Geld in den Rachen werfen, um weiterhin Java 9-Updates zu bekommen. Bis heute unterstützt Oracle ja gegen Einwurf kleiner Münzen sogar Java 5. Wer es also aussitzen will bis zuletzt, muss sich vermutlich bis zum Ende von IE11 (frühestens 2023 laut Microsoft) keine Sorgen machen.

Executive Summary: Don’t Panic.

Java auf dem Desktop? Echt jetzt?

Wie oft haben wir es gehört – der Desktop ist tot, der Browser ist die Plattform der Zukunft, und Java ist auf dem Client sowieso tot und existiert nur auf dem Server.

JSR 377 heißt die Überraschung offiziell. Mindestens zwei der Beteiligten sind keine Unbekannten in der Java-Welt: Andres Almiray und Hendrik Ebbers. Letzterer hat hier darüber gebloggt.

Ich bin gespannt, ob es gelingt, die Pleiten bei JSR 193 (Client Side Container, anno 2002) und dem Swing Application Framework (JSR 296) vergessen zu machen. Der Umfang scheint mir recht ambitioniert zu sein, und man läuft bei diesem Thema immer Gefahr, vom hundertsten ins tausendste zu kommen. Allein die Zielgruppe zu definieren (kleine und/oder große Anwendungen? Modularisierung selbst oder auf Basis von z.B. OSGi (oder gar Jigsaw)? Nur Entwicklung oder auch Test und Deployment? Für ein spezielles Toolkit oder generisch? Support für Persistency irgendeiner Art und wenn ja welcher Art?) ist eine Kunst. Dann die Abgrenzung gegenüber den Platzhirschen Eclipse RCP und Netbeans Platform.

Ich persönlich denke, dass ein schlankes Framework irgendwo zwischen “Swing/JavaFX von Hand” und Netbeans Platform auf jeden Fall seinen Platz finden würde. Aber das dachte ich auch schon beim (B)SAF. Mal sehen, ob es diesmal klappt.

Die initiale maximale Größe eines JPanels

Heute bin ich über ein interessantes Detail bei der Swing-Programmierung gestolpert. Gegeben war eine JLayeredPane, die ein JPanel (leer, nur mit Hintergrundfarbe ausgestattet) und eine Editor-Komponente (basierend auf JTextPane)  beinhaltete. Die Editor-Komponente wurde durch sehr viel Textinhalt nun relativ groß (über 70000 Pixel hoch), und man sah plötzlich, dass das JPanel nicht mehr die komplette Höhe ausfüllte.

Debugging brachte zutage, dass regelmäßig setBounds des JPanels aufgerufen wurde, mit einer Höhe von 32767 Pixeln. Aha, das Maximum eines vorzeichenbehafteten 16bit-Wertes. Aber wo kommt der her?

Des Rätsels Lösung: es ist die maximumSize des JPanels im Initialisierungszustand. Man lernt halt jeden Tag dazu, selbst wenn man seit Swing 1.1 damit Oberflächen baut.

Wer dieses Detail aus dem Stegreif wusste, darf mir gerne mailen.

Besonderer Gruß an THR, mit dem ich zusammen das Problem gedebuggt habe.