Der Weg zum Meister – meine 10.000 Stunden als Entwickler

Ten thousand hours of practice is required to achieve the level of mastery associated with being a world-class expert — in anything.

Daniel Levitin

Die 10.000 Stunden Regel sollte jedem Entwickler (und auch jedem anderen) ein Begriff sein. Die Regel basiert ganz lose auf den Studien von Anders Ericcson und wurde hauptsächlich durch das Buch „Outliers“ von Malcolm Gladwell bekannt.

Die Regel ist nicht wissenschaftlich belegt sondern ist eher als Richtlinie zu verstehen und besagt, dass jeder der auf seinem Fachgebiet führend sein oder werden möchte, mindestens 10.000 Stunden „Deep bzw. Delibate Practice“ benötigt.

Was bedeutet Deep Practice?

Nehmen wir zum Beispielproduct_owner_rahmen Karl.

Karl ist Entwickler und das seit 10 Jahren. Karl ist aber niemand, der sich intensiv mit seiner Arbeit und seiner Tätigkeit identifiziert, sondern ein 09:00 – 17:00 Entwickler.

Karl ist auch niemand, der sich in seiner Freizeit mit neuen Themen, Programmiersprachen und Problemstellungen auseinandersetzt.

Karl macht seinen Job – gut und zufriedenstellend – aber nicht herausragend.

Man könnte jetzt annehmen, das Karl die 10.000 Stunden schon lange geknackt hat und als Meister seines Faches zählt.

Mal im Kopf überschlagen:

220 (grob Arbeitstage pro Jahr abzgl. Urlaub) * 8 (Stunden pro Tag) * 10 Jahre = 17.600 Stunden

So gerechnet müsste Karl also ein Superstar und ein Held in seinem Metier sein. Das was Karl aber tagtäglich macht ist keine Deliberate Practice sondern sein Tagewerk, und Tagewerk zählt nicht in die 10.000 Stunden Regel.

Was wäre wirkliche Praxis in Karls Job als Entwickler:

  • Auseinandersetzung mit komplexen Problemen, die neue Denkmuster erfordern
  • Neue Programmiersprachen, Konzepte und Ansätze erlernen und meistern
  • komplexe Algorithmen verstehen
  • Pair Programming und Auseinandersetzung mit Kollegen
  • Konzepte lehren und schulen
  • Fokussierung und Konzentration

Was zählt nur in Teilen:

  • Wiederholte und oft gelöste Aufgabenstellungen (sich wiederholende Aufgaben können für Routine sorgen, aber nur wenn Sie bewusst gelöst werden und nicht „automatisch“)

Was zählt gar nicht:

  • Fachfremde Aufgaben (Rechnungsstellung etc.)

Stellen wir die Rechnung also nochmals auf:

220 (grob Arbeitstage pro Jahr abzgl. Urlaub) * 4 (Stunden pro Tag) * 10 Jahre = 8.800 Stunden

Nehmen wir vereinfacht an, Karl verbringt pro Tag 4 Stunden mit echter Praxis für seine Arbeit. An manchen Tagen vielleicht weniger, an manchen Tagen mehr. Im Mittel könnte das stimmen. Das bedeutet, Karl hat bis dato 8.800 Stunden Praxis gesammelt. Im fehlen also noch genau 2.200 Stunden zum Meister.

Da potentiell die Möglichkeit zur Konzentration und Fokussierung abnimmt, je länger man im gleichen Job arbeitet,  wird auch die Praxis, die Karl pro Jahr sammeln kann abnehmen. Das bedeutet, es ist sehr fraglich, ob er jemals den Status eines Meisters erreichen kann (und vielleicht auch will).

Ich stelle die Rechnung mal grob für mich auf. Ich habe im Jahr 2002 angefangen, mich mit Programmiersprachen zu beschäftigen (ungefähr..).

2002 (grob jeden zweiten Tag eine Stunde) – (250 / 2 * 1)
2003 (grob jeden dritten Tag eine Stunde, da ich hier viel gearbeitet habe) – (250 / 3 * 1)
2004 (im Studium jeden Tag ca. eineinhalb Stunden inkl Wochenenden) – (250 * 1.5)
2005 – (im Studium jeden Tag ca. zweieinhalb Stunden inkl Wochenenden)(250 * 2.5)
2006 – (Praxissemester – hier habe ich ein halbes Jahr lang extrem viel gelernt und gearbeitet)
(125 * 6) + (125 * 2.5)
2007 – (Annahme, 3 Stunden Praxis pro Tag + 150 Extrastunden für die Abschlussarbeit) (250 * 3) + 150
2008 – (Studienabschluss und erstes grosses Projekt mit steiler Lernkurve) (125 * 5)  + (125 * 6)
2009 – (extrem viel gearbeitet und gelernt – Annahme 270 Arbeitstage) (270 * 6)
2010 – (einige neue Projekte, viele unterschiedliche Programmiersprachen und Werkzeuge) 270 * 6 + 100
2011 – 280 * 6
2012 – (großes neues Projekt, neues Team, Teamlead und Scrummaster) – 280 * 6
2013 (Selbstständigkeit)- 80 * 10

Die Rechnung ergibt also folgendes:

(250 / 2 * 1) + (250 / 3 * 1) + (250 * 1.5) + (250 * 2.5) + (125 * 6) + (125 * 2.5) + (250 * 3) + 150 + (125 * 5)  + (125 * 6) + (270 * 6) + 270 * 6 + 100 + 280 * 6 + 280 * 6 + 80 * 10 = ~12.000 Stunden Praxis in 10 Jahren.

(Natürlich ist die Rechnung komplett an den Haaren herbeigezogen, aber trotzdem interessant, wenn man mal so grob zusammenrechnet)

Das entspricht in etwa auch den Erfahrungen anderer Menschen, die sich mit dieser Regel befasst haben. Typischerweise braucht man 10 Jahre und ~10.000 Stunden Praxis, um gut in etwas zu werden ( ich behaupte jetzt einfach mal, dass ich in dem was ich mache ganz OK bin).

Es ist extrem interessant, wenn man sich weiter mit dem Thema Deliberate Practice beschäftigt. Hier sind einige Buchempfehlungen zum Thema:

 

Malcolm Gladwell – Outliers

Robert Greene – Mastery

Doug Lemov – Practice Perfect

Weitere Links:

10.000 Stunden Regel

Was ich so mache um auf meine 10.000 Stunden zu kommen

 

 

 

Ich bin der Autor des Monats im Java Magazin 04/2013 – vielen Dank!

Das Java Magazin hat mich für die Ausgabe 04/2013 zum Autor des Monats gekürt.Vielen Dank für diese große Ehre.

Martin Dilger - Autor des Monats

Übrigens – wahrscheinlich ist dem Einen oder Anderen aufgefallen, dass in letzter Zeit sehr wenig hier auf dem Blog passiert. Das liegt daran, dass ich mit Hochdruck an dem Buch arbeite, dass in „naher“ Zukunft erscheinen wird.

Meine Pläne für 2013

2013 steht direkt vor der Haustür.

2012 war bereits ein sehr ereignisreiches Jahr.

Ich habe Ende 2011 (Quasi aber fast schon 2012) geheiratet.Ich habe meinen Job als Software Consultant gekündigt und habe mich selbstständig gemacht. Ich biete IT Trainings und Consulting / Professionelle Software Entwicklung.

Bisher bin ich mit meiner Entscheidung mehr als zufrieden.

Ich habe 2012 extrem viel gelesen und gelernt.

Ich habe 2012 einige (aber nicht annähernd genug) Vorträge gehalten.

Ich habe 2012 für die Erstellung meiner HomePage einen Wicket-Port für PHP geschrieben (es fehlt noch so viel…)

Was aber ist für 2013 geplant?

So viele Vorträge wie möglich – Angefangen im Februar bei der JUG München.

Mindestens ein interessantes neues Projekt. Ich bin schon relativ lange in meinem jetzigen Projekt. Es wird Zeit für eine neue Herausforderung.

Wicket Port – Picket – erweitern um ein vollständiges Framework daraus zu machen.

Zertifizierung zum Spring Certified Professional, Oracle Certified JPA Developer.

Scala und Gradle produktiv einsetzen.

Das sind die Must-Haves. Natürlich sind auch unglaublich viele Themen wie jedes Jahr auf der Agenda.

Mindestens einen Code-Retreat besuchen.

Mindestens 3 Konferenzen besuchen.

Mindestens eine neue Sprache lernen (Vorschläge?)

Falls irgendwie möglich einmal Kanban in einem eingespielten Team erleben.

  • Agiles arbeiten
  • Clean Code
  • Software Craftmanship

Ich wünsche euch allen ein Frohes und erfolgreiches Jahr 2013. Ich hoffe, Ihr bleibt dem Blog auch unter der neuen Adresse weiterhin treu.

Wicket 6 und JSR-303 – Beanvalidation

Hallo zusammen,

Wicket 6.4.0 ist released und es versteckt sich eine wirkliche Perle hier. Mit WICKET-4883 hat Igor Vaynberg eine Implementierung für BeanValidation (JSR-303) eingefügt. Das bedeutet, Wicket unterstützt BeanValidation nun „Out-of-the-Box“.

Das lasse ich mir natürlich nicht nehmen.

Zunächst erzeugen wir uns wie immer ein Maven-Wicket-Archetype für die Version 6.3.0 (Die Snapshot-Repositories für den 6.4.0-SNAPSHOT Archetype sind leider nicht aktuell).


mvn archetype:generate -DarchetypeGroupId=org.apache.wicket -DarchetypeArtifactId=wicket-archetype-quickstart -DarchetypeVersion=6.4.0 -DgroupId=de.effectivetrainings -DartifactId=jsr303-beanvalidation -DarchetypeRepository=https://repository.apache.org/ -DinteractiveMode=false

Zunächst registrieren wir die BeanValidationConfguration in der init-Methode der Application.


/**
* @see org.apache.wicket.Application#init()
*/
@Override
public void init()
{
super.init();
new BeanValidationConfiguration().configure(this);
// add your configuration here
}

In Wicket-Experimental ist ein neues Modul entstanden. Dieses Modul deklarieren wir ebenfalls als Dependency in der pom.


<groupId>org.apache.wicket<groupId>
<artifactId>wicket-bean-validation</artifactId>
<version>0.5</version>

Zusätzlich brauchen wir eine Implementierung der Bean-Validation-API. Typischerweise Hibernate Validator.

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>4.3.0.Final</version>
</dependency>

Jetzt fehlt noch ein schön annotiertes Model-Objekt. Wir nehmen hierfür einen….


public class EffectiveTrainer implements Serializable {

@NotNull
private String name;

@Pattern(regexp = "^[_A-Za-z0-9-]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*((\\.[A-Za-z]{2,}){1}$)")
@NotNull
private String email;

@Pattern(regexp = "[0-9]+")
private String phone;

@Past
@NotNull
private Date birthDay;

public EffectiveTrainer(String name, String email, String phone, Date birthDay) {
this.name = name;
this.email = email;
this.phone = phone;
this.birthDay = birthDay;
}

public String getName() {
return name;
}

public String getEmail() {
return email;
}

public String getPhone() {
return phone;
}

public Date getBirthDay() {
return birthDay;
}
}

Im HomePage-Markup ersetzen wir alles durch folgendes Formular.

<body>
<div wicket:id="feedback"/>
<form wicket:id="form">
<input type="text" wicket:id="name"/> <br/>
<input type="text" wicket:id="email"/> <br/>
<input type="text" wicket:id="birthDay"/> <br/>
<input type="text" wicket:id="phone"/> <br/>
<input type="text" wicket:id="zip"/> <br/>
<input type="text" wicket:id="globalZip"/><br/>
<input type="submit"/>
</form>
</body>

und den passenden Java-Code hierzu:


public HomePage(final PageParameters parameters) {
super(parameters);

IModel effectiveTrainerModel = Model.of(new EffectiveTrainer());
add(new FeedbackPanel("feedback"));
Form form = new Form("form", effectiveTrainerModel);

form.add(new TextField("name", new PropertyModel(effectiveTrainerModel, "name"))
.add(new PropertyValidator()));
form.add(new TextField("email", new PropertyModel(effectiveTrainerModel, "email"))
.add(new PropertyValidator()));
form.add(new TextField("phone", new PropertyModel(effectiveTrainerModel, "phone"))
.add(new PropertyValidator()));
form.add(new TextField("date", new PropertyModel(effectiveTrainerModel, "birthDay"))
.add(new PropertyValidator()));

add(form);

}

Starten wir die Anwendung und schicken das Formular testweise ab ergibt sich folgendes Bild.

Bean Validation

Bean Validation in Action

Jede FormComponent bekommt einen neuen PropertyValidator spendiert. Der PropertyValidator liest die entsprechenden Properties und deren Constraints aus.

Sehr schön, das Prinzip funktioniert. Aber die Fehlermeldungen sind nicht ideal. Wie lässt sich hier etwas machen?

Feedback-Messages

Das erste was einem wahrscheinlich einfällt ist die Fehlermeldungen direkt an den Constraints zu definieren.


@NotNull(message = "Bitte geben Sie Ihren Namen ein.")
private String name;

@Pattern(message = "Die E-Mailadresse ist nicht gültig",
regexp = "^[_A-Za-z0-9-]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*((\\.[A-Za-z]{2,}){1}$)")
@NotNull(message = "Bitte geben Sie Ihre E-Mailadresse ein")
private String email;

@Pattern(message = "Ihre Telefonnummer sollte aus Ziffern bestehen",regexp = "[0-9]+")
private String phone;

@Past(message = "Sie können nicht in der Zukunft Geburtstag haben")
@NotNull(message = "Bitte geben Sie Ihr Geburtsdatum ein.")
private Date birthDay;

Und das funktioniert auch.

bean-validation - angepasste meldungen

bean-validation – angepasste meldungen

Ideal ist das aber nicht, denn meiner Ansicht nach ist die Wicket-Komponente (also die UI) verantwortlich sein zu definieren, wann in welchem Kontext welche Fehlermeldung angezeigt wird. Das Domain-Objekt „EffectiveTrainer“ ist so nicht Kontextübergreifend wiederverwendbar. Das muss besser gehen, oder?

Dynamische FeedbackMessages

Natürlich geht das besser. Wir definieren die Attribute am EffectiveTrainer um.


@NotNull
private String name;

@Pattern(message = "{email.invalid}",
regexp = "^[_A-Za-z0-9-]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*((\\.[A-Za-z]{2,}){1}$)")
@NotNull
private String email;

@Pattern(message = "{phone.invalid}",regexp = "[0-9]+")
private String phone;

@Past(message = "{birthday.invalid}")
@NotNull
private Date birthDay;

Die JSR-303 Spezifikation definiert, dass Fehlermeldungen mit Platzhaltern befüllt sein dürfen. Platzhalter sind definiert als { value }. Wir verwenden die Platzhalter einfach als Keys in die Property-Datei der jeweiligen Komponente.

Hierfür definieren wir jetzt nur noch diese Property-Datei (HomePage.properties).


email.Required=Bitte geben Sie Ihre E-Mailadresse ein.
email.invalid = Ihre E-Mailadresse ist ungueltig
name.Required=Bitte geben Sie Ihren Namen ein
phone.invalid=Ihre Telefonnummer sollte aus Ziffern bestehen
birthday.invalid=Ihr Geburtsdatum ist ungültig
birthday.Required=Bitte geben Sie Ihr Geburtsdatum ein

Wieso aber email.Required? Wir haben überhaupt keine message für @NotNull definiert? Igor Vaynberg hat uns hier einen Gefallen getan:). Ein Formkomponente, deren zugeordnetes Attribut mit @NotNull annotiert ist wird standardmässig auf Required gesetzt. Das bedeutet, aber hier greifen die Wicket-Standards – also #attributName.Required.

Ergänzen wir das Beispiel noch um eine Postleitzahl. Ich biete Wicket-Trainings deutschlandweit an, aber ein Effective-Trainer kann nur aus München kommen…

Definieren wir also zusätzlich folgende Property.

//hibernate validator specific
@Range(message = "{zip.muenchen}",min = 80805, max=80805)
private Integer zip;

Folgendes Textfeld.


form.add(new TextField("zip", new PropertyModel(effectiveTrainerModel, "zip"))
.add(new PropertyValidator()));

und folgende Text-Property.


zip.muenchen=Ein Effective-Trainer kann nur aus Muenchen-Schwabing kommen

Probleme

Warum hat es wohl so lange gedauert, bis die erste Implementierung direkt im Wicket-Code zur Verfügung steht?

Wicket arbeitet extrem viel mit PropertyModels. PropertyModels lösen Expressions (diese Punkt-separierten Strings) zur Laufzeit auf.

Das bedeutet, zur Laufzeit müssen wir wissen, welche Typen wir haben und wie Constraints auf diesen konfiguriert sind. Das ist etwas komplizierter. Ich habe auch vor ca. 2 Jahren schon eine JSR-303-BeanValidation Implementierung für Wicket gemacht. Lange nicht so schick wie diese hier, aber hat funktioniert.

Versuchen wir doch mal eine Vereinfachung. Lösen wir die ganzen PropertyModels im Formular auf und ersetzen diese durch ein CompoundPropertyModel. Standard so weit.


IModel effectiveTrainerModel = Model.of(new EffectiveTrainer());
add(new FeedbackPanel("feedback"));
Form form = new Form("form", new CompoundPropertyModel(effectiveTrainerModel));

form.add(new TextField("name")
.add(new PropertyValidator()));
form.add(new TextField("email")
.add(new PropertyValidator()));
form.add(new TextField("phone")
.add(new PropertyValidator()));
form.add(new TextField("date")
.add(new PropertyValidator()));
form.add(new TextField("zip")
.add(new PropertyValidator()));

Sieht tatsächlich besser aus. Funktionierts? Leider nein.

java.lang.IllegalStateException: Could not resolve Property from component: [TextField [Component id = name]]. Either specify the Property in the constructor or use a model that works in combination with a IPropertyResolver to resolve the Property automatically

Das Model der Komponente muss vom Typ IPropertyReflectionAwareModel sein, damit BeanValidation funktioniert. Im Fall CompoundPropertyModel haben wir gar keine speziellen Models.
Das Problem ist, wir kommen so leider nicht ganz einfach an das zu validierende Feld und noch schwieriger, die zugehörige Klasse ran.
Normlerweise durchläuft Wicket standardmässig für jede Expression die Getter-Chain:

"customer.address.street" getCustomer().getAddress().getStreet()

Die Typen der einzelnen Hierarchien sind hierbei egal. Nicht jedoch, wenn es um BeanValidation geht, denn ein JSR303-Validator will den Typ wissen.

Wenn wir mit dem CompoundPropertyModel arbeiten möchten, brauchen wir als einen kleinen Workaround.
Wir teilen Wicket bzw. dem Validator einfach selbst mit, um welchen Typ es sich handelt.

 form.add(new TextField("name")
                .add(new PropertyValidator(new Property(EffectiveTrainer.class,"name"))));
        form.add(new TextField("email")
                .add(new PropertyValidator(new Property(EffectiveTrainer.class,"email"))));
        form.add(new TextField("phone")
                .add(new PropertyValidator(new Property(EffectiveTrainer.class,"phone"))));
        form.add(new TextField("birthDay")
                .add(new PropertyValidator(new Property(EffectiveTrainer.class,"birthDay"))));
        form.add(new TextField("zip")
                .add(new PropertyValidator(new Property(EffectiveTrainer.class,"zip"))));

Validation Groups

Über Gruppen kann noch ein wenig genauer definiert werden, wann eine Property wie validiert wird. Ich bin kein großer Fan hiervon, denn wenn man so etwas wie Gruppen braucht, stellt sich die Frage, ob das Domain-Objekt richtig geschnitten ist. Nichtsdestotrotz, wofür könnte man das brauchen?

Eine Group ist technisch nichts anderes als ein Interface. Definieren wir also ein Interface für unseren EffectiveTrainer.


public interface EffectiveTrainer {
String getName();

String getEmail();

String getPhone();
}

Und benennen die Implementierung so um, dass es Sinn macht in MuenchensEffectiveTrainer.

Die Constraints definieren wir jetzt so um:


@Range.List(value = {
@Range(message = "{zip.muenchen}",min = 80805, max=80805, groups = EffectiveTrainer.class),
@Range(message = "{zip.global}",min = 0, max=Integer.MAX_VALUE)
})
private Integer zip;

Wir haben einen Default-Constraint, der quasi immer dann greift, wenn keine Group definiert ist (alle positiven Zahlen ausser 0 sind erlaubt!).

Im Fall von Münchens Trainer brauchts schon eine Postleitzahl in Schwabing.

Der Einfachheit halber definieren wir einfach nochmal ein zweites Textfeld, das auf ebenfalls auf die Zip-Property im Trainer-Objekt geht.

form.add(new TextField("zip")
.add(new PropertyValidator(new Property(MuenchensEffectiveTrainer.class, "zip"), EffectiveTrainer.class)));

IModel globalZipModel = compoundModel.bind("zip");
form.add(new TextField("globalZip", globalZipModel)
.add(new PropertyValidator(new Property(MuenchensEffectiveTrainer.class, "zip"))));

Im ersten Fall definieren wir im PropertyValidator, dass dieser nur die Group „EffectiveTrainer.class“ validieren soll. Im zweiten Fall geben wir gar nichts an, das bedeutet, die Default Group greift.

Gruppen können unterschiedlich validiert werden

Gruppen können unterschiedlich validiert werden

Wie gesagt, Gruppen sollten die Ausnahme sein. Man kann eine Property auf unterschiedliche Weisen validieren, aber der Code ist schwerer zu verstehen.

Über dieses und viele weitere Themen spreche ich auch in meinem Wicket-Workshop
Kontaktieren Sie mich zum Thema Wicket, ich freue mich, wenn ich Sie unterstützen kann.

Effective Trainings Wicket Workshop

Links

JSR-303 Spezifikation

Safe-Model (von Carl Eric Menzel)
(geht auch in die richtige Richtung)

Mein Wicket Workshop

Wicket und dynamisch generiertes HTML5-Cache-Manifest

Hallo,

ein interessantes Experiment, inwiefern lässt sich Wicket und HTML5-Offline-Cache verheiraten?

Scheinbar zunächst schwierig, aber es funktioniert.

Was wir zunächst brauchen ist ein dynamisch generiertes Cache-Manifest.
Nehmen wir an, wir haben folgenden Use-Case:

Unsere Seiten sollen so stark wie möglich Offline gecached werden. Welche Ressourcen genau soll aber dynamisch generiert werden.
Beispielsweise eine bestimmte Seite soll nur bis zu einem bestimmten Zeitpunkt gecached werden, abhängig von den Daten in unserer Anwendung.

Folgendes Markup definiert ein Html5-Cache Manifest:

<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org" wicket:id="html">
</html>

Wichtig hier ist der HTML5-Doctype und das Attribut „manifest“ im Html-Tag.
Hier haben wir schon die erste Schwierigkeit, wie kommen wir in Wicket an das Html-Tag?

Das schöne ist, für Wicket ist das Html-Tag auch nur ein gewöhnliches Tag und dadurch können wir diesem genau wie jedem anderen Tag eine „wicket:id“ vergeben.

So sieht das Markup für eine sehr einfache WebPage aus:

<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org" wicket:id="html">
<body>
<img wicket:id="image">
<div wicket:id="htmlCacheDemo">
</body>
</html>

Was aber weisen wir dem html-Tag zu? Ein einfacher WebMarkupContainer?
Würde funktionieren, hat aber einen gravierenden Nachteil, vor allem wenn man mit WebPage-Hierarchien arbeitet.
Wicket erwartet, dass die Komponentenhierarchie stimmt. Das würde für diesen Fall bedeuten, dass alle Komponenten auf der Page nicht der Page selbst, sondern dem äußersten WebMarkupContainer (eben dem für das Html-Tag) zugewiesen werden müssen.

Ist das Ganze in einer abstrakten Oberklasse definiert und versteckt muss das ein Entwickler wissen..

Das geht besser.

Wir verwenden hierfür einen TransparentWebMarkupContainer. Ein „transparenter“ Container ist deswegen transparent, weil er in der Hierarchie nicht sichtbar ist. Kann der Container eine Komponente nicht auflösen schaut er einfach in seinem Parent, was in diesem Fall die Page ist. Somit können alle Entwickler genauso arbeiten wie erwartet, indem sie die Komponenten der Page zuweisen und nicht mehr dem Container.

Ich glaube eine gute Idee ist, das Auflösen des Cache-Manifests in guter alter Wicket-Manier zu machen. Wicket erwartet beispielsweise, dass das Markup zu einer Seite genau den Namen der Seitenklasse hat, nur mit der Endung „html“.

Verwenden wir diesen Mechanismus doch auf für manifest-Dateien. Auf diese Weise kann jede Page in Ihrem Package ihre eigene Manifest-Datei definieren.

Das zu implementieren war nicht ganz einfach.

Zunächst definieren wir uns ein einfaches Manifest mit Namen „Homepage.manifest“ im Package der HomePage-Klasse im vorgegebenen Format:

CACHE MANIFEST
#cacheEntries
NETWORK:
#networkEntries
FALLBACK:
#fallbackEntries

Cache-Entries sind einfach eine Liste an Resourcen die im Offline-Cache gecached werden sollen, also beispielsweise /my-image.png.

Network-Entries sind das genaue Gegenteil – alle Ressourcen die zwingend jedesmal neu geladen werden sollen können hier aufgeführt werden. Üblicherweise steht hier nur ein „*“. Das bedeutet, alle Ressourcen die nicht explizit gecached sind sollen extern geholt werden.

Fallback-Entries sind praktisch, hier erlaubt es der Cache-Mechanismus Fallbacks für Ressourcen anzugeben. Das Format hierfür ist beispielsweise „/images/online.png /images/offline.png“.
Der Browser wird versuchen die Ressource „online.png“ zu laden, schlägt dies fehl wird „offline.png“ angezeigt.

Jetzt müssen wir dafür sorgen, dass die Manifest-Dateien auch für jede Seite geladen werden können.

Hierfür brauchen wir eine PackageResource. Eine PackageResource erlaubt es uns, Dateien aus dem Classpath zu laden.

public class Html5CacheResource extends PackageResource {

    private static final Logger LOG = LoggerFactory.getLogger(Html5CacheResource.class);

    public Html5CacheResource(Class<? extends Page> scope, Locale locale, String style, String variation) {
        super(scope, scope.getSimpleName() + ".manifest", locale, style, variation);
        setTextEncoding("utf-8");
        setCachingEnabled(false);
    }

    @Override
    protected byte[] processResponse(Attributes attributes, byte[] original) {
        try {
            String manifest = IOUtils.toString(new ByteArrayInputStream(original));
            return IOUtils.toByteArray(new StringReader(generateManifest(manifest)));
        } catch (Exception e) {
            LOG.warn("Cannot create cache manifest");
        }
        return original;
    }

    @Override
    protected void setResponseHeaders(ResourceResponse data, Attributes attributes) {
        super.setResponseHeaders(data, attributes);
        data.getHeaders().addHeader("Pragma", "no-cache");
        data.getHeaders().addHeader("Cache-Control",
                "no-cache, max-age=0, must-revalidate, no-store");
    }

    /**
     * override this to process your manifest
     *
     * @param template
     * @return
     */
    protected String generateManifest(String template) {
        return template;
    }
}

Die PackageResource erhält die Page, auf der sie verwendet werden soll als Parameter im Konstruktor.

Class<? extends Page> scope

Sie baut sich daraus außerdem den Ressourcen-Namen dynamisch zusammen.

scope.getSimpleName() + ".manifest"

In der Methode „setResponseHeaders“ wird ausserdem sichergestellt, dass das Manifest keinesfalls
gecached wird. Hier bin ich mir nicht ganz sicher, warum das gebraucht wird aber ohne scheint es Probleme zu geben, vor allem mit Firefox in der Version 17. Ich habe ausserdem versucht, PackageResource#setCacheable(false) zu verwenden – leider ohne Effekt.

Interessant ist die Methode „processResources“ die in der Klasse PackageResource definiert ist und mir als Entwickler die Möglichkeit gibt, die geladene Ressource weiter zu verarbeiten.

 @Override
    protected byte[] processResponse(Attributes attributes, byte[] original) {
        try {
            String manifest = IOUtils.toString(new ByteArrayInputStream(original));
            return IOUtils.toByteArray(new StringReader(generateManifest(manifest)));
        } catch (Exception e) {
            LOG.warn(&quot;Cannot create cache manifest&quot;);
        }
        return original;
    }

Wir generieren aus dem übergebenen byte[] einen String (das byte[] ist die geladenene unmodifizierte HomePage.manifest Datei).
Das Ganze geht durch eine von mir definierte Methode „generateManifest“ und direkt wieder zurück in ein byte[] das wir für die weitere Verarbeitung zurückgeben.

Die Methode „generateManifest“ ist zum Überschreiben gedacht, das werden wir gleich noch sehen.

Cache Manifest

Wir definieren uns ausserdem folgende Klassen, die ein einfaches hinzufügen von weiteren Cache-Einträgen erlauben ohne mit Strings arbeiten zu müssen.


public class CacheManifest {

private Map<CacheManifestKeys, List<String>> manifestEntries = new HashMap<CacheManifestKeys, List<String>>();

private static final String CACHE_PLACEHOLDER = "#cacheEntries";
private static final String NETWORK_PLACEHOLDER = "#networkEntries";
private static final String FALLBACK_PLACEHOLDER = "#fallbackEntries";

public CacheManifest(){
manifestEntries.put(CacheManifestKeys.CACHE, new ArrayList<String>());
manifestEntries.put(CacheManifestKeys.FALLBACK, new ArrayList<String>());
manifestEntries.put(CacheManifestKeys.NETWORK, new ArrayList<String>());
}

public void addEntry(CacheManifestEntry entry){
if(entry.isEnabled()){
manifestEntries.get(entry.getKey()).add(entry.getValue());
}
}

public String get(String manifestTemplate){
manifestTemplate = manifestTemplate.replace(CACHE_PLACEHOLDER,joinString(CacheManifestKeys.CACHE));
manifestTemplate = manifestTemplate.replace(NETWORK_PLACEHOLDER,joinString(CacheManifestKeys.NETWORK));
manifestTemplate = manifestTemplate.replace(FALLBACK_PLACEHOLDER,joinString(CacheManifestKeys.FALLBACK));
return manifestTemplate;
}

private String joinString(CacheManifestKeys cacheKey) {
if(manifestEntries.get(cacheKey).isEmpty()){
//return just a comment or * in case of network
return CacheManifestKeys.NETWORK.equals(cacheKey) ? "*": "#";
}
return StringUtils.join(manifestEntries.get(cacheKey).iterator(), '\n');
}

}

Folgende Klasse für einen Eintrag im Manifest.


public class CacheManifestEntry {

private CacheManifestKeys key;
private String value;
private String fallback;

public CacheManifestEntry(CacheManifestKeys key, String value, String fallback) {
this.key = key;
this.value = value;
this.fallback = fallback != null ? fallback : "";
}

public CacheManifestEntry(CacheManifestKeys key, String value){
this(key, value, null);
}

public boolean isEnabled() {
return true;
}

public CacheManifestKeys getKey() {
return key;
}

public String getValue() {
if(CacheManifestKeys.FALLBACK.equals(key)){
return value + " "+ fallback;
} else {
return value;
}
}

}

Interessant ist auch die Methode „isEnabled“. Einträge können ein/ausgeblendet werden.

und dieses Enum für die Keys.


public enum CacheManifestKeys {

CACHE("CACHE"),NETWORK("NETWOR"),FALLBACK("FALLBACK");
private String key;

private CacheManifestKeys(String key){
this.key = key;
}

public String getKey(){
return key;
}
}

Im Manifest-Template, das wir uns zuvor definiert haben ist beispielsweise diese Kommentar definiert: #cacheEntries

Diese Kommentare werden einfach als Platzhalter verwendet, an deren Stelle die dynamisch generierten Einträge generiert werden.

Der Html5 Cache WebmarkupContainer

Im folgenden sieht man den Code für den bereits zuvor angesprochenen transparenten WebMarkupContainer.

public class Html5CacheManifestMarkupContainer<T> extends TransparentWebMarkupContainer implements IResourceListener {

private ResourceReference resource;

private IModel<? extends CacheManifest> cacheManifestModel;

public Html5CacheManifestMarkupContainer(String id, IModel<CacheManifest> cacheManifestModel) {
super(id);
this.cacheManifestModel = cacheManifestModel;
}

public Html5CacheManifestMarkupContainer(String id, IModel<T> model, IModel<? extends CacheManifest> cacheManifestModel){
super(id);
setDefaultModel(model);
}

@Override
protected void onInitialize() {
super.onInitialize();
final Html5CacheResource cacheResource = new Html5CacheResource(getPage().getClass(), getLocale(),getStyle(),getVariation()){
@Override
protected String generateManifest(String template) {
return cacheManifestModel.getObject().get(template);
}
};
resource = new ResourceReference(getPage().getClass().getSimpleName() + ".manifest"){

@Override
public IResource getResource() {
return cacheResource;
}
};

}

@Override
protected void onConfigure() {
super.onConfigure();
add(AttributeAppender.append("manifest",urlFor(resource, getPage().getPageParameters()).toString()));
}

@Override
public void onResourceRequested() {
resource.getResource().respond(new IResource.Attributes(getRequest(),getResponse(),getPage().getPageParameters()));
}
}

Zunächst implementiert der WebmarkupContainer das Interface IResourceListener. Hiermit erlauben wir, Ressourcen auf dieser Seite über eine URL aufzurufen. Das sehen wir gleich.

In der onInitialize-Methode erzeugen wir eine neue Html5CacheResource.


final Html5CacheResource cacheResource = new Html5CacheResource(getPage().getClass(), getLocale(),getStyle(),getVariation()){
@Override
protected String generateManifest(String template) {
    return cacheManifestModel.getObject().get(template);
}
};
resource = new ResourceReference(getPage().getClass().getSimpleName() + ".manifest"){

@Override
public IResource getResource() {
    return cacheResource;
}
};

Sehr interessant ist die Methode „onConfigure“, denn hier erzeugen wir den eigentlichen Eintrag für das Html-Tag mit der korrekten URL.


@Override
protected void onConfigure() {
   super.onConfigure();
   add(AttributeAppender.append("manifest",urlFor(resource,    getPage().getPageParameters()).toString()));
}

Was Wicket jetzt daraus generiert ist das hier:

</pre>
<html xmlns:wicket="http://wicket.apache.org" wicket:id="html" manifest="./wicket/resource/org.apache.wicket.Application/HomePage.manifest">

Ein bisschen Sicherheit muss sein

Leider (oder zum Glück) verbietet Wicket standardmässig den Zugriff auf *.manifest Dateien. Hierfür müssen wir den PackageResourceGuard ein wenig erweitern. Wir definieren einfach folgende Klasse.


public class Html5CacheAwarePackageResourceGuard extends SecurePackageResourceGuard {

public Html5CacheAwarePackageResourceGuard() {
addPattern("+*.manifest");
}
}

und hängen diesen anschließend in der Application-Klasse ein.


@Override
public void init()
{
super.init();
mountPage(HomePage.MOUNT_PATH, HomePage.class);
mountPage(Another.MOUNT_PATH, Another.class);
getResourceSettings().setPackageResourceGuard(new Html5CacheAwarePackageResourceGuard());
}

Zuletzt definieren wir noch in der web.xml den richtigen Content-Type.


<mime-mapping>
<extension>manifest<extension>
<mime-type>text/cache-manifest</mime-type>
</mime-mapping>

Der Cache in Aktion

Betrachten wir das Ganze in Aktion.

Der zuvor definierte Html5CacheManifestMarkupContainer erwartet im Konstruktor ein Model vom Type CacheManifest.

Wir definieren uns hierfür einfach folgendes Model.


public class CacheManifestModel extends AbstractReadOnlyModel<CacheManifest> {

@Override
public CacheManifest getObject() {
CacheManifest manifest = new CacheManifest();
manifest.addEntry(new CacheManifestEntry(CacheManifestKeys.CACHE,"/start.html"));
manifest.addEntry(new CacheManifestEntry(CacheManifestKeys.CACHE, "http://svn.apache.org/repos/asf/wicket/sandbox/dashorst/animation/logo-top.png"));
manifest.addEntry(new CacheManifestEntry(CacheManifestKeys.FALLBACK,"/online.png","/offline.png"));
return manifest;
}
}

Fährt man den Jetty hoch und geht einmal auf die Seite sieht man folgendes (hier ist noch eine kleine Html5-Cache Test Konsole auf Basis von JQuery integriert, aber für diesen Blogpost nicht wirklich interessant).

html5 online application example with wicket

html 5 online application

Fährt man den Jetty jetzt herunter, ohne den Browser zu schliessen geschieht folgendes.

Wicket Html5 Offline Application

Wicket Html5 Offline Application

Der Fallback Mechanismus greift, da das Image nicht mehr geladen werden kann. Der Mechanismus funktioniert also.

Ich finde die Lösung für einen ersten Proof-of-Concept recht interessant und scheint auch zu funktionieren (wenn auch nicht wirklich im Firefox).

Kommentare und Ergänzungen sind wie immer herzlich Willkommen.

Alle Sourcen finden Sich wie immer in meinem <a href=“https://github.com/dilgerma/wicket-6.0-Playground.git“>Wicket-6-Playground-Repo</a&gt;.

Über dieses und viele weitere Themen spreche ich übrigens auch in meinem Wicket-Workshop

Effective Trainings Wicket Workshop

Effective Trainings Wicket Workshop

Wenn ich dieses Jahr nur ein Buch gelesen hätte…

dann wäre das „Essential Skills for Agile Development“.

Selten habe ich ein so wertvolles und durchdachtes Buch in der Hand gehabt.
Das Buch besteht im Prinzip nur aus Source-Code, Übungen und Lösungsvorschlägen für alltägliche Probleme in unserer Arbeit, und ich hab Sie alle schon in echtem Code gesehen…

Ich habe es jedem in meinem Team nahegelegt, dieses Buch zu lesen und die Übungen zu machen. Ich hoffe, das macht sich demnächst auch im Code bemerkbar:).

Das Buch ist kostenlos und hier zu finden.

Obwohl erst Anfang Dezember ist, das ist mein „Buch des Jahres 2012“.