Eclipse Extension Points und Extensions

Veröffentlicht in: Eclipse, RCP | 1

Eclipse beinhaltet ein hoch modulares Konzept um Erweiterungen Plugin-übergreifend realisieren zu können. Das zu erweiternde Plugin bietet einen Extension-Point an, an dem andere Plugins mit ihren Extensions andocken können. Beide Arten an Plugins müssen hierbei zwangsläufig im Manifest als singleton definiert sein.

Um einen Extension-Point zu definieren, wird die plugin.xml Datei des zu erweiternden Bundles geöffnet und zum Reiter Extension Points gewechselt. Sofern keine plugin.xml Datei existiert, kann alternativ im Manifest (MANIFEST.MF) der genannte Reiter aktiviert werden, Eclipse erstellt nun automatisch die plugin.xml Datei. Nun kann ein Extension-Point erzeugt werden, als ID und Namen erhält er im folgenden Beispiel den Wert de.mz.myExtensionPoint und myExtensionPoint.


Nach erfolgreicher Erstellung des Extension-Points wird automatisch das dazugehörige Schema geöffnet. Auch wenn es nicht zwingend notwendig ist, sollten alle Attribute im Schema – Reiter Definition – definiert werden. Nur so können die Felder beim Anlegen einer Erweiterung bequem im Plug-In Manifest Editor über die UI angegeben werden können. Will man später Veränderungen am Schema vornehmen, so ist dieses in der Dateistruktur des Projektes unter schema/myExtensionPoint.exsd zu finden.

Hier wird nun ein neues Element mit dem Namen client erzeugt, dem zwei Attribute zugewiesen werden. Das Erste Attribut hat den Namen class und ist vom Typ Java. Hier hat man nun die Möglichkeit ein Interface zu hinterlegen, welches von der im Attribut class verwiesenen Klasse zu implementieren ist. Im folgenden Beispiel ist das zu implementierende Interface IMyExtension.

Das Interface selber enthält für Beispielzwecke die Methode doSomething:

public interface IMyExtension {
public void doSomething(String text);
}

Das zweite Attribut erhält den Namen text und ist vom Typ String. Hier sind keine weiteren Einstellungen nötig.

Per Context-Menü wird der extension zunächst ein Choice-Element zugewiesen, welches wiederum den Eintrag client erhält. Damit die Extension beliebig viele Client-Einträge erhalten kann, wird die Max Occurrences des Client-Elements auf Unbounded gesetzt. Eine Extension kann also mehrere Erweiterungen enthalten.

Die Plugin.xml hat nun folgenden Eintrag erhalten:

<extension-point id=“de.mz.myExtensionPoint“ name=“myExtensionPoint“ schema=“schema/myExtensionPoint.exsd“/>

Damit andere Bundles nun tatsächlich den Extension-Point erweitern können, müssen sie das Interface IMyExtension implementieren können. Daher muss im Manifest das Package des Interfaces exportiert werden:

Export-Package: de.mz

Damit ist die Definition des Extension-Points abgeschlossen und wir können uns der Erstellung einer Extensions zuwenden. Öffnen wir hierzu die plugin.xml Datei des Bundles, welches den Extension-Point erweitern soll. Auf dem Reiter Extensions kann zwischen den möglichen Erweiterungen gewählt werden, auch der zuvor erstelle Extension-Point de.mz.myExtensionPoint wird hier aufgeführt. Per Context-Menü können hier die Client-Erweiterungen hinzugefügt werden. Für die im Schema deklarierten Attribute stehen im Plug-In Manifest Editor Eingabehilfen zur Verfügung, so kann bequem für das class Attribut eine Implementierung und für das text Attribut ein String angegeben werden.

Diese Einstellung führt zum folgenden Eintrag in der plugin.xml Datei:

<extension point=“de.mz.myExtensionPoint“>
<client
class=“de.mz.MyExtensionImpl“
text=“Hallo Welt“>
</client>
</extension>

Extension-Point und eine dazu passende Extension wurden bereits erstellt, nur Verwendung findet die Erweiterungen bislang noch nicht. Doch hierzu bietet Eclipse die ExtensionRegistry an, auf die man über die Runtime-Platform Zugriff erhält:

IConfigurationElement[] extensions = Platform.getExtensionRegistry().getConfigurationElementsFor(„de.mz.extension.example.myExtensionPoint“);
for (IConfigurationElement extension : extensions) {
try {
String text = extension.getAttribute(„text“);
IMyExtension myExtension = (IMyExtension) extension.createExecutableExtension(„class“);
myExtension.doSomething(text);
// do with myExtension what you have to do
} catch (CoreException e) {
// do some logging
}
}

In obigem Beispiel erhält man alle Erweiterungen die zum Extension-Point de.mz.extension.example.myExtensionPoint gehören. Nun können die Attribute der einzelnen Erweiterungen erfragt werden – hierbei ist es nicht relevant, ob diese im Schema definiert sind – und für das Klassen-Attribut ein entsprechendes Objekt erstellen werden (createExecutableExtension). Bei einem optionalen Klassen-Feld macht es Sinn, sich zunächst zu vergewissern, dass das Attribut einen Wert zugewiesen bekommen hat. Andernfalls wird nämlich eine CoreException geworfen, die über diese Abfrage verhindert werden kann.

Auch wenn das Beispiel auf die Eclipse 3.x Plattform ausgelegt ist, funktioniert der Mechanismus grundsätzlich auch bei Eclipse 4. Hier spart man sich den Umweg über die Runtime-Platform, da sich die ExtensionRegistry bequem per Dependency Injection injizieren lässt.

Will man das Beispiel testen, kann das Laden der Extensions in einem Activator vorgenommen werden, der hierfür lediglich im Manifest eingetragen werden muss:

Bundle-Activator: de.mz.Activator

Nun kann das Extension-Point Bundle als OSGI Framework ausgeführt werden. Treten hierbei Fehler auf, sollten in der Run-Configuration zunächst alle Bundles deselektiert und anschließend nur die Plugins ausgewählt werden, die den Extension-Point und die Extensions beinhalten. Natürlich werden auch Eclipse spezifische Bundles benötigt, die sich über einen Klick auf Add Required Bundles hinzufügen lassen.

Android – Kommunikation mit der Außenwelt

Veröffentlicht in: Android | 0

Nachdem ich in einem vorangegangenen Artikel die Verwendung einer SQLite Datenbank in einer Android-Anwendung beschrieben habe (siehe http://martinzimmermann1979.wordpress.com/2011/11/17/verwendung-einer-sqlite-datenbank-in-einer-android-app/), möchte ich mich in diesem Artikel der Kommunikation mit einem REST-Service widmen. Hierfür kann die im Android-SDK integrierte Apache HttpClient Library verwendet werden. Als REST-Schnittstelle soll das Beispiel aus meinem letzten Artikel (siehe http://martinzimmermann1979.wordpress.com/2012/02/29/rest-services-mit-spring-3/) verwendet werden. Hier wurde exemplarisch gezeigt wie man auf der REST-Ressource MyBean die Operationen GET, POST, PUT und DELETE ausführt. Neben XML- wurden auch JSON-Inhalte ausgetauscht. Letzteres Datenformat wird im folgenden Beispiel benötigt. Hier nun die Java-Repräsentation der MyBean-Ressource:

public class MyBean {
private String id;
private String message;
// getter and setter for id and message
}

Im ersten Schritt soll die Android-Anwendung per GET-Request alle MyBean-Einträge erfragen und den erhaltenen JSON-Inhalt in MyBean-Objekte transformieren. Anschließend wird die Funktionalität sukzessive um die REST-Operationen POST, PUT und DELETE erweitert, wobei der POST- und PUT-Request entsprechenden JSON-Inhalt mitliefern muss.

Bevor es an die REST-Kommunikation geht, muss die Android-Anwendung die Erlaubnis zum Zugriff aufs Internet und damit folgenden Eintrag im Android-Manifest erhalten:

<uses-permission android:name=“android.permission.INTERNET“ />

Anschließend wird eine Hilfsklasse erstellt, welche die Verbindung zum REST-Service aufbaut, den erhaltenden InputStream in einen String umwandelt und für alle Arten von REST-Operationen genutzt werden kann:

public String connect(HttpRequestBase httpRequest) {
HttpClient httpclient = new DefaultHttpClient();
httpRequest.addHeader(„Accept“, „application/json“);
try {
HttpResponse response = httpclient.execute(httpRequest);
HttpEntity entity = response.getEntity();
if (entity != null) {
InputStream inputStream = entity.getContent();
String result = convertStreamToString(inputStream);
inputStream.close();
return result;
}
} catch (Exception e) {
Log.e(„rest“, e.getMessage(), e.getCause());
}
return „“;
}

Die übergebene HttpRequestBase abstrahiert die jeweilige Http-Operation, die bereits die URI der Rest-Ressource und gegebenenfalls weitere Inhalte enthält. Die Kommunikation sieht bei allen Operationen gleich aus. Zunächst wird ein HttpClient benötigt – für dieses Beispiel reicht der Standard-HttpClient aus. Da der Http-Response vom Server JSON enthalten soll, wird die Header-Information Accept: application/json hinzugefügt. Sofern der Server außer dem Status – response.getStatusLine() – mit weiteren Daten antwortet, befinden sich diese in der HttpEntity in Form eines InputStreams. Da auf mobilen Endgeräten nur begrenzt Speicher vorhanden ist, können nicht beliebig Libraries verwendet werden. So kommt man nicht Drumherum, den InputStream selbstständig in einen String zu transformieren:

private String convertStreamToString(InputStream inputStream) {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder stringBuilder = new StringBuilder(50);
String line = null;
try {
while ((line = reader.readLine()) != null) {
stringBuilder.append(line + „n“);
}
} catch (IOException e) {
Log.e(„rest“, e.getMessage(), e.getCause());
} finally {
try {
inputStream.close();
} catch (IOException e) {
Log.e(„rest“, e.getMessage(), e.getCause());
}
}
return stringBuilder.toString();
}

Der InputStreamReader dient dem InputStream als wrapper, der wiederrum von einem BufferedReader gewrappt wird. Letzterer kann zeilenweise ausgelesen und der Inhalt einem StringBuilder hinzugefügt werden. Sofern die Antwort des Servers voraussichtlich mehr als 16 Zeichen beträgt, empfiehlt es sich den StringBuilder mit einem höheren Wert zu initialisieren. Andernfalls muss der interne Buffer des StringBuilders sukzessive erhöht werden.

Nun soll die GET-Operation erstellt und die oben gezeigte connect-Methode aufgerufen werden:

HttpRequestBase httpGet = new HttpGet(REST_RESSOURCE_URI);
String result = connect(httpGet);

Anschließend enthält das result einen JSON-String folgender Art:

{„object“:{„myview“:[{„id“:“1″,“message“:“Some text“},{„id“:“2″,“message“:“Some more text“}]}}

Dieser String soll  nun geparst und der JSON-Inhalt in entsprechende MyBean-Objekte transformiert werden. Hierfür wird der komplette String zunächst einem JSONTokener übergeben. In unserem Fall liefert dessen nextValue-Methode ein JSONObject zurück, grundsätzlich wären aber auch ein JSONArray, String, NULL oder weitere Java-Typen möglich. Das so erhaltene JSONObject enthält wiederrum ein JSONObject mit dem Schlüsselwert „object“. Erst jetzt kommt man an das JSONArray „myview“, welches die gesammelten MyBean-Einträge enthält. Jeder dieser Einträge ist in einem JSONObject hinterlegt, welches die MyBean-Properties id und message enthält. Diese Werte können nun einem neu zu erzeugenden MyBean-Objekt übergeben werden:

.JSONTokener token = new JSONTokener(jsonContent);
while (token.more()) {
Object object = token.nextValue();
if (object instanceof JSONObject) {
JSONObject jsonRoot = (JSONObject) object;
try {
JSONObject jsonObject = jsonRoot.getJSONObject(„object“);
if (jsonObject != null) {
JSONArray jsonBeans = jsonObject.getJSONArray(„myview“);
if (jsonBeans != null) {
for (int i = 0; i < jsonBeans.length(); i++) {
JSONObject jsonBean = jsonBeans.getJSONObject(i);
int id = jsonBean.getInt(„id“);
String subject = jsonBean.getString(„message“);
MyBean myBean = new MyBean(id, message);
// adding myBean to a myBeans list
}
}
}
} catch (JSONException e) {
// do nothing, wrong JSON content
}
}

Soll nun eine POST- oder PUT-Operation getätigt werden, muss der umgekehrte Weg gegangen werden. Es wird nicht empfangener JSON-Inhalt deserialisiert, sondern ein MyBean-Objekt in JSON-Inhalt umgewandelt und an den Server gesendet. Hierfür wird eine Implementierung des HttpEntity-Interfaces benötigt:

public class MyHttpEntity implements HttpEntity {

private JSONObject jsonObject;

public MyHttpEntity(MyBean myBean) {
try {
jsonObject = new JSONObject();
jsonObject.put(„id“, myBean.getId());
jsonObject.put(„message“, myBean.getMessage());
} catch (JSONException e) {
Log.e(„rest“, e.getMessage(), e.getCause());
}
}

@Override
public InputStream getContent() throws IOException, IllegalStateException {
return new ByteArrayInputStream(jsonObject.toString().getBytes());
}

@Override
public long getContentLength() {
return jsonObject.toString().length();
}

@Override
public Header getContentType() {
return new BasicHeader(„Content-Type“, „application/json“);
}

@Override
public void writeTo(OutputStream outstream) throws IOException {
outstream.write(jsonObject.toString().getBytes());
}

// … further methods, which are not required
//    for this example …
}

Im Konstruktor wird das zu transformierende MyBean-Objekt übergeben und dessen Eigenschaften in einem zu erstellenden JSONObject hinterlegt. Anschließend sind folgende Methoden zu erstellen:

  • getContent – Liefert den JSON-Inhalt als InputStream
  • getContentLength – Enthält die Länge des JSON-Inhaltes
  • getContentType – Fügt die Header Information „Content-Type: application/json“ hinzu. Hierfür wird ein BasicHeader verwendet.
  • writeTo – Der JSON-Inhalt wird in einen OutputStream gespeichert.

Das Interface HttpEntity enthält noch weitere zu implementierende Methoden, deren Funktionalität spielt für dieses Beispiel aber keine Rolle. Nachdem der Serialisierung einer MyBean nichts mehr im Wege steht, kann jetzt die eigentliche POST-Operation ausgeführt werden:

HttpPost post = new HttpPost(REST_RESSOURCE_URI);
post.setEntity(new MyHttpEntity(myBean));
String result = connect(post);

Äquivalent hierzu ist der PUT-Request, nur dass dieser auf einer bestehenden MyBean getätigt wird. Daher wird in der URI noch eine id zur Identifikation der MyBean übergeben:

HttpPut put = new HttpPut(REST_RESSOURCE_URI + „/myBean/“ + myBean.getId());
put.setEntity(new MyHttpEntity(myBean));
connect(put);

Einfach gestaltet sich die DELETE-Operation, da der Request außer der zu löschenden Ressource keine weiteren Informationen benötigt. Die id der zum Löschen auserwählten MyBean wird im Pfad übergeben:

HttpDelete delete = new HttpDelete(REST_RESSOURCE_URL + „/myBean/“ + id);
connect(delete);

Ist die Hilfsklasse für die Kommunikation erst einmal geschrieben, sind die einzelnen REST-Operationen schnell umgesetzt. Lediglich bei der Serialisierung bzw. Deserialisierung von und nach JSON handelt es sich um lästige Fleißarbeit. Zumal hier jeder Fehler dank des recht langsamen Android-Emulators zusätzlich Zeit kostet.