Unit-Tests ala Schablonenmethode

Richtig Spaß macht eine Website, wenn man sich ein paar Cent Spielgeld verdient. Ich habe mich früher einmal mit der Amazon Product Advertising API beschäftigt. Die API hat sich etwas geändert. Man bekommt weniger Daten von Amazon und soll Kunden früher zur Ziel-Website umleiten. Reviews kommen nicht mehr als Text per herüber, sondern man bekommt einen Link, dessen Inhalt man in einem HTML-IFrame einbinden kann. Anfragen müssen neuerdings zwingend mit einem privaten Schlüssel signiert abgeschickt werden. Die nötigen XML-Parsereien entwickele ich testgetrieben. Ich schreibe zuerste einen Test, dann programmiere ich gegen den Test. Bei solch einer hohen Testabdeckung hat man die Sicherheit, dass beim Refaktorisieren nichts kaputt geht. In der reinen Theorie sollten Unit-Tests nur Logik testen, also unabhängig von einer Datenquelle wie Festplatte oder Internet sein. Ich finde das aber zu extrem. Ich will von Amazon ausgehend von einer Klassifikationsnummer (Browsenode) die untergeordneten Klassifikationsnummern und die Bestseller dieser Browsenodes. Ich erstelle mir also eine Klasse Browsenode, deren Instanz mir untergeordnete Browsenode-Objekte zurückgibt. Außerdem schreibe ich die Klasse Topseller, die die Attribute eines Topsellers speichert, und ein Browsenode-Objekt soll mir seine Topseller liefern. Zuerst mache ich in meinen Tests echte Anfragen an den Amazon-Server. Am nächsten Tag stelle ich aber fest, dass die Bestseller sich schon geändert haben. So schnell kann es gehen. Was tun? Ich ändere meine Klasse Browsenode ala Schablonenmethode-Entwurfsmuster (template method design pattern) in eine abstrakte Klasse um und deklariere die Methode, die mir Unterobjekte zurück liefert, sowie die Methode, die mir für verschiedene Anfrage-Parameter ein XML-Document erzeugt, abstrakt. Wie das Design Pattern definiert, verlege ich das Skelett des Produktdaten-Algorithmus in die abstrakte Klasse und einzelne Schritte werden in den Unterklassen verschieden implementiert. Jetzt schreibe ich mir noch die Klasse BrowsenodeImpl, welche ich im produktiven Code benutze und eine Klasse BrowsenodeMock, welche ich für die Tests verwende. BrowseNodeMock greift auf lokale Xml-Dateien zu und BrowsenodeImpl auf den Amazon-Server.

Über den Berg

Ich bin über den Berg, oder wie man so sagt. Da ich in meiner neuen Arbeitsstelle mit Java EE zu tun habe, möchte ich mich gerne in meiner privaten Programmierung auch diesem Thema widmen. Nun müssen es aber keine Kunden- und Herstellernummern sein, sondern etwas, was mir ausreichend Ausgleich bietet. Was ich schon immer vorhatte: Ich entwickle eine Anwendung, mit der man mit mehr Spaß als durch bloßes Lesen den am weitesten verbreiteten und am häufigsten übersetzten unumschränkten Weltbestseller, nachfolgend und folgerichtig Das Buch genannt, erkunden kann. Nämlich soll man anhand von Lückentexten verifizieren können, ob man die Redewendungen, die unsere Kultur so sehr im Griff haben, wirklich drauf hat.

Die Datenbank kennt wieder jedes einzelne Wort persönlich, nämlich um welchen POS (Part of Speech) es sich handelt, wie das Lemma (Grundform) heißt und ob es Bestandteil einer Nominalphrase ist. Nachdem ich vier Übersetzungen in die Datenbank (MySQL) geschrieben habe, sind es 188 MB. Datenbankseitig benutze ich natürlich JPA. Anfangs bin ich etwas besorgt um die Performance. Denn meine Datenstruktur sieht so aus: Jeweils eine Entity für Buch, Kapitel, Vers und Wort. Nur für den Buchstaben gibt es kein Entity. Für ein Wort gibt es noch einen Kommentar, der dahinter angezeigt werden kann und ein Buch hat eine Map von Maßen der Wichtigkeit des Wortes innerhalb des Buches für alle relevanten Wörter. Dies sind Wörter, die einer Nominalphrase angehören oder ein Verb, Nomen, Adjektiv, Adverb, … sind. Die Datenbankschicht hat also einiges zu leisten, um nur den Text zu lesen. Soll auch noch ein Lückentext erzeugt werden, wird ordentlich gerechnet. Auf meinem Raspberry Pi installiert ist die Performance tatsächlich unter aller Kanone. Auf meinem betagten Rechner allerdings läuft es wirklich sehr flott. Der Pi ist ungefähr 10 mal langsamer, wirklich eine lahme Möhre.

Als ausbaufähiges Projekt nehme ich das Beispielprojekt der Autoren des Buches JavaServer Faces 2.2, von welchem ich bisher immer noch nur den konzeptionellen Teil gelesen habe. Das ist auch immerhin der wichtigste Teil, den man kann sich ja wirklich alles ergooglen und seit stackoverflow ist der ersuchte Inhalt auch sehr fundiert. Das Projekt ist natürlich overdrived, aber besser overdrived als underdrived. Ausschlachten kann man immer, hinzuschlachten ist schwieriger, so mein Gedanke. Das Projekt bietet eine Datenbank- und eine Oberflächenschicht. Datenbankseitig ist die Komplexität genau richtig, schließlich geht es den Autoren hauptsächlich um die Oberfläche. Was ich letztendlich von den Projekten behalte sind aber nur die Dependencies aus der POM-Datei, der Rest ist sehr einfach. Die Dependencies bieten mir auf einem Apache Tomcat 7 JPA, CDI, JSF und alle möglichen JEE 6-Features, indem zwischen Tomcat und den JEE-Implementierungen der Geronimo-Server aufgesetzt wird.

Jetzt an die Oberfläche. Die Anwendung soll Spaß machen, muss also auf mobilen Geräten laufen. Schließlich will ich überhaupt nicht zurückbleiben und eine Oberfläche für mobile Geräte ist Pflicht. In einer Präsentation von Richfaces habe ich auch einen guten Grund gefunden, eine webbasierte Oberfläche zu entwickeln. Benutzer wollen schließlich nicht für jede Anwendung eine App herunter laden. Einen Browser hat aber jeder. Als erstes muss eine App daher eine Web-Oberfläche für Desktop und mobile Geräte bieten. Danach kann gerne eine native App kommen. Das Web hat den Vorteil, dass man die meisten Nutzer damit sofort erreichen kann. Zuerst versuche ich mein Glück mit Primefaces. Das sieht sogar auf mobilen Geräten gar nicht schlecht aus, lässt aber noch etwas zu wünschen übrig. Als nächstes probiere ich Primefaces mobile. Das funktioniert wesentlich schlechter. Mein Lückentext wird programmatisch erzeugt und unter einer HtmlPanelGroup zusammengefasst. Leider wird diese Komponente aber nicht per Ajax aktualisiert. Auch eine SelectOneMenu-Auswahl soll sich auf eine andere SelectOneMenu-Auswahl auswirken, diese wird aber auch nicht aktualisiert. Was unter dem konventionellen Primefaces noch ging, geht unter mobile nicht mehr. Jetzt mache ich es selbst und setze Primefaces an die frische Luft. Ich arbeite mich in JQery mobile ein und da geht es aber auch nicht. Aber wenn ich es selbst mache, kann ich auch in die Handwerkskiste greifen. Nachdem ich den JQery mobile-Zauber herausnehme, sehe ich, dass es wunderbar funktioniert. Also wird nur das CSS nicht aktualisiert. Ich lese, dass ich das mit $('#meineForm').enhanceWithin(); erzwingen muss. Die programmatisch erzeugte HtmlPanelGroup für den Lückentext wird immer noch schwer aktualisiert, so dass ich dafür einfach eine extra Seite nehme, die immer neu geladen wird. Das macht auch Sinn, da die Seite gänzlich aus Komponenten besteht, die immer aktualisiert werden müssen.

Jetzt wird die Sache noch deployed (Schreckliches denglich finde ich übrigens. Aber so weiß jedenfalls jeder, was ich meine). Mein Pi ist jedenfalls unbrauchbar geworden. Letztens kam unser neuer Wohnzimmerschrank. Meine Frau hat einen Test gemacht, ob er die richtige Entfernung zur Steckdose hat, indem sie öfter mal den Stecker vom Pi rein- und rausgesteckt hat. Dann lade ich die Anwendung eben auf openshift hoch. Im Endeffekt ist das ganze auch wirklich sehr einfach, wenn man nicht Eclipse benutzt, sondern die Shell. Die Anwendung wird sehr komfortabel per SSH auf mein openshift-Konto geladen. Mit SSH lade ich auch mein SQL-Dump der lokalen Datenbank hoch und benutze die MySQL-Shell, um das Backup remote einzuspielen. Es gibt von openshift auch zielgerichtete Dokumentationen zu dem Thema und openshift sieht mir nach sehr viel versprechendem Webhosting aus! Hier kann man sehr viel kostenlos ausprobieren und man hat viele verschiedene Technologien zur Verfügung. So ein Webhosting habe ich noch nicht erlebt!

Los gehts

NSA im eigenen Auftrag

Ich habe eine neue Möglichkeit entdeckt, Notizen zu machen, nämlich indirekt durch jegliche Eingaben ins Handy. Ich ergoogle ja mein ganzes Leben. Deshalb will ich meine Eingaben protokollieren und zu meinem Pi senden. Sozusagen NSA in eigener Regie. Am Ende des Jahres eine Statistik ansehen können, was ich das Jahr lang im Herzen gehabt habe. Und mit solchen ganz nebenbei erstellten Notizen kann ich ja etwas anfangen, z.B. automatisiert Nachrichten oder Filme suchen. Unter Android kann man die Software-Tastatur selbst programmieren. Ich lade mir den Quellcode von Gingerbread-Keyboard herunter. Nachdem ich den nativen Teil mit dem Android NDK kompiliert habe, kann ich das Projekt mit Eclipse auf meinem Handy installieren und den Quellcode ändern. Ich finde verschiedene Methoden, um den Text zu unterschiedlichen Zeitpunkten aus dem Ziel-TextView zu lesen. Alle Methoden muten frickelig an. Es ist also eine interessante, aber weniger übliche Anwendung eines InputMethodService. Wenn sich das Tastaturfenster nach der Eingabe wieder schließt, wird der eingegebene Text verschlüsselt an meinen Raspberry Pi gesendet und in die Datenbank eingetragen. Ein Problem liegt darin, dass nicht alle Eingaben im Ziel-TextView landen. Bei Autocomplete-TextViews wird ein vervollständigtes Wort direkt ausgewählt statt erst im Zielfenster ankommen zu müssen. Allerdings kann man gerade in meinem wichtigsten Eingabefenster, der Adresseingabe des Chrome-Browsers den autovervollständigten Text zuerst ins Zielfenster schubsen um dann darauf aufbauend weitere Vervollständigungen der Suchphrase zu erhalten. Mit etwas Disziplin schneidet man dann doch den gesamten eingegebenen Text mit.

JBoss stoppen

Wie werde ich die Geister, die ich rief, auch wieder los? Wie stoppe ich den JBoss-Zauber?

./jboss-cli.sh --connect controller=mei.ne.ip.adresse:9999 command=:shutdown

Erst einmal wurde das jboss-admin.sh-Skript in jboss-cli.sh umbenannt. Und dann hat der Befehl, aus welchem Grund auch immer, localhost oder 127.0.0.1 nicht akzeptiert, auch wenn in der /etc/hosts-Datei die entsprechenden Einträge vorhanden waren.

Gestartet hatte ich den Zauber mit

./standalone.sh -b mei.ne.ip.adresse -bmanagement=mei.ne.ip.adresse &

Das & sorgt dafür, dass der Prozess im Hintergrund gestartet wird, so dass ich das Terminal-Fester schließen kann, ohne dass ich den Prozess dadurch verenden lasse.

SSLSocket unter Android

Nach einigen Änderungen kann ich jetzt auch von Android eine Verbindung zu meinem SSLServerSocket aufbauen. Aus meiner gestern generierten Schlüsselaufbewahrungsdatei keystore.jks, welche den privaten und den öffentlichen Schlüssel beherbergt, exportiere ich ein Zertifikat cert.pem

keytool -export -keystore keystore.jks -file cert.pem

Da Android nur mit BouncyCastle-formatierten Keystores etwas anfangen kann, erzeuge ich nun einen solchen, indem ich das eben erzeugte Zertifikat importiere:

wget http://bouncycastle.org/download/bcprov-jdk16-146.jar
keytool -importcert -file cert.pem -keystore android.store -provider org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath bcprov-jdk16-146.jar -storetype BKS

Eine beispielhafte Android-Activity sieht nun so aus:

package de.codeschneider.sslmessageclient;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;

import android.os.Bundle;
import android.os.StrictMode;
import android.app.Activity;
import android.content.res.Resources.NotFoundException;
import android.view.Menu;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
      //passphrase for keystore
        char[] keystorePass="pwd".toCharArray();

        TrustManagerFactory trustManagerFactory = null;
        StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();

        StrictMode.setThreadPolicy(policy); 
        
        try {
            KeyStore keyStore=KeyStore.getInstance("BKS");
			keyStore.load(getApplicationContext().getResources().openRawResource(R.raw.android),keystorePass);
	        //create a factory
	        trustManagerFactory=TrustManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
	        trustManagerFactory.init(keyStore);
	        SSLContext sslContext=SSLContext.getInstance("TLS");

	        //init context
	        sslContext.init(
	            null,
	            trustManagerFactory.getTrustManagers(), 
	            new SecureRandom()
	        );

	        
	        //create the socket
	        Socket socket=sslContext.getSocketFactory().createSocket("www.meine.schoene.inetadresse.eu",1111);
	        socket.setKeepAlive(true);        

            InputStream inputstream = getApplicationContext().getResources().openRawResource(R.raw.echoclient);
            InputStreamReader inputstreamreader = new InputStreamReader(inputstream);
            BufferedReader bufferedreader = new BufferedReader(inputstreamreader);

            OutputStream outputstream = socket.getOutputStream();
            OutputStreamWriter outputstreamwriter = new OutputStreamWriter(outputstream);
            BufferedWriter bufferedwriter = new BufferedWriter(outputstreamwriter);

            String string = null;
            while ((string = bufferedreader.readLine()) != null) {
                bufferedwriter.write(string + '\n');
                bufferedwriter.flush();	
            }
	        
		} catch (NoSuchAlgorithmException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (CertificateException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (NotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (KeyStoreException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (KeyManagementException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

        //get context
    }


    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }
    
}

Die Activity nimmt den Keystore aus der Resourcen und übermittelt eine Datei über die hergestellte sichere Verbindung. Man verzeihe mir die Policy, alles zu erlauben. Normalerweise darf man unter Android keine Netzwerkverbindungen im Main-Thread aufbauen, sondern muss dieses nebenläufig tun. Ich wollte aber einfach nur sehen, ob meine Verschlüsselung funktioniert. Hurrah!

Sichere Notizen von SSLSocket an SSLServerSocket

Das nächste Problem: Wie sende ich von einem Android-Client verschlüsselt Nachrichten an einen Server, der stets an einem bestimmten Port horcht? So eine richtig fette JEE-Bibliothek finde ich nicht. Stattdessen finde ich einfache alteingesessene Klassen, nämlich javax.net.ssl.SSLSocket und javax.net.ssl.SSLServerSocket. Zuerst muss ich mit diesem Befehl ein Zertifikat generieren:

keytool -genkey -keystore mySrvKeystore -keyalg RSA

Damit der horchende Server nicht mit der Client-Verbindung stirbt, erzeuge ich für jede Verbindung einen Thread:

import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocket;

public
class EchoServer {
    public static void main(String[] arstring) {
        try {
            SSLServerSocketFactory sslserversocketfactory =
                    (SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
            SSLServerSocket sslserversocket =
                    (SSLServerSocket) sslserversocketfactory.createServerSocket(1111);



            String string = null;
            while (true) {
                SSLSocket sslsocket = (SSLSocket) sslserversocket.accept();
            	ServerThread t = new ServerThread(sslsocket);
            	t.start();
            }
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }
}

Der Thread liest dann solange, bis die Verbindung vom Client beendet wird, und gibt dann seinen Geist auf:


import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;


public class ServerThread extends Thread {
	private Socket s;

	public ServerThread(Socket s) {
		this.s = s;
	}

	public void run() {
		// lesen
		try{
			
		BufferedReader in = new BufferedReader(new InputStreamReader(
				s.getInputStream()));
		String text = null;
		BufferedWriter out = new BufferedWriter(new OutputStreamWriter(
				s.getOutputStream()));
		
		while ((text = in.readLine()) != null){
			System.out.println(text);
			// schreiben
			out.flush();
			
		}
		System.out.println("clean up");
		// aufräumen
		out.close();
		in.close();
		
		} catch (Exception e){
			
		}

	}
}

Dank einer neuen Portweiterleitung meines Routers, der mich mit dem Internet verbindet, zum Pi, wird die Anfrage des Clients an Port 1111 zum Pi weiter geleitet:

import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import java.io.*;

public
class EchoClient {
    public static void main(String[] arstring) {
        try {
            SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
            SSLSocket sslsocket = (SSLSocket) sslsocketfactory.createSocket("www.meine.schone.inetadresse.de", 1111);
            //SSLSocket sslsocket = (SSLSocket) sslsocketfactory.createSocket("localhost", 1111);

            InputStream inputstream = System.in;
            InputStreamReader inputstreamreader = new InputStreamReader(inputstream);
            BufferedReader bufferedreader = new BufferedReader(inputstreamreader);

            OutputStream outputstream = sslsocket.getOutputStream();
            OutputStreamWriter outputstreamwriter = new OutputStreamWriter(outputstream);
            BufferedWriter bufferedwriter = new BufferedWriter(outputstreamwriter);

            String string = null;
            while ((string = bufferedreader.readLine()) != null) {
                bufferedwriter.write(string + '\n');
                bufferedwriter.flush();
            }
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }
}

Nun kann ich sowohl Client als auch Server auf dem Raspberry Pi starten:

java -Djavax.net.ssl.keyStore=mySrvKeystore -Djavax.net.ssl.keyStorePassword=meinPwd EchoServer
java -Djavax.net.ssl.trustStore=mySrvKeystore -Djavax.net.ssl.trustStorePassword=meinPwd EchoClient

Ich hatte schon befürchtet, dass die while-Schleife im Serverprogramm die Prozessorlast auf 100% hochtreibt, aber dem ist nicht so. Jetzt muss ich den Clientcode nur noch auf Android portieren.

Kanonen auf Spatzen: JEE-Notizblock auf dem Raspberry Pi

Weihnachtszeit – Bastelzeit. Für mich lag ein Raspberry Pi unter dem Weihnachtsbaum, und zwar der mit 512 MB Arbeitsspeicher. Lange habe ich davon geträumt, einen eigenen Server im 24/7-Betrieb laufen zu lassen. Es ist wie mit den Smartphones. Irgendwann kam der Augenblick, wo diese Dinger tatsächlich handliche Hochleistungsmaschinen waren und die Internetverbindungen erschwinglich. Der Raspberry kostet mit Gehäuse – den Luxus gönne ich mir -, Netzteil, SD-Card und Netzwerkkabel zum Anschluss an den Router ca. 60 Euro und bei 3,5 Watt Stromverbrauch weniger als 8 Euro pro Jahr an Strom. Er bietet eine große Spielwiese. Da ich ihn kopflos (headless), d.h. remote über SSH, betreibe, kann ich endlich mein 1000-Seiten-Totschläger-Linux-Buch gebrauchen und werde fit im Kommando-Schreiben. Zuerst wollte ich mir eigentlich einen ShevaPlug kaufen. Aber ich habe keinen einzigen Bericht von Benutzern gelesen, denen es nicht durchgeschmort ist, sowohl die alten als auch die neuen Modelle. Der Raspberry ist aber so schwach auf der Brust, dass er kühl bleibt. Das ist mir immer lieber als Hochleistung, aber durchbrennen.

Es gibt genug gleich lautende Anleigung, um JBoss auf dem Raspberry Pi zu installieren. Daher kann ich darauf verzichten. Von mir wie immer nur einen lockeren Overview. Zuerst lade ich mir die eingebettete Version von Java herunter auf den Pi. Das ist eine extra für Arm-Prozessoren angepasste Version. Java ist ja wirklich flott geworden. Es gibt ja auch eine extra virtuelle Maschine für Android. Mein Raspberry-Java hat aber den kompletten Umfang. Dann lade ich mir den aktuellen JBoss herunter und lasse ihn mit dem vorbereiteten Java laufen. So ganz ohne Anwendungen fährt er sehr schnell hoch und der Speicherverbrauch liegt bei schlappen 10%, also ca. 50 MB. Das zum Thema Schwergewicht. Da hätte ich Schlimmeres erwartet. Auch wenn ich noch keine Anwendungen installiert habe, bringt der JBoss eine Management-Anwendung mit, die mit dem gleichen Speicherverbrauch flüssig läuft.

Als nächstes installiere ich den MySQL-Server und richte ihn so ein, dass ich von einem anderen Rechner aus mit MySQL-Workbench komfortabel darauf zugreifen kann. Jetzt schreibe ich mit dieser remote-Verbindung meine Entities mit JPA und lasse meine Datenbankstruktur automatisch erzeugen. Das Deployen ist interessant. Bisher habe ich das immer mit Eclipse-Magic gemacht. Jetzt verpacke ich mein .war-Archiv und entpacke es auf dem Server und erzeuge eine daneben eine .dodeploy-Datei und schon wird die Anwendung installiert. Der Speicherbrauch erhöht sich ein bisschen. Nun startet der JBoss wesentlich langsamer. Die JPA-Performance überzeugt mich leider nicht direkt. Eine einfache Abfrage einer einzigen Tabelle dauert sichtlich Zeit, so dass die Mini-Anwendung oberflächentechnisch verlangsamt wird.

Ich weiß jetzt übrigens auch, wie ich den Webserver meines Pi auch über das Internet zugänglich mache. Den Großteil des Problems löse ich mit meinem Router, der Fritzbox. Hier gebe ich zuerst meinem Raspberry Pi eine feste IP-Adresse, damit er nicht per DHCP bei jedem Reboot eine neue Adresse zugewiesen bekommt. Nun kann ich ein Portforwarding vornehmen. Ich weise die Fritzbox an, eine HTTP-Anfrage an Port 80 aus dem Internet an Port 8080 meines Pi weiter zu leiten. Nun brauche ich noch einen sprechenden Namen für mein Internetangebot und einen Dienst, der die ständig wechselnde IP-Adresse meines Routers mit diesem Namen verknüpft. Dafür gibt es Dienste wie DynDNS, welcher allerdings seit Neustem nicht mehr kostenfrei ist. Im Steuermenü meiner Fritzbox kann ich einige dieser Dienste auswählen. Ich wähle selfhost. Nun sagt die Fritzbox bei jedem IP-Wechsel selfhost Bescheid, so dass ich mit einem gleichbleibenden Namen auf meinen Webserver zugreifen kann. Will man das selfhost im Namen vermeiden, muss man etwas bezahlen und bekommt einen ganz normalen Namen, ohne Werbung. So ganz umsonst ist dieser Service also auch nicht.

Ich plane, den Pi als einen allezeit verfügbaren Notizblock zu benutzen, um meine Ideen zu pflegen und auswerten zu können. Der Webserver und die Datenbankverbindung laufen. Jetzt muss ich noch einen Webservice oder Ähnliches programmieren. Dazu brauche ich noch eine Android-Oberfläche, die mittels gesicherter Verbindung die wichtigen Notizen weiterleitet. Und das so schnell wie möglich. Ich will schließlich nicht meinen ganzen Urlaub am Rechner verbringen. Wenn man im Urlaub programmiert, kann man eben auch noch Schnelligkeit lernen.

Sommer, Sonne, Englischtest

Die Schulferien beginnen, die Sonne strahlt und meine Kinder müssen was lernen, so in der ersten Tageshälfte. Jetzt, da ich meine fantastische Textanalysesoftware geschrieben habe, und meine Tochter dringend Nachhilfe in  Englisch braucht, probiere ich mal aus, ob ich die Software auch einfach mit den englischen linguistischen Modellen initialisieren kann, um Sätze zu zerlegen und POS-Tags, Lemmas und Nominalphrasen zu bestimmen, so dass ich dann auch englische Lückentexe erzeugen kann. Ja, es geht tatsächlich.

Originaltext:

The ball is on the floor. It is a red ball. It is a rubber ball. The baby looks at the ball. The cat looks at the ball. The cat is black. The cat walks over to the ball. The cat hits the ball with its paw. The ball rolls on the floor. The baby smiles.

Lückentext:

The ball ——— on the ———. It ——— a red ball. It ——— a rubber ball. The baby looks at the ———. The ——— looks at the ———. The cat is black. The ——— walks over to the ———. The cat ——— the ball with its paw. The ——— ——— on the ———. The baby smiles.

Ich habe meinen Generator angewiesen, jedes x-te Nomen und jedes y-te Verb auszulassen, wobei x und y Zufallszahlen sind. Die Betweenness Centrality kann bei solch kurzen Texten nicht berücksichtigt werden. In diesem Text hätte Ball als einziges Wort einen solchen Wert, alle anderen Wörter nicht. Es kann also nur ein einziges Wort unter Einbeziehung der Betweenness Centrality berücksichtigt werden.

Ick bin ein Berliner

Nachdem ich ausgiebig nach den passenden Bibliotheken geforscht habe und in IKVM eine so elegante Möglichkeit gefunden habe, zwischen Java und .NET hin- und her zu schalten, schreibe ich nun in Java eine Bibliothek, die auf den gefundenen Bibliotheken aufsetzt. Diese Bibliothek ist plattformunabhängig, da ich sie ja dank IKVM auch unter .NET laufen lassen kann. Sie läuft nicht unter mobilen Platformen. Aber wer will schon ein Programm laufen lassen, dass sich 500 MB Speicher genehmigt und dessen Ausführung auf dem Desktop-Rechner schon 20 Sekunden braucht. Diese Software ist also für den Server oder für den Heimrechner gedacht und daher platformunabhängig. Meine Text-Analysesoftware soll einen Text einlesen und die Betweenness-Zentralitäten der Lemmas berechnen und geordnet ausgeben.

Dieses Mal entwickele ich wirklich testgetrieben. Zuerst den Test schreiben, hinterher die Implementierung, die den Test zufrieden stellt. Ich frage mich, ob ich denn jetzt die objektorientierte Entwicklung an den Nagel hängen muss. Denn wenn ich alles teste, muss ja auch so ziemlich jede Methode öffentlich zu gänglich sein. Nun. TDD (Test Driven Development) soll ja auch das Design verbessern. Das Designprinzip SRP (Single Responsibility Principle) besagt, dass eine Klasse genau eine Verantwortlichkeit und damit genau einen Grund haben sollte, um geändert zu werden. Dann mache ich eben meine Klassen noch kleiner. Die öffentlichen Methoden der Klasse kann ich dann testen und die Klasse als privates Element einer größeren Klasse halten, die dann die getestete Klasse benutzt und kapselt. Zuerst komme ich mir komisch vor, die einfachsten Dinge Schritt für Schritt zu entwickeln. Aber wie denn sonst??? Ich merke wirklich, wie TDD ein gutes Design erzwingt und auch unnötige Komplexität aus der Entwicklung heraus nimmt. Wenn man nur kleine Progrämmchen entwickelt, der kann auch mehrere Sachen gleichzeitig im Kopf behalten. Aber Entwicklung wird schließlich nicht einfacher sondern komplexer und irgendwann ist man ohne Testgerüst nur noch am Fehler suchen.

Hier ist nun die Ausgabe meines Programms, nachdem es die berühmte Rede des Präsidenten Kennedy in Berlin eingelesen hat:

stadt 3544.766394063454
berlin 3081.8249711073245
mauer 2383.5455635867397
jahr 2238.033266080326
freiheit 2171.630400645105
welt 2008.0978779390532
west-berlin 1483.8159451659449
europa 1481.9158730158722
frei 1232.8766987914046
frieden 1210.2662804515742
leute 1004.6358585858586
anderer 988.876190476191
tag 960.3910550234081
berliner 851.7508024655083
hoffnung 802.5428571428571
kommunismus 783.5699134199136
mensch 693.1821668527555
krise 626.8333333333334
familie 583.6430752254281
gesellschaft 571.7579365079367
leben 559.0666666666665
teil 544.5222943722944
geschichte 541.5802308802307
name 532.1333333333332
deutschland 493.7532467532466
mann 474.77068307509484
fortschritt 459.78881673881676
amerikanisch 454.3579365079365
bürgermeister 435.9112554112553
bürger 426.8104336839631
morgen 407.83809523809515
schwer 402.8333333333333
hervorragend 401.9614718614719
demokratie 398.68896103896094
kilometer 395.8666666666667
entschlossenheit 389.5857142857143
deutsche 363.2126984126984

Man merkt, dass der Präsident auf seine Zuhörer eingeht, denn die Stadt Berlin steht in seiner Rede im Mittelpunkt.

Die Ausführung dauert unter Java 20 s. Unter .NET dauert es saftige 25% länger. Ich überlege mir noch einmal, ob ich die Bibliothek wirklich unter .NET verwenden will. Andererseits: Wer parst schon ständig Texte?

Hier die Bibliothek und ein Beispielprojekt. Die Modelle für den LinguisticParser befinden sich im Zip-Archiv im Ordner models. Das Eclipse-Projekt ist portabel und die referenzierten .jar-Dateien relativ eingebunden und im API-Projekt dabei.

mate-tools

Das nächste Vorstellungsgespräch findet wieder in Java statt. Meine Textanalyse war bisher Forschung. Ich habe Bibliotheken gesucht und Möglichkeiten gefunden, diese Java-Bibliotheken zu .NET-Assemblies zu kompilieren. Da ich ja mit IKVM so elegant zwischen den beiden Welten Java und .NET umschalten kann, kann ich ja jetzt mit Java weiter machen und den Code dann, wenn ich Lust habe, in C# umschreiben. Soll keiner denken, ich sei nur auf .NET fixiert und lasse Java links liegen. Wichtig ist ja auch, dass ich voran komme und ob mit C# oder Java ist erst einmal egal.

Das Letzte, was mir zu meiner Textanalyse noch fehlt, ist ein Lemmatisierer. Um einen Graphen aus Text-Knoten aufzubauen, möchte ich z.B. Haus, Häuser, Häusern auf das Lemma Haus abbilden. Das letzte Mal habe ich dafür den IMS Tree Tagger verwendet. Jetzt lese ich aber, dass dieser gar nicht kommerziell nutzbar ist. Da ich ja einen Kassenschlager entwickeln will, kommt er für mich nicht in Frage. Ich finde die mate-tools (Tools for Natural Language Analysis, Generation and Machine Learning). Sie sind so gut wie gar nicht dokumentiert, aber im Quellcode selbst finde ich dann doch ein Beispiel.

Insgesamt sind die linguistischen Tools gerade für die deutsche Sprache nicht perfekt. Jedes macht so seine Fehler. Da können die Modelle noch so gut antrainiert worden sein. Aber gerade bei der deutschen Grammatik, die so extrem viele Ausnahmen und Merkmale aufweist, kann von einer Verarbeitung durch den Computer keine Perfektion erwartet werden. Mit der Verarbeitung meines Beispieltextes in Worte, POS-Tags, Lemmas und Zugehörigkeiten zu Nominalphrasen, bin ich also so halbwegs zufrieden. Die Nominalphrasen werde ich aber mit einer zusätzlichen Logik noch bestimmter herausarbeiten.

Das Ergebnis der Analyse meines Beispieltextes kann ich für meine Tests benutzen, mittels derer ich nun die Berechnung der Betweenness Centrality der einzelnen Lemmas vorantreibe.

Dies ist der Beispieltext:

Die großen Häuser in reichen Gegenden stehen oft unter Beobachtung von Einbrecher. Ein größeres Haus in einer armen Gegend hingegen wird von Einbrechern meist ignoriert. Einbrecher Hugo hat einen großen Hund namens Bello. Hugo wohnt in der Gegend von Emden.

Ich schreibe mir das Erbebnis der linguistischen Verarbeitung als XML heraus, damit ich die vielen Dinge nicht händisch für meine Tests definieren muss:


<code><?xml version="1.0" encoding="UTF-8" standalone="no"?>
<Root>
  <Word>
    <Text>Die</Text>
    <POS>ART</POS>
    <Chunk>B-NP</Chunk>
    <Lemma>der</Lemma>
  </Word>
  <Word>
    <Text>großen</Text>
    <POS>ADJA</POS>
    <Chunk>I-NP</Chunk>
    <Lemma>groß</Lemma>
  </Word>
  <Word>
    <Text>Häuser</Text>
    <POS>NN</POS>
    <Chunk>I-NP</Chunk>
    <Lemma>haus</Lemma>
  </Word>
  <Word>
    <Text>in</Text>
    <POS>APPR</POS>
    <Chunk>O</Chunk>
    <Lemma>in</Lemma>
  </Word>
  <Word>
    <Text>reichen</Text>
    <POS>ADJA</POS>
    <Chunk>B-NP</Chunk>
    <Lemma>reich</Lemma>
  </Word>
  <Word>
    <Text>Gegenden</Text>
    <POS>NN</POS>
    <Chunk>I-NP</Chunk>
    <Lemma>gegend</Lemma>
  </Word>
  <Word>
    <Text>stehen</Text>
    <POS>VVFIN</POS>
    <Chunk>O</Chunk>
    <Lemma>stehen</Lemma>
  </Word>
  <Word>
    <Text>oft</Text>
    <POS>ADV</POS>
    <Chunk>O</Chunk>
    <Lemma>oft</Lemma>
  </Word>
  <Word>
    <Text>unter</Text>
    <POS>APPR</POS>
    <Chunk>O</Chunk>
    <Lemma>unter</Lemma>
  </Word>
  <Word>
    <Text>Beobachtung</Text>
    <POS>NN</POS>
    <Chunk>B-NP</Chunk>
    <Lemma>beobachtung</Lemma>
  </Word>
  <Word>
    <Text>von</Text>
    <POS>APPR</POS>
    <Chunk>O</Chunk>
    <Lemma>von</Lemma>
  </Word></code>

Mit diesen Daten kann ich jetzt meine Berechnung füttern und in Tests Erwartungen an diese stellen. Ich erwarte, dass Haus, groß und Einbrecher zentrale Begriffe und miteinander verbunden sind.