Skip to content

Atlassian ist schlechter als sein Ruf!

Neulich haben wir das Budget für einen selbst gehosteten BitBucket Server bekommen.
Nun war mir schon bewusst, dass BitBucket nicht gleich BitBucket Server ist. Genau genommen, sind BitBucket (Cloud) und BitBucket Server zwei verschiedene Produkte, die außer dem Namen und dem Einsatzzweck nicht viel gemeinsam haben.

Aber wie beschissen die Situation ist, war mir nicht klar. Zu behaupten, dass Atlassian die Server-Variante von BitBucket stiefmütterlich behandelt, wäre noch untertrieben.

Das fängt damit an, dass Änderungen an Pull-Requests keine Webhooks triggern können. Das ist dummerweise nicht sofort ersichtlich, das es durchaus eine Checkbox dafür gibt.
Erst bei genauerem Hinsehen fällt auf, dass das nur für Änderungen an "Titel, Beschreibung oder Zielbranch" eines Pull-Request gilt. Da fragt man sich doch, was sich häufiger bei einem Pull-Request ändert - ein weiterer Commit zum Nachbessern, oder Titel und Beschreibung.

Das ist zwar schon beschissen genug, aber man denkt sich halt: OK, dann muss ich das Ding halt minütlich mit Requests gegen die REST-API bombardieren ...bis man sich mit den Einzelheiten der REST-API beschäftigt.
Denn auch diese ist fundamental anders, als die API der Cloud-Version und lässt ungefähr die Hälfte der Features vermissen!
Angaben über den Status eines Pull-Request? Pustekuchen! Von den Feldern die tatsächlich vom Server zurück kommen, ist auch nur die Hälfte dokumentiert.
Und auch an anderer Stelle ist die Doku für den Arsch! Das fällt spätestens dann auf, wenn man versucht die Kommentare eines Commit oder Pull-Request zu holen. Das geht nämlich nicht!
Kommentare können nur pro File (eines Commit/PR) abgerufen werden. Nur steht das nicht in der Doku. Auch nicht, dass der dazu gehörige Parameter "path" verpflichtent ist. Als Beschreibung steht da nur "the path to stream comments for a given path" - was zum Geier soll mir das sagen?
Auch das Anlegen eines Kommentar ist nicht weniger bekackt. Dafür braucht es nämlich zusätztlich zur Commit/PR-Id und dem Pfad noch einen "fromHash" und "toHash" -.-

Und nur um zu demonstrieren, dass sich dieses Verhalten nicht auf BitBucket (Server) beschränkt: In JIRA kann man für ein Projekt den default assignee nur auf den Projekt-Lead oder unassigned setzen. Es gibt seit Jahren ein Ticket, um auch andere User zu erlauben - bislang ohne Erfolg.

Und um noch mal auf BitBucket zurück zu kommen: wenn man eine Lizenz für BitBucket Server kauft, bekommt man ein Jahr Support - was Updates mit einschließt. Aber nach dem Jahr gibt es nicht nur keine Feature-Updates mehr. Es gibt gar keine Updates mehr - also auch keine Sicherheitsupdates!
Dafür dass das Zeug verdammt viel Geld kostet, ist das nicht nur ziemlich armselig sondern einfach nur dreist!

return null;

In seinem Buch "Clean Code" empfiehlt Robert C. Martin, nicht explizit "null" zurück zu geben. Weil es Arbeit erzeugt und den Code mit null-checks unleserlich macht.
Ich bin aber der Meinung, dass man das nicht verallgemeinern kann und sollte. An folgenden Beispiel will ich das einmal deutlich machen.

These:
Wenn wir einen DAO haben, mit einer Methode "findItemById(id)" dann soll "null" zurück geben werden, wenn kein Item mit dieser Id existiert!

Begründung:
Zum einen bekommen wir mit dem Wert "null" genau das zurück, was wir erwarten würden. Denn wir müssen davon ausgehen, dass in dem Repository oder der DB die wir da abfragen kein Eintrag mit dieser Id existiert.
Es wäre idiotisch zu erwartet, dass die DB einen Eintrag für jede Id hat, die wir uns ausdenken können - es zwingt uns schließlich niemand, der Methode nur Ids zu übergeben, für die ein Eintrag existiert!
Die Methode in dem DAO gibt uns also genau das zurück, was in der DB steht: kein Eintrag! Und "null" ist IMO die beste Repräsentation von "kein Eintrag".

Ich habe schon Implementationen gesehen, wo im Fall von "kein Eintrag" eine UnknownEntryException geschmissen wird. Aber das ist ja nicht richtig.
Eine Exception - also eine Ausnahme - impliziert ja, dass ein Fehler aufgetreten ist, der so nicht zu erwarten war. Aber das stimmt ja nicht. Wie bereits erläutert mussten wir ja zwingend davon ausgehen, dass so ein Fall eintreten könnte. Und es ist auch kein Fehler! Einen Eintrag mit einer beliebigen Id in einer DB nicht zu finden ist kein Fehler sondern normal.

Uncle Bob empfiehlt in seinem Buch "special case"-Objects zu verwenden. Aber in dem Fall würde das ja nur noch mehr Verwirrung stiften. Denn es wäre noch schwerer zu erkennen ob er den Eintrag in der DB gefunden hat, und dieser einfach nur leer ist, oder ob der Eintrag nicht existiert. (Special-Case Objekte sind nur dann sinnvoll, wenn ich nicht explizit diese Unterscheidung machen muss, weil sie ggfs. nicht relevant ist)

Prüfen muss ich es in dieser Konstellation nämlich in jedem Fall!
Das heißt, egal was ich mache um das Zurückgeben von "null" zu vermeiden - die Arbeit, den Fall "kein Eintrag gefunden" zu checken, kann ich mir dadurch nicht ersparen!
Wenn ich es doch machen würde, hieße das: ich frage explizit etwas an, schere mich aber einen Dreck um das Ergebnis - das wäre hochgradig Verantwortungslos. (Zumindest in den allermeisten Fällen)

Da wir nun also festgestellt haben, dass wir eine Überprüfung machen müssen, schauen wir doch mal, wie diese am Einfachsten realisiert werden kann (KISS-Prinzip).

Fall 1 - "return null":
ResultObject result = dao.findItemById(42);
if(result == null) {
    return;
}
Ich denke dazu brauch ich nicht viel schreiben: kurz, auf den Punkt, sofort verständlich.


Fall 2 - "throw Exception":
ResultObject result;
try {
    result = dao.findItemById(42);
} catch(UnknownEntryException e) {
    return;
}
Doppelt so viele Zeilen wie in Fall 1.
Es macht den Code außerdem unleserlicher und kann zu Verwirrung führen, da nicht auf den ersten Blick erkenntlich ist, ob hier tatsächlich ein Fehlerfall behandelt wird.
Dass es keine Option ist, das try-catch-statement einfach weg zu lassen, muss ich hoffentlich nicht extra erwähnen (das hab ich oben schon genug getan).


Fall 3 - "Special case object 1"
ResultObject result = dao.findItemById(42);
if(result.getId() == 0) {
    return;
}
Wie ein Special-Case-Object gestalltet wird, ist natürlich nicht fest vorgegeben. Hier also der Fall, dass es einfach ein normales ResultObject mit leeren Feldern ist.
Und wir sehen direkt die Probleme die damit einher gehen! Zum einen ist nicht klar, ob "0" nicht eventuell eine gültige Id sein könnte.
In diesem Fall setzen wir auch voraus, dass die Id, mit der wir anfragen auch Teil des Ergebnis ist. Wenn sie das nicht wäre, könnten wir nicht mehr unterscheiden, ob der Eintrag gefunden wurde und einfach nur leer ist oder ob er nicht gefunden wurde.
Das nächste Problem damit, erläutere ich gleich in Fall 4.


Fall 4 - "Special case object 2"
ResultObject result = dao.findItemById(42);
if(result.isNoEntry()) {
    return;
}
In diesem Fall haben wir dass Problem, dass wir unser Ergebnis-Objekt um eine Methode oder ein Feld erweitern müssen, um diesen Fall explizit abzufragen.
(bzw. wird es sich wohl eher um eine DataStructure handeln, aber in Java gibt es da keine so explizite Unterscheidung, also nennen wir es der Einfachheit halber ein Objekt)
Das führt direkt zu einem weiteren Problem: wie benennt man so etwas? "isEmpty()" wäre z.B. schlecht, weil jemand der den Code ließt, davon ausgehen könnte, es handelt sich um ein Objekt dass von Collection erbt.
Aber auch bei "isNoEntry()" stellt sich die Frage, ob es sich dabei um einen Wert handelt, der so im Eintrag in der DB steht.
Und was das schon in Fall 3 angeteaserte Problem angeht: bei diesem Code wird die verwendete IDE vermutlich anmeckern, dass "result" null sein könnte. Aber selbst wenn nicht: normalerweise arbeitet man ja nicht alleine. Und sobald ein Kollege den Code anfasst, wird ihm (oder ihr) wahrscheinlich auffallen, dass dort ein null-check fehlt! Und dann sieht der Code ganz schnell so aus:

ResultObject result = dao.findItemById(42);
if(result == null || result.isNoEntry()) {
    return;
}
Was fällt uns auf? Genau: der Code sieht so aus wie in Fall 1 - nur mit einem zusätzlichen Aufruf von "isNoEntry"!
Wäre es nicht einfacher, diesen zusätzlichen Aufruf einfach weg zu lassen? Dann könnten wir auch darauf verzichten, das ResultObject zu erweitern.


Schlussfolgerung:
Wenn wir in einem Repository ein bestimmtes Item abfragen, dann ist es sinnvoll "null" zurück zu geben, wenn dieses Item nicht existiert!


Zusätzliche Anmerkung:
Und dass mir jetzt keiner mit dem Vorschlag kommt, doch stattdessen eine Liste mit nur einem Eintrag zurück zu geben!
Nicht nur, dass wir dem Aufrufer (also dem Nutzer des DAO) damit zusätzliche und unnötige Arbeit aufhalsen, weil dieser auch noch den unwahrscheinlichen Fall abfragen muss, falls mehr als ein Item in der Liste ist.
Es ist unnötige Ressourcenverschwendung, dass Item noch mal in eine Liste zu packen, die sonst keinen Zweck erfüllt.
Außerdem wird das API des DAO dadurch kontra-intuitiv und umständlicher zu benutzen (um es mal vorsichtig zu formulieren).

Nachtrag:
Eine zusätzliche Methode im DAO "existsItemWithId(id)" die ein boolean zurück gibt, halte ich im Hinblick auf Performance ebenfalls für untauglich. Die Datenbank ist ohnehin schon das Bottleneck der gesammten Applikation - da muss man nicht noch eine zweite Abfrage absetzen, wenn eine auch reichen würde.

Maven in Eclipse

Wie beschissen das m2e-Plugin in Eclipse ist, sollte ja hinlänglich bekannt sein.

Die letzte halbwegs funktionierende Version von m2eclipse war 0.12.1 - bevor es von Sonatype zu Eclipse gewandert ist und dort bis zur Unbenutzbarkeit verhunzt wurde. Die repositories/update-sites für die alten Versionen gibt es natürlich schon lange nicht mehr. Das Plugin selber funktioniert an und für sich noch mit aktuellen Eclipse-Versionen - sofern das Eclipse-eigene m2e-plugin nicht installiert ist.

Ich war so frei, die nötigen Daten zusammen zu stellen:
m2eclipse-0.12.1.tar.gz

Das Plugin selbst, benötigt noch einige andere Standard-Plugins (wie zum Beispiel die Web- und XML-Tools). Zum "installieren" müssen dann einfach nur noch die Verzeichnisse "features" und "plugins" in das Eclipse Install-dir kopiert werden.

Das ist zwar alles nicht das Gelbe vom Ei, sollte aber um einiges besser funktionieren, als das aktuelle m2e-Plugin, dem immer irgendwelche Module fehlen!

Netbeans oder Eclipse?

Nachdem die letzte Version "Juno" von Eclipse bei mir unterirdisch langsam war - beinahe unbenutzbar - entschied ich mich, nach Jahren, mal wieder Netbeans auszuprobieren.
Um zwei Dinge gleich vorweg zu nehmen: Netbeans ist nicht schlecht und das Problem mit Eclipse kann gelöst werden.
Aber der Reihe nach.

Das erste was bei Netbeans auffällt, ist das SVN-Plugin. Man kann es benutzen, aber es ist nicht sonderlich überragend. Insebsondere fehlt mir der Repository-Explorer, wie ich ihn von Eclipse kenne. Vor allem wenn man mehrere Projekte auschecken will, artet das bei Netbeans schnell in eine ziemliche klick-und-tipp-Orgie aus. Man muss sich für jedes Projekt wieder neu zum Repository verbinden und - je nach Struktur - den Projektnamen noch mal eintippen, weil Netbeans sonst immer nur in das Verzeichnis "trunk" auschecken will. Aber es funktioniert und so oft checke ich persönlich auch keine Projekte aus.

Kommen wir zur Integration von Maven. Das Plugin dafür ist schon dabei und nach dem auschecken fällt auf, dass die Projekte automatisch als Maven-Projekte angezeigt werden. Auch funktioniert das Maven-Plugin mehr wie ein Wrapper (also ähnlich wie die m2eclipse Versionen bis 0.12.1) und funktioniert damit out of the box (ohne das ich erst Plugins für das Plugin downloaden muss, oder Probleme mit lifecycle-mappings bekomme, wie bei den aktuellen m2e Versionen).
Leider fehlen mir ein paar der Features die m2e vorzuweisen hat. Zum Beispiel die Dependency Hierarchy über die man direkt Abhängigkeiten ausschließen kann.

Das Programmieren selber geht in Netbeans gut von der Hand. Das Syntax-Highlighting lässt sich inzwischen überaus filigran einstellen und auch Content-Assist und Compare-View sind überaus gelungen und machen Spaß zu benutzen!

Einen Dämpfer erlebt man jedoch, wenn es ans Testen geht. Netbeans wickelt die Tests auch über Maven ab, sodass man nur die Ausgabe in der eingebauten Konsole hat. Schon alleine in dieser Hinsicht ist die Integration von JUnit in Eclipse um einiges besser. Noch umständlicher wird es allerdings wenn es ans Thema Testcoverage geht. Bei Eclipse muss ich dafür nur das Plugin ECLemma installieren. Bei Netbeans muss ich jedoch die pom.xml meines Projektes erweitern, und dort ein Mavenplugin wie bspw. JaCoCo einbinden. Auch wenn ich den nicht invasiven Weg von Eclipse deutlich bevorzuge, wäre das noch nicht ganz so schlimm - wenn es denn wenigstens funktionieren würde. Tut es aber - zumindest bei den Projekten die ich habe - nicht! Es könnte daran liegen, dass besagtes Plugin Probleme mit multi-module-poms hat - aber dem bin ich dann nicht weiter nachgegangen.

Zusammenfassend kann ich sagen: Netbeans ist wirklich nicht schlecht! Es ist schnell, hat ein paar wirklich nette Features, die einem Arbeit abnehmen und ist einfach zu Bedienen. Die erwähnten Drawbacks haben mich allerdings dennoch wieder zurück zu Eclipse getrieben.

Und damit komme ich wieder zurück zum Anfang. Es stellte sich nämlich heraus, dass die Probleme mit der non-responsive GUI von Eclipse Juno, nur auftreten, wenn man sich das Package "J2EE" (Eclipse IDE for Java EE Developers) gedownloaded hat! Die Classic-version (Eclipse Classic 4.2.1) hat diese Issues nicht, und reagiert gewohnt schnell!

TriGalaxy wartet auf euch

Das Beta-testen hat ein Ende. Nach Monaten gibt es nun endlich die Version 1.0 von


Das neue, kostenlose Open-World Multiplayer-Science-Fiction-Rollenspiel für Android!


Den Rest kopiere ich einfach mal aus der offiziellen Beschreibung, die Uwe sehr schön zusammen gefasst hat:



Treibe Handel und erfülle Missionen, erforsche mehrere tausende Sternsysteme.

Triff dich mit dubiosen Händlern in dunklen Korridoren, flieg durch Wurmlöcher, baue Erz auf Asteroiden ab oder suche in Wracks nach Alien-Artefakten. Verbessere deine Talente und deinen Ruf bei einer der drei Fraktionen, um besondere Aufträge und Fähigkeiten freizuschalten.
Kauf dir ein größeres Raumschiff und einen Hyperantrieb, und die ganze Galaxis steht dir offen.



Nach einer Aufwärmphase beginnt das erste Kapitel einer Hintergrundstory, die alle Mitspieler betrifft (ja, es ist ein Multiplayer-Spiel). Der Autor der Geschichte ist ein mehrfach ausgezeichneter SciFi-Autor.


Das Spiel ist komplett kostenlos. Es gibt im Bazar die Möglichkeit, ein preiswertes Booster-Pack zu erstehen, mit dem man einige Dinge etwas beschleunigen kann. Aber grundsätzlich steht jedem Mitspieler alles offen. Das Spiel wird ständig weiterentwickelt.

Und nun tauche ein in eine unbekannte Galaxis!