Webservices und noch einmal alle Länder dieser Welt

1. Aufgabe

Webservices sind eine komfortable Option, Anwendungen im Internet auf der Basis von TCP/IP miteinander zu verbinden. Ein Webservice stellt Daten (auf Basis eines XML-Protokolls) zur Verfügung, die sich von einem anderen Programm nutzen lassen. Unterschiedliche Clients können diese konsumieren. Interessant an der Webservice-Technologie ist der Umstand, dass es für die Realisierung des Services wie auch des Clients unmittelbar keine Vorgaben gibt. Die Wahl der Programmiersprache (C, C++, Java, C# usw.) wie auch der Anwendungstyp (Windows/UNIX-Desktop-Anwendung, Mobile Phone Anwendung, etc.) können beliebiger Natur sein.

Natürlich müssen ein Webservice und seine Clients dieselbe Sprache verstehen. Deshalb erfolgt die Kommunikation auf Basis des standardisierten XML-Protokolls SOAP (Simple Object Access Protocol) via HTTP über TCP/IP, bei Bedarf auch mittels anderer Transportprotokolle. Im Prinzip offerieren Webservices einen programmiersprachlichen Aufruf von im Internet verfügbaren Funktionen. Vergleiche mit RPC-Calls (Remote Procedure Call, etwa „Aufruf einer fernen Prozedur“) kommen der Webservice-Technologie sehr nahe.

Weitere Zutaten der Webservice-Technologie sind eine Beschreibungssprache WSDL (Web Service Description Language), die es einem Anbieter ermöglicht, seinen Dienst formal zu beschreiben. Zum Finden und Anbinden der Dienste gibt es mit UDDI (Universal Discovery, Description and Integration) einen standardisierten Verzeichnisdienst.

Wir erstellen in diesem Beispiel noch einmal eine Android-App zur Bereitstellung einer Liste aller Länder dieser Welt. Dieses Mal stammen die Daten von einem Webservice und nicht wie im Beispiel zuvor aus einer Datenbank.

2. Lösung

Um eine Liste aller Länder betrachten zu können, ist die Wahl des Webservices auf den Dienst http://www.webservicex.net/ws/default.aspx gefallen: „WebserviceX.NET provides programmable business logic components and standing data that serve as ‚black boxes‘ to provide access to functionality and data via web services“. Etwas detaillierter betrachtet stellt der o.a. Link den Zugang zur Homepage von Cloud Computing Technologies Ltd. dar, auf der eine ganze Reihe von Webdiensten zur Verfügung gestellt werden. Der direkte Link zum Webservice lautet http://www.webservicex.net/country.asmx. Die Endung .asmx steht für all diejenigen Webdienste, die mit Hilfe des .NET-Frameworks programmiert sind.

Formal betrachtet könnten wir mit Hilfe dieses Links (genauer: http://www.webservicex.net/country.asmx?wsdl, siehe dabei den angehängten Parameter ?wsdl) ein WSDL-XML-Dokument anfordern, das auf Basis der Beschreibungssprache WSDL (Web Service Description Language) den Webservice beschreibt. Mit dieser Beschreibung ließen sich automatisiert Software-Fragmente generieren, die für unterschiedlichste Programmiersprachen den Webservice-Fernaufruf in Wrapper-Klassen kapseln. Vor allem für das Bereitstellen von Parametern und das Auswerten der Rückgabewerte stellen diese Klassen eine unverzichtbare Hilfe dar. Nur aus dem Grund, dass nachfolgende Beispiel möglichst einfach zu halten (und damit die Betrachtung Android-fremder Klassenbibliotheken zu umgehen), verzichte ich auf das Generieren eines solchen Java Webservice Proxy-Objekts.

Damit müssen wir uns den Rohdaten eines GetCountries-Aufrufs zuwenden. Glücklicherweise hält sich die Komplexität der Ergebnisdaten stark in Grenzen, womit wir nicht den Umfang der Daten, sondern ihre Strukturierung ansprechen. Stark verkürzt müssen wir in einer Android-App folgende Daten auswerten, um an eine aktuelle Liste aller Länder zu gelangen:

<?xml version="1.0" encoding="utf-8" ?>
<string xmlns="http://www.webserviceX.NET">
  <NewDataSet>
    <Table><Name>Afghanistan, Islamic State of</Name></Table>
    <Table><Name>Albania</Name></Table>
    <Table><Name>Algeria</Name></Table>
    <Table><Name>American Samoa</Name></Table>
    <Table><Name>Andorra, Principality of</Name></Table>
    <Table><Name>Angola</Name></Table>
    <Table><Name>Anguilla</Name></Table>
    <Table><Name>Antarctica</Name></Table>
    <Table><Name>Antigua and Barbuda</Name></Table>
    <Table><Name>Argentina</Name></Table>
    ...
    <Table><Name>Vanuatu</Name></Table>
    <Table><Name>Venezuela</Name></Table>
    <Table><Name>Vietnam</Name></Table>
    <Table><Name>Virgin Islands (British)</Name></Table>
    <Table><Name>Virgin Islands (USA)</Name></Table>
    <Table><Name>Wallis and Futuna Islands</Name></Table>
    <Table><Name>Western Sahara</Name></Table>
    <Table><Name>Yemen</Name></Table>
    <Table><Name>Yugoslavia</Name></Table>
    <Table><Name>Zaire</Name></Table>
    <Table><Name>Zambia</Name></Table>
    <Table><Name>Zimbabwe</Name></Table> 
  </NewDataSet>
</string>

Um letzten Endes an die Nettodaten dieses XML-Fragments zu gelangen – gemeint ist der textuelle Inhalte der <Name>-Elemente – bedienen wir uns eines XML-DOM-Parsers. Dieser ist in der Android-Klassenbibliothek mit integriert. In den Namensräumen org.w3c.dom und org.xml.sax finden wir die dazu notwendigen Klassen wie zum Beispiel Document, Element und NodeList vor.

Wenn es uns gelungen ist, die Namen der Länder aus diesem XML-Fragment zu extrahieren, sind wir von der Oberfläche nicht mehr weit entfernt. Einen Vorschlag dazu finden Sie in Abbildung 1 vor:

Ländernamen in einer Android-Webservice-App.

Abbildung 1. Ländernamen in einer Android-Webservice-App.


Eine Umsetzung der Oberfläche aus Abbildung 1 in die XML-basierte Oberflächenbeschreibungssprache ist in Listing 1 gegeben:

01: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
02:     xmlns:tools="http://schemas.android.com/tools"
03:     android:layout_width="match_parent"
04:     android:layout_height="match_parent"
05:     android:orientation="vertical" >
06: 
07:     <Button
08:         android:id="@+id/buttonRead"
09:         android:layout_width="match_parent"
10:         android:layout_height="wrap_content"
11:         android:layout_margin="3dip"
12:         android:text="@string/buttonReadCaption" />
13:     
14:     <TextView
15:         android:id="@+id/textViewStatus"
16:         android:layout_width="match_parent"
17:         android:layout_height="wrap_content"
18:         android:layout_marginLeft="3dip"
19:         android:layout_marginRight="3dip"
20:         android:layout_marginBottom="3dip"
21:         android:paddingLeft="6dip"
22:         android:text="@string/textViewStatusEmpty" />
23:      
24:     <View
25:         android:background="@android:color/black"
26:         android:layout_marginLeft="6dip"
27:         android:layout_marginRight="6dip"
28:         android:layout_width="match_parent" 
29:         android:layout_height="2dip" /> 
30:       
31:     <ListView    
32:         android:id="@+id/listviewCountries"
33:         android:layout_width="match_parent"
34:         android:layout_height="match_parent"
35:         android:drawSelectorOnTop="false"
36:         android:textSize="18sp"  
37:         android:layout_margin="3dip" />     
38: 
39: </LinearLayout>

Beispiel 1. Datei activity_main.xml: Benutzeroberflächenlayout.


Wir fahren mit der Programmierung des Logik-Anteils in Listing 2 fort:

001: package com.example.webserviceapp20;
002: 
003: import java.io.InputStream;
004: import java.io.StringReader;
005: import java.util.ArrayList;
006: import java.util.List;
007: 
008: import javax.xml.parsers.DocumentBuilder;
009: import javax.xml.parsers.DocumentBuilderFactory;
010: import javax.xml.parsers.ParserConfigurationException;
011: 
012: import org.apache.http.HttpResponse;
013: import org.apache.http.client.HttpClient;
014: import org.apache.http.client.methods.HttpGet;
015: import org.apache.http.impl.client.DefaultHttpClient;
016: 
017: import org.w3c.dom.Document;
018: import org.w3c.dom.Element;
019: import org.w3c.dom.Node;
020: import org.w3c.dom.NodeList;
021: import org.xml.sax.InputSource;
022: 
023: import android.app.Activity;
024: import android.os.AsyncTask;
025: import android.os.Bundle;
026: import android.util.Log;
027: import android.view.View;
028: import android.view.View.OnClickListener;
029: import android.widget.ArrayAdapter;
030: import android.widget.Button;
031: import android.widget.ListView;
032: import android.widget.TextView;
033: 
034: public class MainActivity extends Activity implements OnClickListener  {
035:     
036:     private Button buttonRead;
037:     private TextView textViewResult;
038:     
039:     private ListView listViewCountries;
040:     private ArrayAdapter<String> countriesAdapter;
041:     
042:     @Override
043:     protected void onCreate(Bundle savedInstanceState) {
044:         super.onCreate(savedInstanceState);
045:         this.setContentView(R.layout.activity_main);
046:         
047:         // connect event handler
048:         this.buttonRead = (Button) this.findViewById(R.id.buttonRead);
049:         this.buttonRead.setOnClickListener(this);
050:         
051:         this.textViewResult = (TextView) this.findViewById(R.id.textViewStatus);
052:         this.textViewResult.setText ("");
053:         this.listViewCountries = (ListView) this.findViewById(R.id.listviewCountries);
054:         
055:         // connect list view with adapter
056:         ArrayList<String> empty = new ArrayList<String>();
057:         this.countriesAdapter =
058:             new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, empty);
059:         listViewCountries.setAdapter (this.countriesAdapter); 
060:     }
061: 
062:     @Override
063:     public void onClick(View v) {
064:         
065:         int id = v.getId();
066:         if (id == R.id.buttonRead) {
067:             
068:             // clear former entries of list view
069:             this.textViewResult.setText ("");
070:             this.countriesAdapter.clear();
071:             this.countriesAdapter.notifyDataSetChanged();
072:             
073:             // populate list view on behalf of a background thread pool 
074:             AsyncTaskHelper helper = new AsyncTaskHelper();
075:             helper.execute("http://www.webservicex.net/country.asmx/GetCountries");
076:         }        
077:     }
078:     
079:     // private helper methods
080:     private InputStream openHttpGetConnection (String url) {
081:         
082:         InputStream is = null;
083:         
084:         try {
085:             HttpClient client = new DefaultHttpClient();
086:             HttpGet get = new HttpGet (url);
087:             HttpResponse response = client.execute(get);
088:             is = response.getEntity().getContent();
089:         }
090:         catch (Exception e) {
091:             Log.e("PeLo", "openHttpGetConnection failed: " + e.getMessage());
092:         }
093: 
094:         return is;
095:     }
096:     
097:     // private inner helper class for asynchronous operations
098:     private class AsyncTaskHelper extends AsyncTask<String, String, String[]> {
099: 
100:         @Override
101:         protected String[] doInBackground(String... urls) {
102:             
103:             String[] result = new String[] { "" };
104:             
105:             // retrieve input stream from HTTP client
106:             this.publishProgress("Open http connection ...");
107:             InputStream is = openHttpGetConnection(urls[0]);
108:             if (is == null) {
109:                 return result;
110:             }
111:             
112:             // need Document Object Model parser
113:             DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
114:             DocumentBuilder builderOuter;
115:             Document docOuter = null;
116:             
117:             this.publishProgress("Parsing outer xml response document ...");
118:             try {
119:                 builderOuter = factory.newDocumentBuilder();
120:                 docOuter = builderOuter.parse(is);
121:             }
122:             catch (ParserConfigurationException e) {
123:                 Log.e ("PeLo", "DOM Configuration failed: " + e.getMessage());
124:                 return result;
125:             }
126:             catch (Exception e) {
127:                 Log.e ("PeLo", "DOM parsing failed: " + e.getMessage());
128:                 return result;
129:             }
130:             
131:             // parsing "outer" envelope of response message
132:             Element root = docOuter.getDocumentElement();
133:             
134:             // moving to "<string>" node
135:             Node stringChild = root.getFirstChild();
136:             String innerContents = stringChild.getNodeValue();
137:             
138:             // parsing "inner" envelope of response message
139:             // (need a second XML DOM parser)
140:             DocumentBuilder builderInner;
141:             Document docInner = null;
142:             
143:             this.publishProgress("Parsing inner xml response document ...");
144:             try  {
145:                 builderInner = factory.newDocumentBuilder();
146:                 StringReader sr = new StringReader (innerContents);
147:                 InputSource innerSource = new InputSource (sr);
148:                 docInner = builderInner.parse (innerSource);
149:             }
150:             catch (Exception e) {
151:                 Log.e ("PeLo", "DOM parsing failed: " + e.getMessage());
152:                 return result;
153:             }
154:             
155:             List<String> listCountries = new ArrayList<String>();
156: 
157:             if (docInner != null) {
158:                 
159:                 // move to "<NewDataSet>" node
160:                  Element rootInner = docInner.getDocumentElement();
161:                  if (rootInner.hasChildNodes()) {
162:                      
163:                      // move to "<Table>" nodes 
164:                      NodeList listTableNodes =  rootInner.getChildNodes();
165:                      for (int i = 0; i < listTableNodes.getLength(); i ++) {
166:                          
167:                          Node tableNode = listTableNodes.item(i);
168:                          
169:                          // skip "whitespace" text nodes
170:                          if (tableNode.getNodeType() != Node.ELEMENT_NODE)
171:                              continue;
172: 
173:                          // move to "<Name>" nodes
174:                          if (tableNode.hasChildNodes()) {
175:                               
176:                              NodeList listNameNodes =  tableNode.getChildNodes();
177:                              for (int j = 0; j < listNameNodes.getLength(); j ++) {
178:                                  
179:                                 // skip again "whitespace" text nodes
180:                                  Node nameNode = listNameNodes.item(j);
181:                                  if (nameNode.getNodeType() != Node.ELEMENT_NODE)
182:                                      continue;
183:                                  
184:                                  // retrieve name of country
185:                                  String value = nameNode.getTextContent();
186:                                  listCountries.add(value);
187:                              }
188:                          }
189:                      }
190:                  }
191:             }
192: 
193:             this.publishProgress("Creating resulting string array ...");
194:             result = listCountries.toArray(new String[listCountries.size()]);
195:             return result;
196:         }     
197:         
198:         @Override
199:         protected void onProgressUpdate(String... progress) {
200:             textViewResult.setText (progress[0]);
201:         }
202:         
203:         @Override
204:         protected void onPostExecute (String[] result) {
205:             // this method is invoked on the UI thread
206:             for (int i = 0; i < result.length; i ++)
207:                 countriesAdapter.add(result[i]);
208:             countriesAdapter.notifyDataSetChanged();
209:             textViewResult.setText ("Added  " + result.length + " entries.");
210:         }
211:     }
212: }

Beispiel 2. Datei MainActivity.java: Implementierung der Logik.


Auf einige Abschnitte aus Listing 2 sollten wir noch etwas näher eingehen. Das Android-Programmiermodell sieht Methodenaufrufe im UI-Thread, deren Laufzeit nicht von vornherein absehbar ist (wie beispielsweise der Zugriff auf einen Webdienst), als unerwünscht an. Aus diesem Grund gibt es mehrere Möglichkeiten, solche Aufrufe in sekundäre Threads der Anwendung auszulagern. Eine davon ist der Gebrauch der Klasse AsyncTask und ihrer Methode execute. Die Klasse AsyncTask sollte am besten spezialisiert werden, um ihre zentralen Methoden doInBackground, onProgressUpdate und onPostExecute geeignet überschreiben zu können. Einen Überblick dazu vermittelt das folgende Code-Fragment:

private class AsyncTaskHelper extends AsyncTask<String, String, String[]>

    @Override
    protected void onPreExecute () {
        ...
    }

    @Override
    protected String[] doInBackground (String... urls) {
        ...
    }

    @Override
    protected void onProgressUpdate (String... progress) {
        ...
    }

    @Override
    protected void onPostExecute (String[] result) {
        ...
    }
}

Ein Aufruf der execute-Methode an einem AsyncTask-Objekt zieht folgende vier Schritte nach sich:

  • Aufruf von onPreExecute – Wird im Kontext des UI-Threads aufgerufen. Dient zum Einrichten des asynchronen Vorgangs.

  • Aufruf von doInBackground – Wird im Kontext eines Hintergrund-Threads(!) aufgerufen und führt die asynchrone Tätigkeit aus. Rückmeldungen an den UI-Thread der Anwendung können mit Hilfe der publishProgress-Methode durchgeführt werden.

  • Aufruf von onProgressUpdate – Ausführung im UI-Thread der Anwendung. Diese Methode dient dem Zweck, Zwischenergebnisse oder einfach auch nur eine Wasserstandsmeldung der asynchronen Tätigkeit in den UI-Thread einzuschleusen.

  • Aufruf von onPostExecute – Wird im Kontext des UI-Threads nach der Beendigung der asynchronen Tätigkeit aufgerufen.

In der doInBackground-Methode des vorliegenden Beispiels wird im Wesentlichen der Webservice-Aufruf abgesetzt und das Ergebnis ausgewertet. Dies bedeutet, mit Hilfe der XML-DOM-API-Schnittstelle ein XML-Fragment zu traversieren und die in den untersten Blättern des DOM-Baumes angesiedelten Ländernamen zu extrahieren. Das Extrakt der Ländernamen wird in einem String[]-Objekt zusammengestellt. Dies hat den Grund, dass ein ArrayAdapter-Objekt, wie der Name erwarten lässt, ein Feld erwartet und speziell für Android-ListView-Objekte konzipiert ist. Erfolgen Änderungen im ArrayAdapter-Objekt, kann das korrespondierende ListView-Objekt durch einen Aufruf von notifyDataSetChanged gezwungen werden, seine Ansicht zu aktualisieren.