Inhaltsverzeichnis

FAQ für Plugin-Entwickler

Diese FAQ hat Reinhold (Autor von Jollina) aus einer Reihe von ausgetauschten Mails zusammengestellt.

CVS / Eclipse

Wie richte ich mir das Beispiel-Plugin in Eclipse ein?

Name Wert
Project jameica
Main-Class de.willuhn.jameica.Main
Program-Arguments „-f /home/<username>/.jameica.test -p test“. Der Parameter „-f“ gibt ein vom Standard abweichendes Plugin-Verzeichnis vor, „-p“ übergibt das Master-Passwort direkt, sodass es beim Starten im Eclipse-Degbugger nicht immer manuell eingegeben werden muss
VM-Arguments „-Djava.library.path=lib/swt/linux“ bzw. „-Djava.library.path=lib/swt/win32“ (abhängig vom verwendeten Betriebssystem)
Classpath Default-Classpath des Projekts „jameica“
  [...]
  jameica.plugin.dir.0=plugins
  jameica.plugin.dir.1=../jameica_exampleplugin
  [...]

Eclipse-Fehlermeldung: A cycle was detected in the build path of project

Dieser Fehlercode tritt auf, wenn zwei Plugin's gegenseitig auf die Daten des jeweils anderen Plugin zugreifen. Folgende Einstellungen in den Project/Properties von eclipse sind zu treffen:

Eclipse compiliert nun alle Projekte neu - fertig .

SWT

Wenn ich aus einem separaten Thread auf die GUI zugreifen will, erhalte ich eine SWTException

SWT ist single-threaded. Greift man nicht aus dem GUI-Thread heraus auf SWT-Komponenten zu, erscheint eine „SWTException: Invalid Thread Access“. Das kannst man umgehen, indem die Aktion über die SWT-Job-Queue ausgeführt wird.

  public void run()
  {
    GUI.getDisplay().syncExec(new Runnable() {
      public void run()
      {
        // Hier Programm-Code ausführen
      }
    });
  }

Kann ich ausser den GUI-Elementen von jameica auch normale SWT-Widgets verwenden?

Ja. In einer View hast du Zugriff auf das gegehörige SWT-Composite und kannst dieses beliebig erweitern:

  public class Test extends AbstractView
  {
 
    public void bind() throws Exception
    {
 
      GUI.getView().setTitle("Blubb");
 
      Composite comp = this.getParent();
 
      // Das Composite kannst man nun beliebig weiterverwenden
      // und da drauf weitere Sachen malen. Falls du z.Bsp. unter
      // der Tabelle Buttons haben willst, koenntest du das so machen:
 
      TablePart table = ....;
 
      table.paint(comp);
 
      ButtonArea buttons = new ButtonArea(comp,1);
      buttons.addButton(i18n.tr("Zurück"),new Back());
 
      // Man kann auch direkt SWT-Widgets verwenden. Z.Bsp.:
      Label l = new Label(comp,SWT.BORDER);
      l.setText("laber");
 
    }
  }

Falls Elemente nicht angezeigt werden, liegt das meist daran, dass die Angabe des Layout oder LayoutData fehlt. Beispiel für GridLayout:

  Composite comp = new Composite(getParent(),SWT.NONE);
  GridData gridData = new GridData(GridData.FILL_BOTH);
  comp.setLayoutData(gridData);
 
  // Zweispaltiges Layout, wobei die Spalten unterschiedlich breit sein koennen
  GridLayout layout = new GridLayout(2,false);
  comp.setLayout(layout);

Hinweis: Das Composite, welches man von AbstractView via getParent() erhaelt, besitzt bereits ein 1-spaltiges GridLayout.

Wann muss ich SWT-Komponenten disposen

Wenn sie als Bestandteil einer View angelegt wurden, müssen sie nicht manuell disposed werden. Das übernimmt Jameica automatisch rekursiv, sobald eine View verlassen wird.

Wie kann ich Tabellen und Bäume markierter (checkable) machen?

Mit „setCheckable()“ legt man fest, ob eine Tabelle ueberhaupt Checkboxen anzeigen soll. Da hier noch nicht klar ist, ob die Checkboxen aktiviert oder deaktiviert sein sollen, sind per Default erstmal alle aktiviert. Ob nun fuer ein konkretes Objekt die Checkbox aktiv sein soll oder nicht, kann man entweder in einem Formatter machen. Oder *nachdem* die Tabelle gezeichnet wurde (also irgendwo das paint() der Tabelle direkt oder indirekt aufgerufen wurde) auch ausserhalb eines Formatters.

Mit folgendem Code z.Bsp. wird in jeder dritten Zeile die Checkbox aktiviert:

[...]
c.addPart(table);
[...]
List<Object> objects = table.getItems();
for (int i=0;i<objects.size();++i)
{
 table.setChecked(objects.get(i),i % 3 == 0);
}

Jameica-API

Kann ich einzelne Elemente der Navigation oder des Menues zur Laufzeit aktivieren/deaktivieren?

Ja, wie folgt:

  Manifest mf = Application.getPluginLoader().getManifest(<Pluginklasse>.class);
 
  // Entweder: Ermitteln des Navigationsbaumes (links in Jameica)
  NavigationItem item = mf.getNavigation();
 
  // Oder: Menu (oben in Jameica)
  MenuItem item = mf.getMenu();
 
  // Der erste Parameter legt fest, ob das Element aktiv oder inaktiv sein soll.
  // Mit dem zweiten Parameter kann die Einstellung rekursiv für alle ggf. vorhandenen Kind-Elemente übernommen werden
  item.setEnabled(false,true);

Ich muss in meinem Programm XML-Dateien auswerten. Hilft mir da jameica?

Ja, du kannst entweder die DOM/SAX-API von Java verwenden oder den bei Jameica ohnehin mitgelieferten minimalistischen XML-Parser NanoXML verwenden. Jameica verwendet den intern, um z.Bsp. die plugin.xml zu lesen.

Beispiel:

  <?xml version="1.0" ?>
  <elster>
    <steuer typ="Lohn">
      <betrag>100.00</betrag>
      <waehrung>EUR</waehrung>
    </steuer>
  </elster>
  // Parser erzeugen
  XMLParser parser = XMLParserFactory.createDefaultXMLParser();
  parser.setReader(new StdXMLReader(new FileInputStream("test.xml")));
 
  // Root-Element "elster" ermitteln
  IXMLElement root = (IXMLElement) parser.parse();
 
  // Element "steuer" holen
  IXMLElement steuer = root.getFirstChildNamed("steuer");
 
  // Testen, ob der Typ "Lohn" ist
  if (!steuer.getAttribute("typ","").equals("Lohn"))
    return;
 
  Enumeration e = steuer.enumerateChildren();
  while (e.haseMoreElements())
  {
    IXMLElement element = (IXMLElement) e.nextElement();
    System.out.println(element.getName() + ": " + element.getContent());
  }

Kann ich die relevanten Verzeichnisse (Programm-, Config-, Datenverzeichnis) irgendwie ermitteln?

  // Liste der Plugin-Verzeichnisse
  File[] dirs = Application.getConfig().getPluginDirs();
 
  // Benutzer-Verzeichnis
  // Default:
  //   Linux: /home/<username>/.jameica
  //   Windows: C:\Dokumente und Einstellungen\<username>\.jameica
  String dir = Application.getConfig().getWorkDir();
 
  // Config-Verzeichnis des Benutzers
  // Default:
  //   Linux: /home/<username>/.jameica/cfg
  //   Windows: C:\Dokumente und Einstellungen\<username>\.jameica\cfg
  String dir = Application.getConfig().getConfigDir();
 
  // Work-Verzeichnis eines konkreten Plugins
  String dir = Application.getPluginLoader().getPlugin(<PluginClass>.class).getResources().getWorkPath();

Ich möchte in meinem Plugin Einstellungen speichern. Welche Jameica-Bordmittel sind hierfür vorhanden?

Über die Plugin-Ressourcen erhälst du ein vorkonfiguriertes Settings-Objekt mit Gettern und Settern. Ein explizites Speichern ist nicht nötig.

  Settings s = Application.getPluginLoader().getPlugin(<PluginClass>.class).getResources().getSettings();
  boolean test = s.getBoolean("foo.blubb",false);
  s.setAttribut("foo.bar","test");

Wie können Plugins miteinander kommunizieren?

Möglichkeit 1: Direkt auf die Datenservices des anderen Plugins zugreifen

Verwende hierzu die Service-Factory und schau in der plugin.xml des anderen Plugins nach den benötigten Servive-Namen.

  DBService db = Application.getServiceFactory().lookup(HBCI.class,"database");
  SammelUeberweisung s = (SammelUeberweisung)db.createObject(SammelUeberweisung.class,null);
  s.setFoo(...);
  [...]
  s.store();

Möglichkeit 2: Zugriff auf GUI-Elemente des anderen Plugins mittels Extension-System

Siehe Javadoc

Möglichkeit 3: Senden und Empfangen von Messages

Siehe Javadoc

  // Definieren eines Nachrichtentyps
  public class MyMessage implements Message
  {
    public String getText()
    {
      return "Hallo";
    }
  }
 
 
  // Abonnieren dieses Nachrichten-Typs
  public class MyConsumer implements MessageConsumer
  {
    /**
    * @see de.willuhn.jameica.messaging.MessageConsumer#handleMessage(de.willuhn.jameica.messaging.Message)
    */
    public void handleMessage(Message message) throws Exception
    {
      MyMessage m = (MyMessage) message;
      System.out.println(m.getText);
    }
 
    /**
    * @see de.willuhn.jameica.messaging.MessageConsumer#autoRegister()
    */
    public boolean autoRegister()
    {
      // Ist der MessageConsumer NICHT in einer "Inner-Class" definiert, wird
      // er beim Start von Jameica automatisch erkannt und kann ggf. auch automatisch
      // registriert werden, wenn diese Methode hier true zurueckliefert.
      // Andernfalls kann der Consumer auch manuell mit folgendem Code
      // registriert werden:
      // Application.getMessagingFactory().registerMessageConsumer(new MyConsumer());
      return true;
    }
 
    /**
    * @see de.willuhn.jameica.messaging.MessageConsumer#getExpectedMessageTypes()
    */
    public Class[] getExpectedMessageTypes()
    {
      // Legt fest, welche Arten von Nachrichten der Consumer erhalten will.
      return new Class[]{MyMessage.class};
    }
  }
 
  // Asynchrones Senden einer Nachricht:
  // Sie werden dann in einem separaten Worker-Thread an alle Abonnenten verteilt.
  Application.getMessagingFactory().sendMessage(new MyMessage());
 
  // Synchrones Senden
  Application.getMessagingFactory().sendSyncMessage(new MyMessage());
 
  // Das Senden und Zustellen der Nachrichten erfolgt plugin-übergreifend

Kann man an eine Tabelle einen Listener anhaengen, der aufgerufen wird, wenn eine Zeile der Tabelle mit einfachem Klick markiert wird?

  TablePart t = new TablePart(...);
  t.addSelectionListener(new Listener() {
    public void handleEvent(Event event)
    {
      Object o = event.data;
      // In event.data befindet sich dann direkt das markierte Fachobjekt. Falls mehrere markiert sind, ist o ein Array.
    }
  });

Ich möchte einen Listener an einen TextInput anhaengen. Wie geht das?

  final TextInput eingabeFeld1 = new TextInput("foobar");
  eingabeFeld1.addListener(new org.eclipse.swt.widgets.Listener()
  {
      public void handleEvent(Event event)
      {
        // Das Event wird immer dann ausgeloest, wenn sich der Focus
        // des Eingabe-Felds aendert. Also sowohl beim Aktivieren
        // als auch beim Deaktivieren.
        String value = (String) eingabeFeld1.getValue();
      }
  });

Kann ich Formulareingaben gliedern, zum Beispiel mit Tab-Reitern?

  public class MyView extends AbstractView
  {
    public void bind()
    {
      GUI.getView().setTitle("Test");
 
      TabFolder folder = new org.eclipse.swt.widgets.TabFolder(getParent(), SWT.NONE);
      folder.setLayoutData(new GridData(GridData.FILL_BOTH));
      folder.setBackground(Color.BACKGROUND.getSWTColor());
 
      TabGroup tab1 = new TabGroup(folder,"Tab1");
      pers.addLabelPair("Vorname",new TextInput("Max"));
      pers.addLabelPair("Nachname",new TextInput("Mustermann"));
 
      TabGroup tab2 = new TabGroup(folder,"Tab2");
      pers.addLabelPair("Strasse",new TextInput("Musterstrasse"));
      pers.addLabelPair("Ort",new TextInput("Musterhausen"));
      [...]
 
      ButtonArea colorButtons = new ButtonArea(getParent(),2);
      colorButtons.addButton(i18n.tr("Zurück"), new Back());
      colorButtons.addButton(i18n.tr("Speichern"), new Action()
      {
        public void handleAction(Object context) throws ApplicationException
        {
          [...]
        }
      });
    }
  }

Wie definiere ich einzelne Menü- oder Navigationselemente in der plugin.xml?

  <navigation>
    [...]
    <item name="Adressen"
          icon-close="page.gif"
          icon-open="page.gif"
          action="dein packagename.gui.action.AddressList" />
    [...]
  </navigation>
  public class AddressList implements Action
  {
    public void handleAction(Object context) throws ApplicationException
    {
      GUI.startView(....gui.view.AddressList.class,null);
    }
  }

Wie definiere ich in dem SQL-Create-Script für die embedded McKoi-Datenbank einen Primär-Schlüssel mit Autoincrement-Funktion?

  CREATE TABLE foo (
    id NUMERIC DEFAULT UNIQUEKEY('foo'),
    name VARCHAR(20) NOT NULL,
    [...]
    PRIMARY KEY (id)
  );

Wie kann ich die SQL-Datenbank beim ersten Start des Plugins automatisch anlegen?

Die Tabelle und die Datenbank muss man manuell anlegen. Im Beispiel-Plugin stehen die Statements in create.sql. Desweiteren muss eine Basis-Klasse des Plugins existieren, welche von AbstractPlugin abgeleitet und in plugin.xml registriert ist. Im Beispiel-Plugin heisst die Klasse „ExamplePlugin“. Sie besitzt 4 relevante Funktionen, welche von Jameica beim Initialisieren automatisch aufgerufen werden:

  public void install() throws ApplicationException
  {
    // Wenn wir im Netzwerk-Betrieb arbeiten und Jameica nicht mit
    // lokalen Daten sondern mit einem Jameica-Server arbeiten soll,
    // dann erstellen wir keine Datenbank.
    if (Application.inClientMode())
      return;
 
    PluginResources res = Application.getPluginLoader().getPlugin(ExamplePlugin.class).getResources();
    EmbeddedDatabase db = new EmbeddedDatabase(res.getWorkPath() + "/db","exampleuser","examplepassword");
 
    // Wenn wir bis hierher gekommen sind, wurde eine leere Datenbank
    // erfolgreich installiert. Wir koennen nun die Tabellen erstellen
 
    // Dazu erzeugen wir ein File-Objekt, welches auf die SQL-Datei zeigt.
    // res.getPath() liefert hierbei das Installations-Verzeichnis, in dem
    // sich das Plugin befindet.
    java.io.File file = new File(res.getPath() + "/sql/create.sql");
 
    // und fuehren es nun auf der Datenbank aus.
    db.executeSQLScript(file);
  }

Wenn nicht alles erfolgreich durchlief, werfen wir eine ApplicationException. Damit geben wir Jameica die Information, dass die Installation fehlschlug. Es wird daraufhin auf der Jameica-Startseite einen Hinweis anzeigen, dass die Installation nicht funktionierte. Jameica wird die install-Methode beim nächsten Start automtatisch wieder aufrufen. Und das geschieht solange bei jedem Start, bis die install-Funktion endlich fehlerfrei durchläuft.

Kann eine existierende McKoi-Tabelle, um Spalten erweitert werden oder gehen dabei Daten verloren?

Ja, hier kann das Statement „ALTER CREATE TABLE“ genutzt werden. Hierbei können sowohl Spalten hinzugefügt als auch existierende geändert werden. Existiert die Tabelle noch nicht, wird sie automatisch angelegt. Allerdings dürfen die neuen Spalten nicht „NOT NULL“ sein.

  ALTER CREATE TABLE testtabelle (
    id NUMERIC DEFAULT UNIQUEKEY('testtabelle'),
    beschreibung VARCHAR(20) NOT NULL,
    kommentar text,
    zahl DOUBLE,
    dasistneu charchar(40) NULL,
    UNIQUE (id),
    PRIMARY KEY (id)
  );

Wie sieht die Package-Gliederung des Codes im Beispiel-Plugin aus?

Package Beschreibung
.rmi Interfaces der Fachobjekte
.server Implementierungen der Fachobjekt-Interfaces.
.gui Grafische Benutzeroberfläche
.gui.views Die Views mit den Eingabefeldern, Tabellen usw.
.gui.controller Controller (Kopplung Fachobjekt-View
.gui.dialogs Modale Dialoge
.gui.parts Vorgefertigte grafische Komponenten (z.Bsp. fertig konfigurierte Umsatz-Tabelle
.gui.menus Context-Menus
.gui.action Klassen, die das Interface de.willuhn.jameica.gui.Action implementieren und die Aufgaben kapseln, die bei Benutzer-Interaktivität ausgelöst werden können