Nach einem Jahr der Blog-Abstinenz heute zur Erfrischung mal ein wenig Scala.
Nehmen wir an, wir haben die folgende login-Funktion, die REST-assured
nutzt, um HTTP-Requests für einen Login zu tätigen. Zurückgegeben wird ein Tupel
mit einem Access-Token und wie lange selbiges gültig ist.
deflogin(credentials:Credentials):(String,Int)={defjson=given()// Skipped: add the credentials somehow to the request
.post("…").Then.statusCode(HttpStatus.SC_OK).extract().jsonPath()(json.getString("access_token"),json.getInt("expires_in"))}
Manchmal gibt die Methode ein Access-Token zurück, das nicht gültig ist, obwohl
es laut der ebenfalls zurückgegebenen Gültigkeitsdauer noch lange gültig sein
sollte. Warum?
Sehr subtil, aber das def json = … sollte natürlich ein val json = …
sein. Ansonsten findet bei der Erstellung des Rückgabe-Tupel zwei mal ein Aufruf
der Funktion json statt: einmal bei der Auswertung von
json.getString("access_token") und das andere Mal bei der Auswertung von
json.getInt("expires_in"). Beides mal wird natürlich auch ein frischer
HTTP-Request abgesetzt. Es konnte daher passieren, dass für das Access-Token ein
Token zurückgegeben wurde, das gerade am ablaufen ist, und für den
expires_in-Wert wurde bereits ein neues Token ausgestellt. Daher sieht es so
aus, als wäre das Token noch lange gültig, dabei ist es bereits abgelaufen.
Im heutigen Blog-Post geht es um Webrahmenwerke. Genauer: es geht darum, wie man
auf typsichere Art und Weise Links zu Controllern erzeugt. Typsicher in dem
Sinne, dass man Variablen im Routing nur durch Werte des selben Typs ersetzen
kann, den der Controller auch tatsächlich erwartet. Übergibt man die falsche
Anzahl an Werten oder Werte des falschen Typs, soll der Code erst gar nicht
kompilieren – in anderen Worten: Es sollen nicht erst Exceptions zur Laufzeit
geworfen werden, sondern bereits beim kompilieren. Für das Beispiel beschränken
wir uns auf Pfad-Variablen.
Die Code-Beispiele für die Controller sind dabei lose an JAX-RS orientiert.
Es werden die folgenden Konventionen benutzt:
Methoden, die Requests behandeln, haben lediglich die Pfad-Variablen als
Parameter. Die Reihenfolge der Parameter ist dabei die Reihenfolge, in der sie
im Routing-Template definiert sind.
Note
In diesem Blog-Beitrag wird nur die Methode vorgestellt, mit der man Links
erstellen kann. Ein Router, der Requests auf Controller dispatcht, wird nicht
vorgestellt.
Außerdem wird eine gewisse Vertrautheit mit Java 8 (insbesondere Lambdas)
vorausgesetzt sowie auch ein wenig mit JAX-RS. Für eine Intro zu Lambdas
siehe etwa den Lambda Quick Start
Prior Art
Da Links erzeugen ein Recht häufiges Problem ist, gibt es natürlich so einiges
an Prior Art. Wahrscheinlich in so ziemlich jedem Webrahmenwerk, in den
unterschiedlichsten Ausprägungen. Ich habe stellvertretend einmal drei
herausgegriffen:
Zeigt schon mal die grobe Idee, wenn auch in Python und nicht in Java. Gewisse
Fehler werden abgefangen (falsche Anzahl an Argumenten zum Beispiel).
Allerdings passiert alles zur Laufzeit. Man braucht also eine recht gut
ausgeprägte Test-Suite, um Fehler beim Linken zu erkennen.
Allerdings funktioniert die Methoden-Auflösung zur Laufzeit und via Name als
String. Die Parametertypen müssen explizit angegeben werden und die
Typüberprüfung passiert ebenfalls erst zur Laufzeit.
Warum nicht Methodenreferenzen?
Da Java 8 endlich Methodenreferenzen eingeführt hat, liegt natürlich die Idee
nahe, es damit irgendwie zu versuchen.
Stellen wir uns vor, dass wir den folgenden Controller hätten:
Als erstes erstellen wir ein FunctionalInterface.
Das Interface soll eine Referenz auf eine Methode der Klasse H darstellen,
die keine Parameter entgegen nimmt und ein R als Ergebnis zurück gibt.
Dementsprechend sieht das Interface wie folgt aus:
Dieses Interface kann aus einer Methodenreferenz erstellt werden:
NoParam<HelloWorldController,String>hello=HelloWorldController::hello;// Aufrufen könnte man die Methode jetzt so (angenommen,
// someHelloWorldController ist eine Instanz von HelloWorldController):
Stringresult=hello.apply(someHelloWorldController);assert"Hello, world!".equals(result);
Folglich können wir damit dann die folgende Methode bauen:
<H,R>URIlinkTo(NoParam<H,R>handler){// Hier der Code, der die Routing-Informationen von handler ausliest und
// daraus dann eine URI baut
}
Jetzt ist es möglich, aus einem anderen Controller heraus einen Link zu unserer
gewünschten Methode HelloWorldController#hello() zu bauen:
URIhelloLink=linkTo(HelloWorldController::hello);
Wenn wir ein Argument zu viel übergeben würden (zum Beispiel, weil wir
denken, dass die hello-Ressource einen Namen entgegen nimmt, um einen
personalisierten Gruß zu erzeugen, kompiliert der Code nicht:
java: no suitable method found for linkTo(HelloWorldController::hello)
Ziel erreicht. Um tatsächlich Pfad-Parameter zu unterstützen, müssen wir jetzt
einfach (relativ mechanisch) weitere Interfaces einführen.
Erweitern wir zunächst unseren Controller um einen personalisierten Gruß:
Wenig überraschend steht H hierbei für den Typ des Controllers, P für
den Parameter und R für den Rückgabewert.
Desweiteren muss eine weitere Überladung von linkTo eingeführt werden:
URIlinkTo(OneParam<H,P,R>handler,Pparam){// Hier wieder Routing-Infos von handler auslesen und dann param einsetzen
}
Das ist zum Implementieren zwar ein wenig wortreich (für jede Anzahl an
Pfad-Variablen ein eigenes Interface und eine entsprechende linkTo-Methode),
aber das muss man zum Glück nur einmal tun und außerdem hat man ja auch nicht unendlich
lange Pfade in der Praxis.
Viel gravierender ist jedoch: es funktioniert überhaupt nicht. Man kann zwar aus
einer Methodenreferenz ein Lambda bauen. Allerdings geht die Information, aus
welcher Methode das Lambda erzeugt wird, dabei verloren. Wir brauchen die
Information, um welche Methode es sich handelt, jedoch, da wir ansonsten nicht
an die Route kommen.
Proxies to the rescue
Da die Antwort auf die meisten Probleme in Java "(dynamische) Code-Generierung"
ist, probieren wir es doch auch einmal damit. Genauer gesagt dynamische
Proxy-Objekte. Die Idee ist dabei folgendermaßen:
Wir erzeugen uns ein Proxy-Objekt vom gleichen Typ der Handler-Klasse.
Wir rufen die Methode auf, die übergeben wurde (genauer gesagt, das Lambda)
Das Proxy-Objekt ruft nicht wirklich die eigentliche Methode auf, sondern
merkt sich einfach, welche Methode aufgerufen wurde.
Wir holen uns die gemerkte Methode vom Proxy-Objekt.
Gehen wir davon aus, dass wir eine Klasse MethodResolver<T>, die die
Proxy-Objekte erstellt, könnte unsere linkTo-Methode also in der Art
aussehen:
URIlinkto(Class<H>handlerClass,OneParam<H,P,R>handler,Pparam){MethodResolver<H>methodResolver=MethodResolver.on(handlerClass);handler.apply(methodResolver,param);Methodmethod=methodResolver.resolve();// Mit handlerClass und method kann man jetzt an die Routing-Informationen
// kommen
}
Die meisten AOP-Rahmenwerke bieten Method-Interceptors an, mit denen man das
recht einfach umsetzen kann. Für Proxetta könnte ein entsprechendes Advice zum
Beispiel so aussehen:
/**
* MethodResolver advice applied on all methods. It puts the method in a class
* variable that can be accessed later using reflection.
*/classMethodResolverAdviceimplementsProxyAdvice{publicMethodmethod;publicObjectexecute(){finalClass<?>targetClass=targetClass();finalStringmethodName=targetMethodName();finalClass<?>[]argumentTypes=createArgumentsClassArray();try{method=targetClass.getMethod(methodName,argumentTypes);}catch(NoSuchMethodExceptione){thrownewRuntimeException(e);}returnreturnValue(null);}}
Seit heute steht Let's Encrypt als öffentliche
Beta zur Verfügung. Mit Let's Encrypt kann sich jeder einfach und kostenlos
ein X.509-Zertifikat erstellen lassen. Somit gibt es keine Ausrede mehr, nicht
HTTPS zu benutzen. Deswegen versucht dieses Blog auch mit gutem Beispiel voran
zu gehen:
Wie man am Titel bereits leicht erraten kann: heute geht es (zum
ersten Mal in diesem Blog?) um Java. Genauer: Wie man heterogene
Container in Java umsetzen kann.
Zunächst zur Idee: Wir wollen eine Klasse Favorites
implementieren, in die man seine Lieblingsobjekte speichern und auch
wieder abrufen kann. Damit man nicht einfach irgendein beliebiges
Objekt zurückbekommt, sondern ein Objekt von einem bestimmten Typ,
übergibt man beim Speichern und abrufen die Klasse des Objekts.
Der geneigte Java-erfahrene Leser wird direkt erkennen, dass
das Beispiel aus Effective Java von Joshua Bloch ist. Das
ist absolut richtig und auch Teile des hier gezeigten Codes
stammen aus diesem Buch, das durchaus lesenswert ist (auch
für Nicht-Java-Programmierer).
Die erste Idee, das zu implementieren, könnte etwa so aussehen:
Man nimmt also einfach das Klassenobjekt des Werts als Schlüssel in
einer Map, um den Wert zu speichern. Beim Auslesen wird dann auch
wieder das Klassenobjekt übergeben, womit man an den Wert kommt. Wurde
für den Typ kein Wert hinterlegt, wird einfach nichts gefunden.
An sich scheint das auch ganz gut zu funktionieren:
Auch der Fall, dass kein Wert hinterlegt wurde für den Typ,
funktioniert einfach: favorites.get(type) gibt dann null
zurück und null kann zu allem gecastet werden.
Und hätte Java jetzt nicht Type Erasure, wäre der Blogpost auch
schon zu Ende. Da Java allerdings Type Erasure hat, stößt man recht
bald auf ein Problem, wenn man auf versucht, ein Generic in den
Container zu packen: List<Integer>.class ist ein Syntaxfehler. Das
liegt daran, dass List<Integer> und List<String> dasselbe
Klassenobjekt haben, nämlich List.class. Das bedeutet aber auch,
dass man eine Liste von Strings in Favorites packen kann und
danach als Liste von Integern auslesen kann (bzw. man kann es
zumindest versuchen):
Wenn man das jetzt ausführt, scheint es sogar zu
funktionieren. Jedenfalls läuft es anstandslos durch. Allerdings nur
bis man dann den Wert tatsächlich einmal versucht als den angegebenen
Wert zu verwenden:
Integerfirst=values.get(0);
Bei einer erneuten Ausführung wird jetzt eine Ausnahme geworfen:
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
Bei genauem Hinschauen erkennt man auch bereits beim Kompilieren, dass
hier potenziell zur Laufzeit etwas kaputt gehen könnte:
TypeReference kann jetzt zur Laufzeit nachsehen, welcher Wert als
Typvariable übergeben wurde. Das wiederum kann dann Favorite für sich
benutzen.
Die Implementierung von TypeReference:
importjava.lang.reflect.ParameterizedType;importjava.lang.reflect.Type;publicabstractclassTypeReference<T>{privatefinalTypetype;protectedTypeReference(){TypesuperClass=getClass().getGenericSuperclass();if(superClassinstanceofClass<?>){thrownewRuntimeException("Missing type parameter");}type=((ParameterizedType)superClass).getActualTypeArguments()[0];}publicTypegetType(){returntype;}}
Die Ausgabe ist wie erwartet Some string 1234 null. Fertig ist er
also, unser typsicherer heterogener Container, der auch mit Generics
funktioniert (wenn auch etwas umständlich).
Oder fast. Wenn da dieses @SuppressWarnings("unchecked") nicht
wäre. Immerhin haben wir vor einem Augenblick erst gesehen, dass man
Compiler-Warnungen nicht ignorieren sollte. Und tatsächlich kann man
auch für die neue Favorites einen Fall konstruieren, bei dem zur
Laufzeit eine Ausnahme fliegt:
static<T>List<T>favoriteList(Favoritesf){TypeReference<List<T>>typeReference=newTypeReference<List<T>>(){};List<T>result=f.getFavorite(typeReference);if(result==null){result=newArrayList<T>();f.putFavorite(typeReference,result);}returnresult;}publicstaticvoidmain(String[]args){Favoritesf=newFavorites();List<String>listOfStrings=favoriteList(f);List<Integer>listOfIntegers=favoriteList(f);listOfIntegers.add(42);Stringbooooom=listOfStrings.get(0);}// java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
Um diese Ausnahme zu vermeiden, müsste man die Typ-Argumente
durchgehen und schauen, ob noch irgendwo eine TypeVariable
vorkommt. Ein Beispiel, in dem das gemacht wird, ist GenericType<T>
aus JAX-RS (Quelltext). Das
würde aber noch immer zur Laufzeit eine Exception werfen und nicht zur
Compile-Zeit einen Fehler produzieren.
ikanobori hat sich die Mühe gemacht und die
Software, die auf bpaste.net läuft, komplett
erneuert. Dabei hat er auch lange gewünschte neue Features
umgesetzt. Außerdem ist bpaste.net damit gleichzeitig auf neue,
leistungsfähigere Hardware umgezogen.
Neu ist:
Man kann Pastes löschen (via einem Löschen-Link)
Pastes können sich automatisch nach einer bestimmten Zeit löschen
Gleich geblieben ist:
API-Kompatibilität zu LodgeIt (hoffentlich zumindest)
Die Daten werden innerhalb der nächsten zwei Monate automatisch in das
neue System übertragen. Aber nur, wenn man ein altes Paste
öffnet. Danach werden die Daten unwiderruflich gelöscht sein.
Inspiriert vom Idea-GitLab-Integration-Plugin, das ein open file in
browser anbietet, habe ich das ganze mal für Emacs gebaut:
(require'magit)(setqgit-browser-url-templates'(("bitbucket.org"."https://bitbucket.org/{{repo}}/src/{{branch}}/{{file-name}}#cl-{{lineno}}")("github.com"."https://github.com/{{repo}}/blob/{{branch}}/{{file-name}}#L{{lineno}}")("pwmt.org"."https://git.pwmt.org/?p={{repo}}.git;a=blob;f={{file-name}};hb={{branch}}#l{{lineno}}")))(defunformat-gitviewer-url(templatevars)(let((expand(lambda(match)(let*((name(substringmatch2-2))(value(assocnamevars)))(unlessvalue(error(format"Unknown variable %s"name)))(cdrvalue)))))(replace-regexp-in-string"{{.+?}}"expandtemplate)))(defungit-open-in-browser()(interactive)(let*((remote(magit-get-remotenil))(remote-url(magit-get"remote"remote"url"))(branch(substring(magit-get-tracked-branch)(+1(lengthremote))))(file-name(magit-file-relative-name(buffer-file-name)))(lineno(line-number-at-pos)))(unless(string-match"\\(@\\|://\\|^\\)\\([^:@/]+?\\)[:/]\\([^/].*?\\)\\(.git\\)?$"remote-url)(error"Could not find repo name"))(let*((host-name(match-string2remote-url))(repo-name(match-string3remote-url))(url-template(assochost-namegit-browser-url-templates)))(unlessurl-template(error(format"Could not find URL template for host %s"host-name)))(browse-url(format-gitviewer-url(cdrurl-template)(list(cons"repo"repo-name)(cons"branch"branch)(cons"file-name"file-name)(cons"lineno"(number-to-stringlineno))))))))
Dieser Artikel ist schon ein paar Jahre alt und war eigentlich nur für
den Gebrauch bei meinem damaligen Arbeitgeber gedacht. Damit sich aber
endlich mal etwas in diesem Blog tut, habe ich mich entschlossen, ihn
dennoch zu veröffentlichen.
Note
Die Code-Beispiele sind in Python 2-Syntax gehalten. Das
Übersetzen nach Python 3 wird dem Leser als Übung
überlassen.
Was sind Dekoratoren?
Wie der Namen bereits vermuten lässt, dekorieren Dekoratoren
etwas. Bei Python sind es Funktionen bzw. Methoden und seit Version
2.6/3.0 auch Klassen. Ein Beispiel für Dekoratoren:
@decoratordefspam():print"Ich bin spam."
Note
Ein Hinweis für Java-Entwickler: Die Syntax sieht zwar
ähnlich aus wie Annotations in Java, hat aber ansonsten
nichts mit Annotations in Java zu tun.
Was passiert jetzt, wenn man den obigen Code ausführt?
>>> @decorator... defspam():... print"Ich bin spam."...Traceback (most recent call last):
File "<stdin>", line 1, in <module>NameError: name 'decorator' is not defined
Der NameError ist jetzt natürlich wenig überraschend, schließlich
wurde der Dekorator decorator noch nicht definiert. Im Folgenden
werden wir das nachholen:
defdecorator(func):print"Ich bin ein Dekorator und dekoriere",func.__name__returnfunc
Was passiert jetzt, wenn man den obigen Code ausführt?
>>>defdecorator(func):...print"Ich bin ein Dekorator und dekoriere",func.__name__...returnfunc...>>>@decorator...defspam():...print"Ich bin spam."...IchbineinDekoratorunddekorierespam>>>spam<functionspamat0x7f8d95b3c488>
Und was, wenn man die dekorierte Funktion ausführt?
>>>spam()Ichbinspam.
Wie einfach zu erkennen ist, hat unser simpler Beispiel-Dekorator
keine direkte Auswirkung auf die dekorierte Funktion.
Ich weiß jetzt noch immer nicht, was ein Dekorator ist
Ein Beispiel alleine erklärt natürlich nicht, was ein Dekorator
ist. Was ist also ein Dekorator? Ein Dekorator ist nichts anderes als
ein Callable, das implizit als Argument die zu dekorierende Funktion
bzw. Methode bzw. Klasse übergeben bekommt. Der Rückgabewert des
Dekorators wird dann an den Namen der Funktion bzw. Methode
bzw. Klasse gebunden. Anders ausgedrückt:
@decoratordefspam():pass
ist nichts anderes als syntaktischer Zucker für
defspam():passspam=decorator(spam)
Das ist auch der Grund, warum unser Beispieldekorator decorator
die Funktion explizit wieder zurück gibt.
Das, was nach dem @ folgt, muss jedoch nicht zwingend ein Name
sein. Vielmehr kann es ein beliebiger Ausdruck sein. Der Ausdruck
wird evaluiert und das Ergebnis des Ausdrucks ist dann der Dekorator.
Um das zu verdeutlichen, folgt jetzt ein Beispiel für eine
Dekoratoren-Factory:
defdecorator_for(name):"""Ich bin eine Factory und ich gebe einen Dekorator zurück."""print"Erzeuge einen Dekorator für",name# Den eigentlichen Dekorator (aus dem vorigen Beispiel) zurück gebenreturndecorator@decorator_for("Csaba")defspam():print"Ich bin spam."
Und wieder die Frage, was passiert, wenn man den Code ausführt?
>>> defdecorator_for(name):... """Ich bin eine Factory und ich gebe einen Dekorator zurück."""... print"Erzeuge einen Dekorator für",name... # Den eigentlichen Dekorator (aus dem vorigen Beispiel) zurück geben... returndecorator...>>> @decorator_for("Csaba")... defspam():... print"Ich bin spam."...Erzeuge einen Dekorator für Csaba
Ich bin ein Dekorator und dekoriere spam
Und was passiert, wenn man die dekorierte Funktion ausführt?
>>> spam()Ich bin spam.
Wie zu erwarten, noch immer nichts spektakuläres.
Wann wird ein Dekorator ausgeführt?
Der aufmerksame Leser kann sich diese Frage bereits selbst
beantworten: Der Dekorator wird nicht etwa bei jedem Aufruf der
dekorierten Funktion bzw. Methode aufgerufen, sondern genau ein
einziges Mal, nämlich dann, wenn die Funktion bzw. Methode deklariert
wird.
Ich will aber, dass bei jedem Funktionsaufruf etwas passiert
Auch das kann sich der aufmerksame Leser bereits selber herleiten: Wie
bereits erwähnt, wird der Rückgabewert des Dekorators an den Namen der
Funktion gebunden, die dekoriert wird. Als Beispiel also jetzt ein
Dekorator, der nicht explizit die dekorierte Funktion wieder
zurückgibt.
defuseless_decorator(func):returnNone
>>> @useless_decorator... defspam():... print"Ich bin spam."...>>> spam()Traceback (most recent call last):
File "<stdin>", line 1, in <module>TypeError: 'NoneType' object is not callable>>> spam>>>
Da der Dekorator None zurückgibt, wird auch None an den Namen
der Funktion, also spam, gebunden und die ursprüngliche Funktion
ging verloren.
Man kann jetzt natürlich nicht nur die Original-Funktion oder None
zurück geben. Vielmehr kann man auch einen Wrapper zurück geben, der
dann die ursprüngliche Funktion aufruft. Damit hat man dann das
erreicht, was man wollte: Bei Jedem Funktionsaufruf soll etwas
passieren.
defverbose_caller(func):print"Erzeuge einen Wrapper für die Funktion",func.__name__defwrapper():print"Rufe die Funktion",func.__name__,"auf"func()returnwrapper
>>> @verbose_caller... defspam():... print"Ich bin spam"...Erzeuge einen Wrapper für die Funktion spam
>>> spam()Rufe die Funktion spam auf
Ich bin spam
>>> spam<function wrapper at 0x7f8d95b3c758>
So gesehen ist der Dekorator in diesem Fall nichts anderes als eine
Wrapper-Factory.
Da das immer noch ziemlich langweilig ist, wollen wir uns jetzt einen
personalisierten Wrapper erzeugen. Dazu bauen wir eine
Wrapper-Factory-Factory:
defverbose_caller_for(name):print"Erzeuge eine Wrapper-Factory für",namedefverbose_caller(func):print"Erzeuge einen Wrapper für die Funktion",func.__name__defwrapper():print"Rufe die Funktion",func.__name__,"für",name,"auf"func()returnwrapperreturnverbose_caller
>>> @verbose_caller_for("Csaba")... defspam():... print"Ich bin spam"...Erzeuge eine Wrapper-Factory für Csaba
Erzeuge einen Wrapper für die Funktion spam
>>> spam()Rufe die Funktion spam für Csaba auf
Ich bin spam
Da das ganze langsam doch recht unübersichtlich wird, überlegen wir
uns als gute Entwickler, wie man das ganze verschönern könnte.
Klassenbasierte Dekoratoren
Dekoratoren sind einfach Callables, die das zu dekorierende Objekt
entgegen nehmen. Instanzen von Klassen können jedoch ein Callable
implementieren, über die spezielle Methode __call__. Warum also
nicht einen Dekorator über eine Klasse implementieren? Als
passionierte Java-Entwickler wissen wir, dass Klassen alles
übersichtlicher machen.
classPersonalizedVerboseCaller(object):def__init__(self,name):self.name=namedef__call__(self,func):returnself.decorate(func)defdecorate(self,func):"""Wrapper factory."""print"Erzeuge einen Wrapper für die Funktion",func.__name__defwrapper():print"Rufe die Funktion",func.__name__,"für",self.name,"auf"func()returnwrapper
>>> @PersonalizedVerboseCaller("Csaba")... defspam():... print"Ich bin spam"...Erzeuge einen Wrapper für die Funktion spam
>>> spam()Rufe die Funktion spam für Csaba auf
Ich bin spam
Das ist Peter[*]. Peter arbeitet beim "Zentrum für Cyber
Defence". Das "Zentrum für Cyber Defence" hilft, den deutschen
Mittelstand vor Industriespionage zu schützen.
Da der Cyber aber ziemlich groß ist, kann man da leicht den Überblick
verlieren. Dashboards to the rescue!
Damit man auch bei der Kaffeepause enorm wichtige Nachrichten nicht
verpasst (für den unwahrscheinlichen Fall, dass in der Cafeteria
kein riesiger Monitor mit dem CND Radar hängt), gibt es das
ganze auch als praktische Smartphone-App.
Auch vor Deep Packet Inspection macht die Cyber-Defence-Forschung
keinen Halt:
Nach langer Zeit der Abstinenz, unter anderem ausgelöst durch den
Plattentod des Blog-Hosts, habe ich beschlossen mal wieder ein wenig
zu bloggen. Wie sich das gehört habe ich also als erstes einmal den
kompletten Blog umgebaut.
Was hat sich geändert?
Das Aussehen! Und mit dem Aussehen auch die verwendete
Blogsoftware. Nach Drupal und schrift ist jetzt Pelican an der
Reihe. Zum ersten Mal habe ich das Theme komplett alleine erstellt,
mit der Hilfe von Bootstrap.
Was ist gleich geblieben?
Für den geneigten Leser sichtbar: Es gibt noch immer keine
Kommentare. Wer das Bedürfnis hat, einen Beitrag zu kommentieren oder
mir einfach so etwas mitzuteilen, mag mir bitte eine E-Mail
schreiben. Meine E-Mail-Adresse kann man auf der About-Seite finden.
Nicht so sichtbar: Der Inhalt wird noch immer aus reStructuredText
erstellt.
Ich habe mich bemüht, dass meine alten Beiträge erhalten bleiben. Es
gibt auch einige neue öffentliche Einträge, die ich aus dem
nicht-öffentlichen Archiv ausgegraben habe. Die URLs zu den
einzelnen Einträgen haben sich geändert, da muss ich noch überlegen,
ob es mir die Mühe wert ist, dass die alten URLs noch gehen. Da der
Blog aber eine ganze Weile nicht verfügbar war und sich niemand
beschwert hat, glaube ich fast nicht, dass das passieren wird.
Wird es hier in Zukunft wieder regelmäßig Inhalte geben?
Das weiß ich ehrlich gesagt noch nicht. So manchen kleinen
Blogeintrag, den ich hier früher verfasst habe, würde ich heute
einfach auf G+ posten. Wir werden sehen. Vorgenommen habe ich es mir
jedenfalls. Ihr könnt ja den Feed
mal in euren Google-Reader-Ersatz aufnehmen.