Tastatur und Maus unter Java fernsteuern

Für bestimmte Automatisierungsaufgaben ist es manchmal nötig die Tastatur und die Maus eines Rechners fernzusteuern. Unter Java kann dies mit der Klasse Robot aus dem Package java.awt erledigt werden. Ein einfaches Beispiel zur Nutzung sieht dabei wie folgt aus:

// Create robot
Robot robot = new Robot();

// Move mouse and make a mouse click, then wait
robot.mouseMove(1000, 1000);
robot.mousePress(InputEvent.BUTTON1_MASK);
robot.mouseRelease(InputEvent.BUTTON1_MASK);
robot.delay(1000);

// Press key a, then wait
robot.keyPress(KeyEvent.VK_A);
robot.keyRelease(KeyEvent.VK_A);
robot.delay(1000);

In diesem Beispiel wird zunächst eine Instanz der Klasse Robot angelegt. Anschließend wird die Maus bewegt und die linke Maustaste gedrückt und wieder losgelassen. Danach wartet der Robot eine Sekunde, um anschließend die Taste A zu drücken. Neben den im Beispiel gezeigten Funktionalität kann unter anderem das Mausrad gesteuert werden. Auch Methoden um einen Pixel auszulesen oder größere Bereiche des Bildschirmes sind in der Klasse zu finden.

LanguageTool-Server unter Ubuntu aufsetzen

Für die freie Grammatik- und Rechtschreibprüfung LanguageTool existieren eine Reihe von Add-ons, unter anderem für den Browser Firefox.

Standardmäßig nutzen diese Add-ons den vom Projekt bereitgestellten Server unter languagetool.org. Nicht jeder möchte seine Daten zur Korrektur an Dritte schicken und so besteht die Möglichkeit einen eigenen Server aufzusetzen. Dieser kann lokal betrieben oder auf einem eigenen Server installiert werden. In diesem Artikel soll die Installation unter Ubuntu beschrieben werden. Im ersten Schritt muss sich auf dem Server eingeloggt und dort ein neuer Nutzer angelegt werden:

adduser languagetool
su languagetool
cd

Nachdem der Nutzer angelegt wurde und in das entsprechende Home-Verzeichnis des Nutzers gewechselt wurde, kann das LanguageTool heruntergeladen und entpackt werden:

wget https://languagetool.org/download/LanguageTool-4.5.zip
unzip LanguageTool-4.5.zip
mv LanguageTool-4.5 server
rm LanguageTool-4.5.zip

Neben dem LanguageTool, werden noch sogenannte N-Gramme heruntergeladen. Diese dienen der Verbesserung der Erkennungsleistung des LanguageTool. Sie belegen knapp 27 GiB auf der Festplatte, müssen aber nicht zwingend installiert werden:

mkdir ngrams

wget https://languagetool.org/download/ngram-data/ngrams-de-20150819.zip
wget https://languagetool.org/download/ngram-data/ngrams-en-20150817.zip
wget https://languagetool.org/download/ngram-data/ngrams-es-20150915.zip
wget https://languagetool.org/download/ngram-data/ngrams-fr-20150913.zip
wget https://languagetool.org/download/ngram-data/ngrams-nl-20181229.zip

unzip ngrams-de-20150819.zip
unzip ngrams-en-20150817.zip
unzip ngrams-es-20150915.zip
unzip ngrams-fr-20150913.zip
unzip ngrams-nl-20181229.zip

rm ngrams-de-20150819.zip
rm ngrams-en-20150817.zip
rm ngrams-es-20150915.zip
rm ngrams-fr-20150913.zip
rm ngrams-nl-20181229.zip

Damit ist das LanguageTool installiert. Ein erster Test kann erfolgen, indem der Server mittels:

java -cp languagetool-server.jar org.languagetool.server.HTTPServer --port 8081

gestartet wird. Über Curl können erste Testdaten an den Server gesendet werden:

curl --data "language=en-US&text=a first test" http://localhost:8081/v2/check

In meinem Setup wird der Server auf dem Port 8081 (oder einem beliebigen anderen Port betrieben) und ist über einen Reverse Proxy, in diesem Fall Nginx, erreichbar. Dazu muss die Konfiguration angepasst werden:

nano /etc/nginx/sites-available/example

In der Nginx-Konfigurationsdatei wird nun folgende Konfiguration hinterlegt:

server {
        listen   443 ssl;
        listen [::]:443 ssl;

        ssl_certificate /etc/letsencrypt/live/api.example.org/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/api.example.org/privkey.pem;

        root /var/www/example/api;
        index index.php index.html index.htm;

        server_name api.example.org;

        # proxy for languagetool
        location /languagetool/ {
                proxy_pass http://localhost:8081/v2/;
        }
}

Nachdem die Konfiguration für Nginx hinterlegt wurde, wird Nginx neugestartet:

nginx restart

Über den Browser kann die API nun getestet werden:

https://api.example.org/languagetool/check?language=en-US&text=Wong wong wong

Damit ist die Konfiguration des LanguageTool allerdings noch nicht abgeschlossen. Für den produktiven Betrieb wird eine Konfigurationsdatei unter /home/languagetool/server/ erstellt:

nano languagetool.cfg

Diese Datei wird mit folgendem Inhalt befüllt:

languageModel=/home/languagetool/ngrams

Damit der Service automatisch startet, wird eine systemd-Unit angelegt:

nano /etc/systemd/system/languagetool.service

Diese Datei wird mit folgendem Inhalt befüllt:

[Unit]
Description=LanguageTool
After=syslog.target
After=network.target

[Service]
Type=simple
User=languagetool
Group=languagetool
WorkingDirectory=/home/languagetool/server
ExecStart=/usr/bin/java -cp /home/languagetool/server/languagetool-server.jar org.languagetool.server.HTTPServer --config languagetool.cfg --port 3001 --allow-origin "*"
Restart=always
Environment=USER=git HOME=/home/languagetool

[Install]
WantedBy=multi-user.target

Nachdem die Datei angelegt wurde, wird sie aktiviert und anschließend der Service gestartet:

systemctl enable languagetool
service languagetool start

Zum Test der N-Gramme kann folgende URL aufgerufen:

https://api.example.org/languagetool/check?language=en-US&text=I%20want%20to%20go%20their.

werden. Wenn keine N-Gramme installiert oder konfiguriert sind, kommt ein relativ kurzes JSON als Antwort zurück:

{„software“:{„name“:“LanguageTool“,“version“:“4.5″,“buildDate“:“2019-03-26 11:37″,“apiVersion“:1,“premium“:false,“premiumHint“:“You might be missing errors only the Premium version can find. Contact us at supportlanguagetoolplus.com.“,“status“:““},“warnings“:{„incompleteResults“:false},“language“:{„name“:“English (US)“,“code“:“en-US“,“detectedLanguage“:{„name“:“English (US)“,“code“:“en-US“,“confidence“:0.9999997}},“matches“:[]}

Sind die N-Gramme erfolgreich installiert und konfiguriert, wird das LanguageTool mit einem längeren Response antworten:

{„software“:{„name“:“LanguageTool“,“version“:“4.5″,“buildDate“:“2019-03-26 11:37″,“apiVersion“:1,“premium“:false,“premiumHint“:“You might be missing errors only the Premium version can find. Contact us at supportlanguagetoolplus.com.“,“status“:““},“warnings“:{„incompleteResults“:false},“language“:{„name“:“English (US)“,“code“:“en-US“,“detectedLanguage“:{„name“:“English (US)“,“code“:“en-US“,“confidence“:0.9999997}},“matches“:[{„message“:“Statistics suggests that ‚there‘ (as in ‚Is there an answer?‘) might be the correct word here, not ‚their‘ (as in ‚It’s not their fault.‘). Please check.“,“shortMessage“:““,“replacements“:[{„value“:“there“,“shortDescription“:“as in ‚Is there an answer?'“}],“offset“:13,“length“:5,“context“:{„text“:“I want to go their.“,“offset“:13,“length“:5},“sentence“:“I want to go their.“,“type“:{„typeName“:“Other“},“rule“:{„id“:“CONFUSION_RULE“,“description“:“Statistically detect wrong use of words that are easily confused“,“issueType“:“non-conformance“,“category“:{„id“:“TYPOS“,“name“:“Possible Typo“}},“ignoreForIncompleteSentence“:false,“contextForSureMatch“:3}]}

Damit ist der Server komplett eingerichtet. Nun kann der eigene Server bei entsprechenden Add-ons eingerichtet und genutzt werden.

Nachdem der Server aufgesetzt wurde, können entsprechende Add-ons umgestellt werden

Das gleiche Setup kann natürlich genutzt werden, einen solchen Server lokal auf dem eigenen Ubuntu-Rechner zu installieren.

Freie Grammatik- und Rechtschreibprüfung

Vor einigen Jahren schrieb ich einen kurzen Artikel über die Nutzung von LanguageTool in der freien Office-Lösung LibreOffice. LanguageTool ist eine freie Software zur Rechtschreib- und Grammatikprüfung, ähnlich dem wahrscheinlich im deutschen Raum bekannteren Duden Korrektor. Allerdings ist LanguageTool weit mehr als eine Erweiterung für LibreOffice. Es handelt sich um eine in Java entwickelte Software, welche mittlerweile über 30 Sprachen unterstützt. Mittlerweile gibt es neben der freien Version, einen Dienst, welcher weitere Korrekturen gegen Entgelt bereitstellt (languagetoolplus.com).

LanguageTool in der Standalone-Version

Neben der Standalone-Desktop-Version existieren unzählige Integrationen, wie z.B. für LibreOffice, Firefox und Chrome in Form von Add-ons. Neben den offiziellen Add-ons existieren weitere Add-ons, welche durch die Community bereitgestellt werden. Die offizielle Seite des Projektes ist unter languagetool.org zu finden. Die Entwicklung findet auf GitHub statt. Dort ist ebenfalls der Quelltext von LanguageTool und etwaiger Erweiterungen zu finden. Lizenziert ist LanguageTool unter der LGPL.

Performance beim Ermitteln von Elementen aus einer Liste

Gestern schrieb ich einen Artikel über die Möglichkeiten ein Element aus einer Liste unter Java zu ermitteln. Dort wurde unter anderem eine Lösung mittels der Stream-API aufgezeigt. In einem Kommentar zu dem Artikel kam die Frage nach der Performance dieser Methode auf. Aus diesem Grund habe ich einen kleinen Benchmark geschrieben, welcher die Unterschiede in der Performance ermitteln sollte. Der Testfall bestand daraus ein Element aus einer Liste zu ermitteln. Dazu wird eine Liste mit knapp 125.000 Elementen erzeugt. Nun wurde mit unterschiedlichen Methoden versucht das entsprechende Element zu ermitteln. Das gesuchte Element befindet sich in den Testfällen immer an der Position 75.004 der Liste. Erzeugt wird die Liste mit der Methode getElements:

private List<Element> getElements() {

	List<Element> elements = new ArrayList<>();

	// Add 75000 elements
	for (int i = 0; i < 75000; i++) {
		elements.add(new Element(String.valueOf(i), String.valueOf(i)));
	}

	elements.add(new Element("Suppe", "Löffel"));
	elements.add(new Element("Wasser", "Flüssigkeit"));
	elements.add(new Element("Käse", "Gelb"));
	elements.add(new Element("Huhn", "Ei"));

	// Add 50000 elements
	for (int i = 0; i < 50000; i++) {
		elements.add(new Element(String.valueOf(i), String.valueOf(i)));
	}

	return elements;
}

Vom Gefühl her hätte ich vermutet, das die Stream-API immer langsamer ist als die klassische Iteration durch die Liste. Vier unterschiedliche Methoden wurden für das Benchmark implementiert. Beim Benchmark Iterate list wird die Liste über eine foreach-Schleife iteriert und beim entsprechenden Element abgebrochen:

for (Element element : elements) {

	if ("Huhn".equals(element.Key)) {
		specificElement = element;
		break;
	}
}

Die nächste Variante iteriert die Liste ebenfalls durch, nutzt aber die klassische Variante über den Index:

for (int j = 0; j < elements.size(); j++) {

	Element element = elements.get(j);

	if ("Huhn".equals(element.Key)) {
		specificElement = element;
		break;
	}
}

Anschließend folgt eine Variante über die Stream-API, bei welcher die Methode findFirst genutzt wird:

specificElement = elements.stream()
		.filter(element -> "Huhn".equals(element.Key))
		.findFirst()
		.orElse(null);

Bei der letzten Variante wird ebenfalls die Stream-API genutzt, nur diesmal wird findAny genutzt:

specificElement = elements.stream()
		.filter(element -> "Huhn".equals(element.Key))
		.findAny()
		.orElse(null);

Die Idee bei der Nutzung der Methode findAny ist, dass diese schneller ist, da die Suche theoretisch parallelisiert werden kann. Im JavaDoc zu der Methode wird das Ganze so beschrieben:

The behavior of this operation is explicitly nondeterministic; it is
* free to select any element in the stream. This is to allow for maximal
* performance in parallel operations; the cost is that multiple invocations
* on the same source may not return the same result.

Der Benchmark selber führt für jeden Testfall 75.000 mal durch, damit sich Ungenauigkeiten bei einzelnen Läufen wegmitteln und etwaige Optimierung zum tragen kommen können. Wird die Ausführungszeit über alle 75.000 Durchläufe betrachtet ergibt sich folgendes Bild:

Die Durchführungszeiten über alle Durchläufe

Bei der Betrachtung der einzelnen Durchläufe ergibt sich ein ähnliches Bild:

Die Durchführungszeiten pro Durchlauf

Die ermittelten Werte sehen wie folgt aus:

Iterate list
Time in seconds (total): 58.621323501
Time in seconds (per run): 0.00078161764668

Iterate list (without for each)
Time in seconds (total): 51.9264994
Time in seconds (per run): 0.0006923533253333333

Stream list and find first
Time in seconds (total): 55.3019915
Time in seconds (per run): 0.0007373598866666667

Stream list and find any
Time in seconds (total): 90.196209799
Time in seconds (per run): 0.0012026161306533333

Die schnellste Variante scheint die klassische Iteration über den Index zu sein, anschließend folgt die Variante mit der Stream-API und der Methode findFirst. Danach kommt die Iteration mittels einer foreach-Schleife und am Ende folgt weit abgeschlagen die Stream-API-Variante mit der Methode findAny. Das diese so schlecht abschneidet hat mich überrascht. Natürlich sollten Zahlen aus einem Benchmark immer mit Vorsicht genoßen werden, da es sich immer um eine künstliche Gegenüberstellung handelt. Das komplette Benchmark befindet sich auf GitHub und ist unter der MIT-Lizenz lizenziert und damit freie Software.

Element einer Liste unter Java mittels des Stream-API ermitteln

Unter Java kommt es öfter vor, das ein bestimmter Eintrag in einer Liste gesucht werden soll. Gegeben sei in diesem Beispiel folgende Liste:

// List of elements
List elements = new ArrayList();

elements.add(new Element("Suppe", "Löffel"));
elements.add(new Element("Wasser", "Flüssigkeit"));
elements.add(new Element("Käse", "Gelb"));
elements.add(new Element("Huhn", "Ei"));

Die Klasse, welche die Elemente hält ist wie folgt definiert:

class Element {
    public final String Key;
    public final String Value;

    public Element(String key, String value) {
        Key = key;
        Value = value;
    }
}

Soll nun ein bestimmtes Element der Liste ermittelt werden, so kann dies auf dem klassischen Weg wie folgt erledigt werden:

// Get element
Element specificElement = null;

for (Element element : elements) {

    if ("Huhn".equals(element.Key)) {
        specificElement = element;
        break;
    }
}

Mit der seit Java 8 eingeführtes Stream-API kann das Problem eleganter gelöst werden:

// Get element via stream api
Element specificElement = elements.stream()
        .filter(element -> "Huhn".equals(element.Key))
        .findFirst()
        .orElse(null);

In diesem Fall werden die Elemente durchgegangen und überprüft ob in element.Key die Zeichenkette Huhn enthalten ist. Ist dies der Fall, wird das entsprechende Element zurückgegeben. Wird das Element in diesem Fall nicht gefunden, so ist die Variable specificElement null.