Donnerstag, 23. Mai 2013

Eine konfigurierbare Dropdown-Liste

Ich setze mir beim Entwickeln oft bestehende Programme zu Befehlen zusammen, die mich beim Arbeiten mit Code unterstützen sollen und die ich mir z.B. in der Werkzeugliste meines Klartexteditors UltraEdit verfügbar mache.

Bei manchen dieser Befehlsketten gibt es Zwischenstufen, in denen ich eine Auswahl aus verschiedenen Möglichkeiten haben möchte: Eine einzelne, isolierte Dropdown-Liste als Befehl zwischenschaltbar, mit konfigurierbaren Inhalten - statt für jede konkrete Dropdownliste ein neues GUI programmieren zu müssen.

Hier möchte ich eine solche Komponente vorstellen: In einer einzelnen Java-Klasse ListBox.java kombiniere ich die Swing-Komponente JOptionPane mit der zu Java 6 neu hinzugekonnen ScriptEngine-Architektur, um folgende Aufgabe zu lösen:
Lies die Konfiguration der Dropdown-Liste als JSON-Datei aus der Standardeingabe - und gib dann einen Dialog mit dem gewünschten Titel, der gewünschten Meldung und den gewünschten Optionen aus. Schreibe die Auswahl des Benutzers in die Standardausgabe.
Wenn ich zum Beispiel eine Datei testoptions.json folgenden Inhalts habe:
{ 
  "options": {
    "test1":"Testoption 1",
    "test2":"Testoption 2" 
    },
  "title":"Testoptionen", 
  "msg":"Bitte auswählen" 
}
dann generiert mir der Befehl
java -cp "." ListBox <testoptions.json
folgendes Dialogfenster:

Wenn ich eine Auswahl treffe, gibt die Klasse den betreffenden Key in stdout aus.

So lässt sich die Dropdownliste als eine Komponente in eine Kette von Werkzeugen verwenden, die - ganz getreu der Unix-Philosophie - ihre Informationen in Textform über die Standardein- und -ausgabe weiterreichen. Hier zum Beispiel mein Werkzeug, um eine oMeta-Transformation (aus einer Liste von möglichen) auf das aktuelle Textdokument anzuwenden:

Da die Verwendung von Javas Script Engine vielleicht noch nicht so verbreitet ist (obwohl Java 6 ja schon vor langer Zeit das Licht der Welt erblickt hat), sind ein paar detailliertere Ausführungen vielleicht nützlich.

Der Einfachheit halber habe ich die Daten, die sich tatsächlich konfigurieren lassen, als statische Klassenattribute ausgeprägt:

  private static ArrayList list = new ArrayList();
  private static String title = "Selection";       // Default
  private static String msg   = "Please select";   // Default
In der Methode getListData() baue ich mir das JSON, das ich aus der Standardeingabe lese, in ein kleines Script ein, das die JSON-Daten in gebundene Java-Variablen überträgt.

Für den Array ist die Aufgabe bereits mit dem js.put("list",list) erledigt: Ein Array ist ein änderbarer Datentyp - er kann im JavaScript-Code mit ändernden Operationen wie add( ) bearbeitet werden, und nach der Rückkehr vom JavaScript-Prozessor hat er automatisch den aktuellsten Stand. Anders ist es bei den Strings msg und title. Der Datentyp String ist in Java unveränderlich. Änderungen an der durch die Bindung zugeordneten JavaScript-Variablen muss ich mir nach Durchlaufen des JavaScripts explizit mit js.get() ins Java abholen.
// Den JSON-Array mit dem Directory auswerten  
  private static void getListData( ) throws IOException {
    
    String _title = "",
           _msg   = "",
           json = readEntireInput(); 
           
    if (json.length() == 0) {
      System.err.println( "No option input" );
      return;
      }       
           
    try {

      String dir = "var all = " + json + ";"
                 + "for( var key in all.options ) { "
                 + "  list.add( key + ' - ' + all.options[key] ); "
                 + "  }; " 
                 + "if (all.title) title = all.title;"
                 + "if (all.msg)   msg = all.msg;";
                 
      ScriptEngine js = new ScriptEngineManager().getEngineByName("JavaScript");
      js.put("list",list);
      js.eval( dir );  
      _title = (String) js.get("title");
      if (_title != null && _title.length() > 0) {
        title = _title;
        }
      _msg = (String) js.get("msg");
      if (_msg != null && _msg.length() > 0) {
        msg = _msg;
        }
      
      }
      catch (javax.script.ScriptException ex) {
        ex.printStackTrace(); 
        }
    
    }  
Wenn die Daten mittels getListData() einmal beschafft sind, müssen sie nur noch dem JOptionPane vorgelegt werden:
  public static void main(String[] args) throws IOException {
        
  getListData();
  if (list.size() == 0) {
    return;
    }
  
  String selectedValue = (String) JOptionPane.showInputDialog(
        null,
        msg, 
        title,
        JOptionPane.INFORMATION_MESSAGE, 
        null,
        list.toArray(), 
        list.get(0)
        );
  
  if (selectedValue == null) selectedValue = "";      
        
  System.out.println( selectedValue.replaceFirst("\\s*\\-.*$","") );
  
  } 
Schlüssel und Wert habe ich in die Dropdownbox-Option einmontiert und entferne den Textteil am Schluss mit dem regulären Ausdruck \s*\-.*$, um nur den gewählten Schlüssel in die Standardausgabe zu schreiben.

Zu beachten ist noch, dass die Standardeingabe in der Unicode-Codierung UTF-8 erwartet wird. Man sollte sein Konfigurationsfile also im UTF-8-Format abspeichern (in den meisten Klartexteditoren kann man die Codierung im Speichern-Dialog noch auswählen). Die Codierung UTF-8 habe ich in der Metode readEntireInput() explizit angegeben - sonst würde ein gerätespezifischer Zeichensatz verwendet, auf meinem Gerät beispielsweise Windows-1252, was zu Problemen mit den Texten in der Swing-Komponente führt: Dort werden Texte immer im Unicode-Format erwartet. Aus diesem Grunde ist es gut, bei Readern von der Standardeingabe oder vom File, wann immer möglich, auch den zu verwendenden Zeichensatz anzugeben:
  private static final int MAX_INPUT = 4096;
  private static String readEntireInput() throws IOException {
    StringBuilder contents = new StringBuilder();
    InputStreamReader in = new InputStreamReader(System.in, "UTF-8");       
    char[] buffer = new char[MAX_INPUT];  
    int read = 0;
    do {
      contents.append(buffer, 0, read);
      read = in.read(buffer);
      } while (read >= 0);
    return contents.toString();
    }
Insgesamt finde ich den Code der Methode readEntireInput() zu lang. Für die Standardaufgabe, den gesamten Inhalt einer Datei in einen String einzulesen, sollte es eine einfachere Lösung geben - einen einzigen Methodenaufruf. Man vergleiche nur mal mit Perl 6 (Quelle: Rosetta Code)
my $string = slurp 'sample.txt';
oder mit Groovy:
def fileContent = new File("c:\\file.txt").text
Meines Wissens gibt es in Java keine vergleichbar kurze Lösung zum Einlesen einer Datei.

Keine Kommentare :