Sonntag, 19. Juli 2009

GWT 1.6 Hosted Mode: Internen Jetty Server mit JDNI Datasourcen und Environment Informationen für PostgreSQL versorgen

DSCF0005c
Update: Auch in GWT Version 2.0.3 scheint das Verfahren weiterhin zu funktionieren!
Als eines der mühseligsten Dinge, die man beim Einstieg in die GWT Programmierung herausfinden muss, empfand ich die Lösung der folgende Frage:
Wie bringt man den internen Jetty Server, der bei der Hosted Mode Entwicklung unter Eclipse mit dem Google Plugin verwendet wird dazu, der serverseitigen Programmierung JBDC Verbindungen bzw. Connection Pools und andere JNDI Ressourcen zur Verfügung zu stellen?
Vermutlich wird fast jedes halbwegs ambitionierte GWT Projekt irgendwann vor dieser Aufgabe stehen. Falls sie nicht gelöst wird müsste man auf die enormen Vorteile der Hosted Mode Entwicklung verzichten, sein GWT Projekt für jeden Testlauf übersetzen (Java –> Javascript) und ohne die hilfreichen Entwicklungsfunktionen auskommen.

Infos im Web

In den eigentlichen GWT Seiten findet man keine besonders hilfreichen Informationen. In dem Thread Setting DataSource (JNDI) in GWT 1.6 (hosted mode – Jetty) in den Google Groups findet man allerdings Hilfestellungen, die einen letztlich zu der Lösung bringen.
Bei meinem ersten Anlauf zur Problemlösung bin ich allerdings folgendem Link aus den Kommentaren gefolgt, weil die Lösung einfacher schien:
http://humblecode.blogspot.com/2009/05/gwt-16-using-jndi-datasource.html
Auch findet man weitere hilfreiche Links, z. B. zu passenden Jetty Seiten. Leider bin ich bei dieser Lösung an der Frage gescheitert, wie man dem Jetty Server den per Default fehlenden JNDI Context java:comp/env zur Verfügung stellt. Daher habe ich dann letztlich doch die Lösung aus den Google Groups umgesetzt. Dies, und die Anpassung für unsere PostgreSQL Datenbank, soll im Folgenden beschrieben werden:

Schrittchen für Schrittchen

1. Jetty und JDBC JARs
Zunächst sollte man sich die notwendigen JARs besorgen. Dies sind zum einem Erweiterungen für Jetty, zum anderen der JDBC Treiber für PostgreSQL. Hier die Links für den Download:
Jetty: http://dist.codehaus.org/jetty/
Hier findet man Jetty bis Version 6. Ab Version 7 ist der Download beim Eclipse Projekt zu finden. Beim Download einer 6er Version in eines der Unterverzeichnisse gehen und das größere (nicht-src) ZIP Archiv holen, darin finden sich die beiden JARs.
PostgreSQL JDBC: http://jdbc.postgresql.org/download.html
Ich habe die JDBC3 Version verwendet.
Insgesamt muss man dem Jetty Server dann folgende JARs verfügbar machen:
  • jetty-naming-*.jar
  • jetty-plus-*.jar
  • postgresql-*.jdbc3.jar
Man kann die JARs der Einfachheit halber in /war/WEB-INF/lib/ platzieren, auch wenn für die eigentliche Anwendung nicht gebraucht werden.  Die JARs müssen dann in den Launch Configuration Properties in den Classpath gesetzt werden (unter User Entries).
2. jetty-env.xml
Im /war/WEB-INF/ Verzeichnis die Datei jetty-env.xml anlegen. Hier werden nun die Ressourcen für den JNDI Kontext angelegt. Leider kann man dabei nur wenig von einer eventuell vorhandenen Konfiguration für Tomcat lernen, da Jetty vieles anders macht. In unserem Fall muss neben der DataSource auch eine Konfigurationsinformation abgelegt werden. Mit beiden Definitionen sieht meine jetty-env.xml so aus:
<?xml version="1.0"?>

<!-- Wird nur fuer die Nutzung des Hosted Modes bei der GWT Entwicklung gebraucht -->

<configure class="org.mortbay.jetty.webapp.WebAppContext">

<new class="org.mortbay.jetty.plus.naming.EnvEntry">
<arg></arg>
<arg>meinEnvParameter</arg>
<arg type="java.lang.String">irgendein String</arg>
<arg type="boolean">true</arg>
</new>

<new id="jdbc/meinDBPool" class="org.mortbay.jetty.plus.naming.Resource">
<arg></arg>
<arg>jdbc/meinDBPool</arg>
<arg>
<new class="org.postgresql.ds.PGSimpleDataSource">
<set name="User">meinPostgresBenutzer</set>
<set name="Password">meinPostgresPasswort</set>
<set name="DatabaseName">meinePostgresDatenbank</set>
<set name="ServerName">localhost</set>
<set name="PortNumber">5432</set>
</new>
</arg>
</new>

</configure>


Die entsprechenden Ressourcen müssen natürlich auch wie immer in der web.xml definiert werden:


<resource-ref>
<res-ref-name>jdbc/meinDBPool</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>


Aber das sollte in den meisten Fällen schon der Fall sein.


3. env-Kontext konfigurieren


Ein Problem ist jetzt noch zu überwinden: Jetty kennt in der Standardkonfiguration noch keinen Environment Context, also das, was man typischerweise so abfragt:


Context initCtx = new InitialContext();
Context myEnv = (Context) initCtx.lookup("java:comp/env");


Möglicherweise soll Jetty auf diese Weise schlank gehalten werden für bestimmte Einsatzszenarien, aber hier stellt es noch einen weiteren Stolperstein dar, der zu überwinden ist. Es scheint möglich zu sein, Jetty über die XML Konfiguration mit dem Kontext zu versorgen, aber leider habe ich nicht herausgefunden wie. Daher wird der in den Google Groups beschriebene Weg eingeschlagen, der in der Implementierung einer eigenen Starterklasse für den internen Server besteht.


Das GWT verwendet per Default die Klasse



com.google.gwt.dev.shell.jetty.JettyLauncher



um den Server für den Hosted Mode zu starten. Diese Klasse befindet sich samt Quellcode im gwt-dev-windows.jar. Diese Klasse muss erweitert werden. Am einfachsten geht dies vermutlich in dem man sich den Quellcode in eine eigene Klasse kopiert (beim Upgrade auf das nächste GWT muss man vielleicht nachsehen, ob sich etwas verändert hat). Nennen wir die neue Klasse



org.unibi.sm.edit.MyCustomJettyLauncher



und legen sie im Sourceverzeichnis des GWT Projekts ab. Jetzt geht es nur noch darum dem WebAppClassLoader die entsprechende Konfiguration unterzuschieben, mit der er uns den gewünschten Kontext zur Verfügung stellt. Hier der entsprechende Codeschnipsel, am Ende findet sich noch einmal die komplette MyCustomJettyLaucher.java:


// Zusaetzlicher Quellcode Anfang:
private static String[] __dftConfigurationClasses = {
"org.mortbay.jetty.webapp.WebInfConfiguration", // 
"org.mortbay.jetty.plus.webapp.EnvConfiguration",// jetty-env
"org.mortbay.jetty.plus.webapp.Configuration", // web.xml
"org.mortbay.jetty.webapp.JettyWebXmlConfiguration",// jettyWeb
};
// Zusaetzlicher Quellcode Ende.

/**
* A {@link WebAppContext} tailored to GWT hosted mode. Features....
*/
protected final class WebAppContextWithReload extends WebAppContext {

/**
* Specialized {@link WebAppClassLoader} that allows outside…
*/
private class WebAppClassLoaderExtension extends WebAppClassLoader {

private static final String META_INF_SERVICES = "META-INF/services/";

public WebAppClassLoaderExtension() throws IOException {
super(bootStrapOnlyClassLoader, WebAppContextWithReload.this);

// Zusaetzlicher Quellcode Anfang:
setConfigurationClasses(__dftConfigurationClasses);
// Zusaetzlicher Quellcode Ende.
}


Es werden also im Konstruktor der inneren Klasse, die den WebAppClassLoader erweitert, eine Liste von zuvor definierten Konfigurationsklassen hinzugefügt. Als letztes muss jetzt noch die Run Configuration des Projektes dazu gebracht werden unsere neue Launcherklasse zu verwenden. Dazu wird einfach im Arguments-Reiter unter Program arguments die folgende Option ergänzt:



-server org.unibi.sm.edit.MyCustomJettyLauncher



So sieht es dann aus:


EclipseJetty


4. Start


Wenn alles richtig gemacht wurde sollte der interne Jetty jetzt ohne Fehlermeldungen starten und die serverseitige GWT Programmierung kann wie oben beschrieben auf Datenbanken und andere Environment Objekte zugreifen.


Wichtig: Um den Effekt von Änderungen zu testen sollte man eine eventuell laufende Hosted Mode Umgebung komplett stoppen und neu starten.




Der dritte Weg



Ergänzend noch der Hinweis auf einen möglichen dritten Weg zwischen Verzicht auf den Hosted Mode und der mühseligen Jetty Konfiguration: Oliver beschreibt einen Weg um den Hosted Mode ohne den eingebauten Server zu verwenden. Hier werden dann der serverseitige Code und die statische Inhalte des Projektes über einen externen AppServer ausgeführt bzw. geliefert. Das ist besonders dann interessant, wenn man sowieso einen schon fertig konfigurierten AppServer für seine Entwicklung hat. Allerdings bezieht sich Olivers Text auf eine schon etwas ältere GWT Version, bei der noch Tomcat als interner Server verwendet wurde.




Quellcode ‘org.unibi.sm.edit.MyCustomJettyLauncher’



package org.unibi.sm.edit;

import com.google.gwt.core.ext.ServletContainer;
import com.google.gwt.core.ext.ServletContainerLauncher;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.dev.shell.jetty.JettyNullLogger;
import com.google.gwt.dev.util.InstalledHelpInfo;

import org.mortbay.component.AbstractLifeCycle;
import org.mortbay.jetty.AbstractConnector;
import org.mortbay.jetty.Request;
import org.mortbay.jetty.RequestLog;
import org.mortbay.jetty.Response;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.handler.RequestLogHandler;
import org.mortbay.jetty.nio.SelectChannelConnector;
import org.mortbay.jetty.webapp.WebAppClassLoader;
import org.mortbay.jetty.webapp.WebAppContext;
import org.mortbay.log.Log;
import org.mortbay.log.Logger;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Enumeration;

/**
* A {@link ServletContainerLauncher} for an embedded Jetty server.
*/
public class MyCustomJettyLauncher extends ServletContainerLauncher {

/**
* Log jetty requests/responses to TreeLogger.
*/
public static class JettyRequestLogger extends AbstractLifeCycle implements
RequestLog {

private final TreeLogger logger;

public JettyRequestLogger(TreeLogger logger) {
this.logger = logger;
}

/**
* Log an HTTP request/response to TreeLogger.
*/
@SuppressWarnings("unchecked")
public void log(Request request, Response response) {
int status = response.getStatus();
if (status < 0) {
// Copied from NCSARequestLog
status = 404;
}
TreeLogger.Type logStatus, logHeaders;
if (status >= 500) {
logStatus = TreeLogger.ERROR;
logHeaders = TreeLogger.INFO;
} else if (status >= 400) {
logStatus = TreeLogger.WARN;
logHeaders = TreeLogger.INFO;
} else {
logStatus = TreeLogger.INFO;
logHeaders = TreeLogger.DEBUG;
}
String userString = request.getRemoteUser();
if (userString == null) {
userString = "";
} else {
userString += "@";
}
String bytesString = "";
if (response.getContentCount() > 0) {
bytesString = " " + response.getContentCount() + " bytes";
}
TreeLogger branch = logger.branch(logStatus, String.valueOf(status)
+ " - " + request.getMethod() + ' ' + request.getUri()
+ " (" + userString + request.getRemoteHost() + ')'
+ bytesString);
TreeLogger headers = branch.branch(logHeaders, "Request headers");
Enumeration headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String hdr = headerNames.nextElement();
String hdrVal = request.getHeader(hdr);
headers.log(logHeaders, hdr + ": " + hdrVal);
}
// TODO(jat): add response headers
}
}

/**
* An adapter for the Jetty logging system to GWT's TreeLogger. This
* implementation class is only public to allow {@link Log} to instantiate
* it.
* 
* The weird static data / default construction setup is a game we play with
* {@link Log}'s static initializer to prevent the initial log message from
* going to stderr.
*/
public static class JettyTreeLogger implements Logger {
private final TreeLogger logger;

public JettyTreeLogger(TreeLogger logger) {
if (logger == null) {
throw new NullPointerException();
}
this.logger = logger;
}

public void debug(String msg, Object arg0, Object arg1) {
logger.log(TreeLogger.SPAM, format(msg, arg0, arg1));
}

public void debug(String msg, Throwable th) {
logger.log(TreeLogger.SPAM, msg, th);
}

public Logger getLogger(String name) {
return this;
}

public void info(String msg, Object arg0, Object arg1) {
logger.log(TreeLogger.INFO, format(msg, arg0, arg1));
}

public boolean isDebugEnabled() {
return logger.isLoggable(TreeLogger.SPAM);
}

public void setDebugEnabled(boolean enabled) {
// ignored
}

public void warn(String msg, Object arg0, Object arg1) {
logger.log(TreeLogger.WARN, format(msg, arg0, arg1));
}

public void warn(String msg, Throwable th) {
logger.log(TreeLogger.WARN, msg, th);
}

/**
* Copied from org.mortbay.log.StdErrLog.
*/
private String format(String msg, Object arg0, Object arg1) {
int i0 = msg.indexOf("{}");
int i1 = i0 < 0 ? -1 : msg.indexOf("{}", i0 + 2);

if (arg1 != null && i1 >= 0) {
msg = msg.substring(0, i1) + arg1 + msg.substring(i1 + 2);
}
if (arg0 != null && i0 >= 0) {
msg = msg.substring(0, i0) + arg0 + msg.substring(i0 + 2);
}
return msg;
}
}

/**
* The resulting {@link ServletContainer} this is launched.
*/
protected static class JettyServletContainer extends ServletContainer {
private final int actualPort;
private final File appRootDir;
private final TreeLogger logger;
private final Server server;
private final WebAppContext wac;

public JettyServletContainer(TreeLogger logger, Server server,
WebAppContext wac, int actualPort, File appRootDir) {
this.logger = logger;
this.server = server;
this.wac = wac;
this.actualPort = actualPort;
this.appRootDir = appRootDir;
}

public int getPort() {
return actualPort;
}

public void refresh() throws UnableToCompleteException {
String msg = "Reloading web app to reflect changes in "
+ appRootDir.getAbsolutePath();
TreeLogger branch = logger.branch(TreeLogger.INFO, msg);
// Temporarily log Jetty on the branch.
Log.setLog(new JettyTreeLogger(branch));
try {
wac.stop();
wac.start();
branch.log(TreeLogger.INFO, "Reload completed successfully");
} catch (Exception e) {
branch.log(TreeLogger.ERROR,
"Unable to restart embedded Jetty server", e);
throw new UnableToCompleteException();
} finally {
// Reset the top-level logger.
Log.setLog(new JettyTreeLogger(logger));
}
}

public void stop() throws UnableToCompleteException {
TreeLogger branch = logger.branch(TreeLogger.INFO,
"Stopping Jetty server");
// Temporarily log Jetty on the branch.
Log.setLog(new JettyTreeLogger(branch));
try {
server.stop();
server.setStopAtShutdown(false);
branch.log(TreeLogger.INFO, "Stopped successfully");
} catch (Exception e) {
branch.log(TreeLogger.ERROR,
"Unable to stop embedded Jetty server", e);
throw new UnableToCompleteException();
} finally {
// Reset the top-level logger.
Log.setLog(new JettyTreeLogger(logger));
}
}
}

private static String[] __dftConfigurationClasses = {
"org.mortbay.jetty.webapp.WebInfConfiguration", // 
"org.mortbay.jetty.plus.webapp.EnvConfiguration",// jetty-env
"org.mortbay.jetty.plus.webapp.Configuration", // web.xml
"org.mortbay.jetty.webapp.JettyWebXmlConfiguration",// jettyWeb
};

/**
* A {@link WebAppContext} tailored to GWT hosted mode. Features hot-reload
* with a new {@link WebAppClassLoader} to pick up disk changes. The default
* Jetty {@code WebAppContext} will create new instances of servlets, but it
* will not create a brand new {@link ClassLoader}. By creating a new
* {@code ClassLoader} each time, we re-read updated classes from disk.
* 
* Also provides special class filtering to isolate the web app from the GWT
* hosting environment.
*/
protected final class WebAppContextWithReload extends WebAppContext {

/**
* Specialized {@link WebAppClassLoader} that allows outside resources
* to be brought in dynamically from the system path. A warning is
* issued when this occurs.
*/
private class WebAppClassLoaderExtension extends WebAppClassLoader {

private static final String META_INF_SERVICES = "META-INF/services/";

public WebAppClassLoaderExtension() throws IOException {
super(bootStrapOnlyClassLoader, WebAppContextWithReload.this);

setConfigurationClasses(__dftConfigurationClasses);
}

@Override
public URL findResource(String name) {
// Specifically for
// META-INF/services/javax.xml.parsers.SAXParserFactory
String checkName = name;
if (checkName.startsWith(META_INF_SERVICES)) {
checkName = checkName.substring(META_INF_SERVICES.length());
}

// For a system path, load from the outside world.
URL found;
if (isSystemPath(checkName)) {
found = systemClassLoader.getResource(name);
if (found != null) {
return found;
}
}

// Always check this ClassLoader first.
found = super.findResource(name);
if (found != null) {
return found;
}

// See if the outside world has it.
found = systemClassLoader.getResource(name);
if (found == null) {
return null;
}

// Warn, add containing URL to our own ClassLoader, and retry
// the call.
String warnMessage = "Server resource '"
+ name
+ "' could not be found in the web app, but was found on the system classpath";
if (!addContainingClassPathEntry(warnMessage, found, name)) {
return null;
}
return super.findResource(name);
}

/**
* Override to additionally consider the most commonly available JSP
* and XML implementation as system resources. (In fact, Jasper is
* in gwt-dev via embedded Tomcat, so we always hit this case.)
*/
@Override
public boolean isSystemPath(String name) {
name = name.replace('/', '.');
return super.isSystemPath(name)
|| name.startsWith("org.apache.jasper.")
|| name.startsWith("org.apache.xerces.");
}

@Override
protected Class<?> findClass(String name)
throws ClassNotFoundException {
// For system path, always prefer the outside world.
if (isSystemPath(name)) {
try {
return systemClassLoader.loadClass(name);
} catch (ClassNotFoundException e) {
}
}

try {
return super.findClass(name);
} catch (ClassNotFoundException e) {
// Don't allow server classes to be loaded from the outside.
if (isServerPath(name)) {
throw e;
}
}

// See if the outside world has a URL for it.
String resourceName = name.replace('.', '/') + ".class";
URL found = systemClassLoader.getResource(resourceName);
if (found == null) {
return null;
}

// Warn, add containing URL to our own ClassLoader, and retry
// the call.
String warnMessage = "Server class '"
+ name
+ "' could not be found in the web app, but was found on the system classpath";
if (!addContainingClassPathEntry(warnMessage, found,
resourceName)) {
throw new ClassNotFoundException(name);
}
return super.findClass(name);
}

private boolean addContainingClassPathEntry(String warnMessage,
URL resource, String resourceName) {
TreeLogger.Type logLevel = (System
.getProperty(PROPERTY_NOWARN_WEBAPP_CLASSPATH) == null) ? TreeLogger.WARN
: TreeLogger.DEBUG;
TreeLogger branch = logger.branch(logLevel, warnMessage);
String classPathURL;
String foundStr = resource.toExternalForm();
if (resource.getProtocol().equals("file")) {
assert foundStr.endsWith(resourceName);
classPathURL = foundStr.substring(0, foundStr.length()
- resourceName.length());
} else if (resource.getProtocol().equals("jar")) {
assert foundStr.startsWith("jar:");
assert foundStr.endsWith("!/" + resourceName);
classPathURL = foundStr.substring(4, foundStr.length()
- (2 + resourceName.length()));
} else {
branch.log(TreeLogger.ERROR,
"Found resouce but unrecognized URL format: '"
+ foundStr + '\'');
return false;
}
branch = branch.branch(logLevel, "Adding classpath entry '"
+ classPathURL
+ "' to the web app classpath for this session", null,
new InstalledHelpInfo("webAppClassPath.html"));
try {
addClassPath(classPathURL);
return true;
} catch (IOException e) {
branch.log(TreeLogger.ERROR, "Failed add container URL: '"
+ classPathURL + '\'', e);
return false;
}
}
}

/**
* Parent ClassLoader for the Jetty web app, which can only load JVM
* classes. We would just use null for the parent
* ClassLoader except this makes Jetty unhappy.
*/
private final ClassLoader bootStrapOnlyClassLoader = new ClassLoader(
null) {
};

private final TreeLogger logger;

/**
* In the usual case of launching {@link com.google.gwt.dev.HostedMode},
* this will always by the system app ClassLoader.
*/
private final ClassLoader systemClassLoader = Thread.currentThread()
.getContextClassLoader();

@SuppressWarnings("unchecked")
private WebAppContextWithReload(TreeLogger logger, String webApp,
String contextPath) {
super(webApp, contextPath);
this.logger = logger;

// Prevent file locking on Windows; pick up file changes.
getInitParams().put(
"org.mortbay.jetty.servlet.Default.useFileMappedBuffer",
"false");

// Since the parent class loader is bootstrap-only, prefer it first.
setParentLoaderPriority(true);
}

@Override
protected void doStart() throws Exception {
setClassLoader(new WebAppClassLoaderExtension());
super.doStart();
}

@Override
protected void doStop() throws Exception {
super.doStop();
setClassLoader(null);
}
}

/**
* System property to suppress warnings about loading web app classes from
* the system classpath.
*/
private static final String PROPERTY_NOWARN_WEBAPP_CLASSPATH = "gwt.nowarn.webapp.classpath";

static {
// Suppress spammy Jetty log initialization.
System.setProperty("org.mortbay.log.class", JettyNullLogger.class
.getName());
Log.getLog();

/*
* Make JDT the default Ant compiler so that JSP compilation just works
* out-of-the-box. If we don't set this, it's very, very difficult to
* make JSP compilation work.
*/
String antJavaC = System.getProperty("build.compiler",
"org.eclipse.jdt.core.JDTCompilerAdapter");
System.setProperty("build.compiler", antJavaC);
}

public ServletContainer start(TreeLogger logger, int port, File appRootDir)
throws Exception {
TreeLogger branch = logger.branch(TreeLogger.INFO,
"Starting Jetty on port " + port, null);

checkStartParams(branch, port, appRootDir);

// Setup our branch logger during startup.
Log.setLog(new JettyTreeLogger(branch));

// Turn off XML validation.
System.setProperty("org.mortbay.xml.XmlParser.Validating", "false");

AbstractConnector connector = getConnector();
connector.setPort(port);

// Don't share ports with an existing process.
connector.setReuseAddress(false);

// Linux keeps the port blocked after shutdown if we don't disable this.
connector.setSoLingerTime(0);

Server server = new Server();
server.addConnector(connector);

// Create a new web app in the war directory.
WebAppContext wac = new WebAppContextWithReload(logger, appRootDir
.getAbsolutePath(), "/");

RequestLogHandler logHandler = new RequestLogHandler();
logHandler.setRequestLog(new JettyRequestLogger(logger));
logHandler.setHandler(wac);
server.setHandler(logHandler);
server.start();
server.setStopAtShutdown(true);

// Now that we're started, log to the top level logger.
Log.setLog(new JettyTreeLogger(logger));

return new JettyServletContainer(logger, server, wac, connector
.getLocalPort(), appRootDir);
}

protected AbstractConnector getConnector() {
return new SelectChannelConnector();
}

private void checkStartParams(TreeLogger logger, int port, File appRootDir) {
if (logger == null) {
throw new NullPointerException("logger cannot be null");
}

if (port < 0 || port > 65535) {
throw new IllegalArgumentException(
"port must be either 0 (for auto) or less than 65536");
}

if (appRootDir == null) {
throw new NullPointerException("app root direcotry cannot be null");
}
}

}

Mittwoch, 8. Juli 2009

Zuverlässigkeit der Google Apps-Services

Als Ergänzung des Artikels ‘Nutzung von Google Mail, Kalender, Docs und Apps an Hochschulen in Deutschland und weltweit’ hier noch eine kleine Sammlung von Links mitQP1050077 Aussagen zur Zuverlässigkeit der Google Dienste (Apps-Services). Grundsätzlich ist dabei wohl eine Unterscheidung zwischen der kostenfreien (aka werbefinanzierten) Standard Edition und der kostenpflichtigen Professional Edition zu treffen. Einen Überblick über die verschiedenen Editionen bietet im übrigen der englische Wikipedia Artikel.

Google Apps Professional / Premier

In der Google Apps Hilfe wird zur Verfügbarkeit folgende Aussage getroffen:

Wir garantieren, dass Google mindestens 99,9 Prozent der Zeit verfügbar ist, sodass Mitarbeiter produktiver arbeiten können und Sie sich weniger Gedanken über einen Systemausfall machen müssen.**

Nach Googles Rechnung ergibt die 15 Minuten Ausfallzeit im Monat. In den Fußnoten wird allerdings auf die Nutzungsbedingungen verwiesen mit der Einschränkung, dass die SLA nur gilt, wenn die Apps im Einklang mit diesen Bedingungen verwendet werden. Die Professional Edition wird heute (08.07.2009) zum Preis von 40€/Nutzerkonto/Jahr angeboten.

In der Seite wird auch ein Vergleich mit Microsoft Exchange von der Radicati Group aufgeführt. Demnach sei bei Exchange Servern mit deutlich höheren ungeplanten Ausfallzeiten zu rechnen.

Education Edition

Ist laut Wikipedia eine Variante der Premier / Professional Edition. Allerdings offenbar ohne die Verfügbarkeitsgarantie.

Standard Edition

Hier habe ich keine Informationen von Google gefunden. Allerdings ist nach den bisherigen Erfahrungen die Zuverlässigkeit kaum weniger hoch als die Professional Edition. Oder kennt jemand Quellen?

Sich selbst überzeugen…

..kann man in gewissem Umfang über das Apps Status Dashboard:

http://www.google.com/appsstatus#hl=de

Hier kann man für die verschiedenen Dienste die entsprechenden Statusinformationen abrufen, dabei kann man auch weit in die Vergangenheit gehen und sich bei den (relativ wenigen) Problemfällen ansehen, wie Google damit umgegangen ist (und sich z. B. die Lösungsdauer ansehen). Die Ziele, die Google mit dem Dashboard verfolgt, werden von Google selbst so beschrieben:

Im Februar haben wir das Apps Status Dashboard auf den Weg gebracht, um euch besser über den Zustand und die Leistungsfähigkeit unserer Services wie Google Mail, Google Kalender oder Google Text & Tabellen auf dem Laufenden zu halten. Dieses Dashboard könnt ihr nutzen, um zu jedem Zeitpunkt zu überprüfen, ob unsere Services problemlos funktionieren oder ob etwa eine Störung vorliegen sollte. Wir haben viele positive Rückmeldungen und hilfreiche Hinweise von Nutzern und Kunden erhalten, um das Dashboard noch nützlicher zu machen.

Auf Grund der hohen Sichtbarkeit wird es Google wohl auch nicht einfach möglich sein in dem Dashboard Ausfälle zu verschleiern.

Dienstag, 7. Juli 2009

Newsfeeds für Kategorien in Blogspot / Blogger Blogs

Für die Integration unseres Caféblogs in unsere Homepage verwenden wir die vom 200601_DSCF0026b Blogspot Dienst angebotenen Newsfeeds und die Google Ajax Feed API. Für die meisten Zwecke reichte dabei der Standardfeed, der für dieses Blog unter folgender URL abgerufen werden kann (Atom Format):

http://curtstech.blogspot.com/feeds/posts/default

Der fett gedruckte Teil der Adresse würde bei einem anderen Blog abweichend. Alternativ ist auch folgende URL möglich:

http://www.blogger.com/feeds/2321699238659363147/posts/default

Dabei ist 2321699238659363147 die Blog-Id, die man z. B. im Newsfeed selbst finden kann.

Was ist mit Kategorien bzw. Labels?

In den Blogspot Blogs können die Einträge mehreren Kategorien oder Labels zugeordnet werden. Für unsere Homepage brauchen wir nun allerdings auch die Möglichkeit alle Meldungen der Kategorie/des Labels Aktuell abzurufen für eine separate Einbindung.

Eine Beschreibung, wie man auch Feeds mit den Einträgen bestimmter Kategorien abrufen kann, findet sich in der Hilfe zu den Blogger APIs und zwar in der Seite ‘Protocol’. Dort bringt der Text unter der Überschrift Retrieving posts using query parameters die Lösung:

http://hof-brune.blogspot.com/feeds/posts/default/-/Aktuell

oder

http://www.blogger.com/feeds/3088683439170101886/posts/default/-/Aktuell

Donnerstag, 25. Juni 2009

Nutzung von Google Mail, Kalender, Docs und Apps an Hochschulen in Deutschland und weltweit

Googles immer weiter wachsendes Portfolio von webbasierten Anwendungen ist inzwischen offensichtlich auch für Hochschulen interessant. Über das Google Enterprise Blog werden QP1030854-1 Erfolgsgeschichten wie die von der Boise State University, bei der offenbar mehr als 20.000 Studierende plus MitarbeiterInnen auf die Google Services umgestellt wurden, verbreitet. Google selbst bündelt die Beschreibung seines Angebotes für Bildungseinrichtungen auf einer speziellen Seite.

Interessant ist für mich dabei zunächst die Frage, welche Verbreitung diese Dienste, die ja vergleichsweise jung und oft noch als Beta Version gekennzeichnet sind, inzwischen unter Hochschulen in Deutschland, aber auch weltweit erreicht haben. Im Anschluss will ich dann noch keine Beurteilung wagen, welche Vorteile die Nutzung dieser Dienste mit sich bringt, und welche Probleme bei der Einführung an einer Hochschule wohl zu bewältigen sein werden.

Wer nutzt die Google Services

Bei der Frage ‘Wer nutzt die Google Services’ ist es zum einen interessant zu fragen, in welchem Umfang denn die Services verwendet werden, zum anderen aber auch wie groß denn die jeweilige Bildungseinrichtung ist. Es macht natürlich einen Unterschied, ob man eine Hochschule mit 1.000 Studierenden umstellen, oder mit 65.000. Viele Beispiele auch von Hochschulen findet man auf der Kunden Seite der Google Apps.

USA

Hier wird nur eine kleine Auswahl von besonders umfangreichen Projekten getroffen. Laut Google soll es hunderte von Bildungseinrichtungen geben, die die Google Apps nutzen:

Hochschule #Studierende/
MitarbeiterInnen
Nutzungsumfang  

Arizona State University

65.000 Studierende

GMail, Chat, Kalender Fallstudie
Lakehead University, Ontario 38.000 Studierende und Mitarbeiter GMail, Chat und VoIP, Kalender Fallstudie
Northwestern University   GMail, Chat, Kalender Studierende forderten die Einführung
University of Maine System, Verbund von Hochschulen des Bundesstaates Maine Laut Wikipedia insgesamt ca. 34.000 Studierende. Haben einen zentralen IT Dienstleister GMail, Kalender, Docs Die Ankündigung des IT Dienstleisters geht auf Fragen wie Werbeeinblendungen und Datenbesitz ein, vorhandene Mailadressen wurden beibehalten

Australien
Hochschule #Studierende/
MitarbeiterInnen
Nutzungsumfang  
University of Adelaide 16.000 Studierende GMail ZDNet Artikel
Mails aus existierenden Accounts werden automatisch übertragen
New South Wales, Bundesstaat Der Mailservice des kompletten Bundesstaats wurde zentral vergeben, betrifft offenbar 1.3  Millionen Schüler/Studierende GMail Bloomberg Meldung
Es wurde damit eine der größten Outlook/Exchange Installationen abgelöst
Macquarie University 31.000 Studierende, 37.000 Alumni, Mitarbeiter später (Stand 2007) GMail MIS Meldung

Deutschland

Hier bin ich leider nicht wirklich fündig geworden. Zwar gibt es viele eLearning Bereiche an deutschen Hochschulen, die den Einsatz von Google Docs diskutieren, aber eine Hochschule, die die Nutzung der Google Apps zur Strategie gemacht hat, konnte ich nicht aufspüren. Also nur ein Treffer:

Hochschule #Studierende/
MitarbeiterInnen
Nutzungsumfang  
TU Chemitz   Kalender URZ Infoseite

Welche Vorteile könnte man von der Nutzung der Google Services erwarten

Was macht das Google Angebot eigentlich so attraktiv für Hochschulen? Hier ein Benutzerbekenntnis aus einer Google Seite:

Google Apps Education unterstützt die Arizona State University bei ihrer Entwicklung hin zu einer überaus flexiblen Universität, die ihren Studenten hervorragende technische Voraussetzungen bietet. Die Einbindung von Webmail, Instant Messaging und Kalenderfunktionen bei Google ist einfach konkurrenzlos.

- Kari Barlow, Vizedirektor des University Technology Office, Arizona State University

Coole|Visionäre|Moderne Services auf der Höhe der Zeit

Im Google Paket sind Dienste enthalten, die heute vermutlich zu den besten und innovativsten ihrer Art gehören. Hier nur eine minimale, sehr subjektive Aufstellung:

GMail: Hat die Art wie man mit Mail arbeitet revolutioniert. Es erscheint heute schwer vorstellbar wie man ohne thread conversations, superschnelle Suche, eingebauten Chat, Dokumentenvorschau und dergleichen jemals leben bzw. effizient arbeiten konnte. In einem Test der Stiftung Warentest hat GMail als bester Dienst abgeschnitten und Spam ist fast kein Problem mehr. Und die Studierenden sind schon da, zumindest ein signifikanter Anteil. Hinzu kommt der quasi unbegrenzte Speicherplatz von heute 7 Gigabyte. Von Hochschulen angebotene Maildienste bieten meist nur einen Bruchteil, so erhalten die Studierenden an der Universität Bielefeld 500 Megabyte große Mailpostfächer.

Kalender: Mit seiner Offenheit, Flexibilität und Kollaborationsfunktion sicher eines der führenden Produkte in diesem Segment. Der Kalender lässt sich in GMail, iGoogle und anderen Kalenderanwendungen integrieren.

Docs: Auch wenn sie weiterhin im Vergleich zur den klassischen Officepaketen weniger Funktionen bieten, so haben sie doch gerade im Bereich der Kollaboration neue Wege gezeigt, die für eine Hochschule z. B. im Bereich der elektronische Lehr-Lern-Unterstützung viel höher zu bewerten sind.

Da alle Dienste komplett webbasiert sind lassen sich Weiterentwicklungen sehr schnell ausrollen und stehen allen NutzerInnen unmittelbar zu Verfügung.

Integrationsmöglichkeiten

Google scheint im Konkurrenzkampf mit Microsoft auf Offenheit zu setzen (siehe Heise). Bestätigt wird dies durch Fallbeispiele wie das der Arizona State University, bei denen z. B. Anmeldungen über hochschuleigene CAS Dienste realisiert wurden.

Das Geld

Auf dieser Google Seite ist davon die Rede, dass die Dienste kostenlos seien. Aber möglicherweise gibt doch bestimmte Aufwendungen, wenn man wie z. B. die Grand Valley State University die Mailpostfächer werbefrei halten will. In diesem Artikel der PC Welt ist allerdings auch die Rede davon, dass für Bildungseinrichtungen die Nutzung kostenlos sei.

Die Personaleinsparung

Die Nutzung der Google Anwendungen könnte massive Entlastungen beim Betrieb der IT Infrastrukturen bringen. Dies hat zwei Gründe:

  1. Cloud Computing: Eine ganze  Reihe von bisher von der Hochschule zu betreibenden Diensten wird ausgelagert und kann von erheblich weniger Personal betreut werden. Beispiele für solche wegfallenden Services: Mailserver, eLearning Dienste, Serverbetrieb, Backup
  2. Das Web als neuer Desktop: Alle Google Anwendungen sind komplett webbasiert und lassen sich mit einer großen Anzahl von Browsern auf beliebigen Betriebssystemen nutzen. NutzerInnen dieser Dienste, die keine anderen Anwendungen brauchen, können sich an beliebigen PCs anmelden und arbeiten. Für den IT Support bedeutet dies die Option stark standardisierte, wartungsarme PC Installationen vorzunehmen.

Beide Aspekte haben das Potential das vorhandene IT Personal an Hochschulen stark zu entlasten.

Welche Probleme könnte man erwarten

Abgabe von Kontrolle

Die Nutzung der Google Dienste bedeutet den Einstieg in das Cloud Computing. Bisher direkt an der Hochschule betriebene Dienste wie Mailserver werden ausgelagert in eine technische Struktur, auf die die Hochschule keinen direkten Zugriff mehr hat. Daraus ergeben sich u. a. folgende Fragen:

  • Wie kann der Zugriff der Hochschule auf ihre Daten gesichert werden, um z. B. einen Anbieterwechsel prinzipiell offen zu halten?
    • Teilfrage: kann/soll sich die Hochschule komplett auf die Datensicherung durch den Anbieter verlassen, oder ist ein eigenes Backup notwendig?
    • Teilfrage: Was sind überhaupt die Daten der Hochschule? MitarbeiterInnen und insbesondere Studierende werden die Plattformen auch für individuelle Zwecke nutzen (private Dokumente, Mails, etc.). Dies sind dann keine Daten der Hochschule mehr
  • Wie ist eine solche Auslagerung von Daten und Datenverarbeitungen mit der Datenschutzgesetzgebung vereinbar?
  • Was sind angemessene Service Level Agreements (SLAs), wie wird die Einhaltung kontrolliert und wie werden Verstöße sanktioniert?
Vorbehalte der BenutzerInnen gegen Google

Google stößt trotz seines ‘Don’t be evil’-Mottos zunehmen auf Vorbehalte insbesondere bei Datenschützern,  denen der Informationshunger des Unternehmens suspekt ist. Im heterogenen Umfeld einer Hochschule ist auf jeden Fall damit zu rechnen, dass unter den MitarbeiterInnen und Studierenden ein Anteil vorhanden ist, der dieses Bedenken teilt.

Zum Schluss

Habe ich etwas vergessen? Gibt es weitere Hochschulen in Deutschland, die ich übersehen habe? Bin für Ergänzungen und Hinweise dankbar!

Update

Die spezielle Frage nach der Zuverlässigkeit der Google Dienste habe ich in einem späteren Blogeintrag kurz betrachtet: Zuverlässigkeit der Google Apps

Update  2

Hier zwei interessante Blogeinträge, die die Integration von Google Apps mit Learning Management Systemen (LMS) wie Moodle und Blackboard beschreiben:

In einem anderen Beitrag geht es um ein Angebot an Schulen ihren Mailservice von Googles Postini Service sicherer machen zu lassen:

Diese Beiträge zeigen, auf welch unterschiedlichen Wegen Google Angebote für Bildungseinrichtungen platziert.

Mittwoch, 24. Juni 2009

Nutzung von JSON Daten in GWT und GXT

Das Google Web Toolkit GWT bietet von Haus aus bereits eine relativ einfach nutzbare (c) H. Brune Möglichkeit Daten von Servern im JSON Format abzuholen. Hier soll ein kleiner Vergleich mit dem Möglichkeiten gezogen werden, die GXT bzw. Ext GWT bieten.

Wichtig ist dabei insbesondere die Frage, wie einfach JSON Daten mit Widgets verbunden werden können, da wir relativ viele Datensatzarten haben, die aber in mehr oder weniger gleichartigen Masken bearbeitet werden sollen. Die Frage, wie die Daten dann wieder an den Server zurück geschickt werden spielt hier erstmal keine Rolle.

Testvorbereitungen: Statische JSON Dateien

Um die Implementierung zu vereinfachen werden die JSON Daten im GWT über statische Dateien verfügbar gemacht. Im meinem über das Google Plugin automatisch erstellten Eclipse Projekt gibt es den /war-Ordner, der seid GWT Version 1.6 die komplette Webanwendung enthält. Für die statischen JSON Dateien wird dort einfach ein Unterordner angelegt, nennen wir ihn modell/.

Beispieldatei: /war/modell/abschluss.json

Inhalt der Datei:

[
{
id : 123,
name : Abschluss 1,
kuerzel : Abs1
},
{
id : 456,
name : Abschluss 2,
kuerzel : Abs2
},
]








Diese Datei kann im Jetty Server, der in Ecplise gestartet wird, unter http://localhost:8080/modell/abschluss.json abgerufen werden (bzw. unter dem konfigurierten Port).









JSON mit GWT









Hier nur eine ganze kurze Skizze wie dies funktioniert, die ausführliche Beschreibung findet sich in dieser GWT 1.6 Seite:













  1. JSON Quelle anlegen (haben wir schon)






  2. Stub Klasse anlegen, welche JSNI Methoden implementiert um direkt auf die Inhalte der JSON Struktur zuzugreifen. Instanzen der Stub Klasse werden über eine JSNI Methode generiert, die im Kern eval() aufruft (Sicherheitshinweise beachten!)







  3. Implementierung eines asynchronen RequestBuilder Aufrufes, der die JSON Daten holt und verarbeitet.







  4. Erzeugung einer Darstellung, z . B. mit einem Grid.











Wichtig: Der URL zur JSON Datei darf nicht GWT.getModuleBaseURL() vorangestellt werden, wie es im GWT Tutorial gezeigt wird. Es reicht eine Angabe dieser Art:









	private static final String JSON_URL = 
"/modell/abschluss.json";








Erfahrungen












  • Die Implementierung der Stub Klasse kann dann ärgerliche Mehrfacharbeit bedeuten, wenn es auf der Serverseite bereits entsprechende (Bean-)Klassen gibt. Hier lässt sich aber kein Reuse erzielen






  • Sobald man die Stub Klassen hat ist der Rest der Implementierung vergleichsweise einfach, abgesehen von der üblichen ‘Mühsal’ der Darstellung mit den relativ primitiven GWT Widgets










Quellcode








package com.chb.gxt2m3.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;

public class GXTTest implements EntryPoint {

public void onModuleLoad() {

Button btLaden = new Button("Laden");
RootPanel.get("buttons").add(btLaden);

btLaden.addClickHandler(new ClickHandler() {

public void onClick(ClickEvent event) {
refreshAbschluesse();

}
});
}

private static final String JSON_URL_ABS = "/modell/abschluesse.json";

private void refreshAbschluesse() {

RootPanel.get("display").clear();
RootPanel.get("display").add(
new Label("Abschlüsse werden geladen von " + JSON_URL_ABS));

RequestBuilder builder = new RequestBuilder(RequestBuilder.GET,
JSON_URL_ABS);

try {
builder.sendRequest(null, new RequestCallback() {

public void onError(Request request, Throwable exception) {
displayError("JSON Aufruf konnte nicht ausgeführt werden");
}

public void onResponseReceived(Request request,
Response response) {

if (200 == response.getStatusCode()) {

clearError();
abschluesseZeigen(asArrayOfAbsData(response.getText()));

} else {
displayError("Fehler beim JSON Aufruf ("
+ response.getStatusText() + ")");
}

}
});
} catch (RequestException e) {
displayError("Zugriff auf JSON gescheitert");
}
}

private void abschluesseZeigen(JsArray abs) {

RootPanel.get("display").clear();

if (abs == null || abs.length() == 0) {
RootPanel.get("display").add(
new Label("Keine Abschlüsse geladen..."));
} else {
Grid agrid = new Grid(abs.length() + 1, 3);
RootPanel.get("display").add(agrid);
agrid.setWidget(0, 0, new HTML("Id"));
agrid.setWidget(0, 1, new HTML("Kürzel"));
agrid.setWidget(0, 2, new HTML("Name"));

for (int idx = 0; idx < abs.length(); idx++) {
AbschlussData a = abs.get(idx);
agrid.setWidget(idx + 1, 0, new Label(""
+ a.getId()));
agrid.setWidget(idx + 1, 1, new Label(a.getKuerzel()));
agrid.setWidget(idx + 1, 2, new Label("" + a.getName()));
}
}
}

private void clearError() {
RootPanel.get("errorMsg").clear();
}

private void displayError(String error) {
clearError();
if (error != null)
RootPanel.get("errorMsg").add(new Label(error));
}

private final native JsArray asArrayOfAbsData(String json) /*-{
return eval(json);
}-*/;
}








JSON mit GXT









Eine Stärke von GXT (dessen Version 2 heute als Milestone 3 vorliegt) sind sicher die tollen Widgets und die Databinding Möglichkeiten, durch die sich Änderungen am zu Grunde liegenden Datenmodell an alle Teile der Anwendung melden lassen. Leider gibt es in der Demoseite nur ein Gridbeispiel mit einer XML Quelle. Etwas Suchen zeigt einem aber, dass es auch einen JsonReader gibt. Man kann das Beispiel also übertragen.









Ein Problem ist aber der Aufbau der JSON Datei. Offenbar sind verschachtelte Formate nicht möglich, zumindest kann man diesen Thread im ExtJS Forum wohl so verstehen.









Hier gibt es aber noch ein anderen Beispiel: http://extjs.net/forum/showthread.php?t=71978

Dienstag, 23. Juni 2009

Java, HTML und Javascript Quellcode schön im Blog darstellen?

Beim Bloggen über Themen der Programmierung kommt man schnell dazu viele Quellcode(c) H. Brune Häppchen zeigen zu wollen, damit das Gesagte direkt verdeutlicht werden kann. Ein Copy-and-Paste aus der Lieblingsentwicklungsumgebung funktioniert da leider nicht, übrig bleibt meist nur ein wirrer Worthaufen, der manuell mühselig wieder aufbereitet werden muss.

Aber es gibt Lösungen, mit denen man (fast) die Einfachheit eines Copy-and-Paste erreicht:

Google Code Prettifier

Mit dem Google Code Prettifier werden Bloginhalte mit Quellcode, sofern sie in ein bestimmtes Tag eingeschlossen werden, durch von Google geliefertes Javascript aufgehübscht. Eine Beschreibung in Deutsch findet sich im Google Watch Blog, dort wird die Menge der unterstützten Sprachen so definiert:

Unterstützt werden zur Zeit die Sprachen bash, C, C++, Java, JavaScript, Perl, Python, XML, HTML, HTMLXmp, WhiteSpace und misc1 - damit fast alle wichtigen.

Auch CSS gehört inzwischen offenbar zum Funktionsumfang dazu. Man hat also alles um z. B. eine GWT Programmierung schön zu zeigen.

Wie funktionierts

Als Vorbereitung lädt man die CSS und JS Dateien von der Prettify-Webseite. Diese Dateien muss man auf dem eigenen Webserver verfügbar machen. Es gibt dabei für die unterschiedlichen Sprachen eigene JS Dateien, vermutlich kann man die, von denen man annimmt sie nie zu brauchen, weglassen. Danach bindet man CSS Datei und die Hauptjavascriptdatei in sein Blog ein und ergänzt im body Tag ein

<body onload="prettyPrint()">

Damit sind die Vorbereitungen abgeschlossen. Um nun Quellcode zu formatieren wird der entsprechende Codeblock einfach mit einem

<pre class="prettyprint">...</pre>

umgeben. Es ist dabei möglich die Sprache mit anzugeben, wenn man dies nicht tut, so ‘rät’ der Prettifier. Beispiel:

<pre class="prettyprint lang-java">...</pre>

Links:

Homepage Google Code Prettify: Mit Download der notwendigen Dateien und Doku

Open Tutorial: Hier gibt es ergänzende Hinweise in Deutsch zur Einbindung von HTML/XML

Beispiel

Und so sieht es dann aus:

	/**
* Kleine Hilfsmethode zum Test auf leere Strings.
*
* @param to_test
* Zu testender String
* @return Wenn der String null oder leer oder nur mit
* Leerzeichen gefüllt ist kommt true, sonst false.
*
*/
public static boolean isEmptyString(String to_test) {
return to_test == null || to_test.trim().length() == 0;
}

Samstag, 6. Juni 2009

Wie macht man einen richtigen Carajillo?

Nachdem wir in unserem Stammlokal Theke im Canta © H. Bruneletztens zu einem leckeren Heißgetränke überredet und damit angefixt wurden wollen wir wissen, was das war und wie man es herstellt. Klar war nur, dass Espresso rein muss und ein Brandy, z. B. Osborne Veterano. Und es hat etwas mit Feuer zu tun.

Der Name

Die erste Hürde war der Name: wir verstanden so etwas wie ‘Karacho’, ‘Carrachero’ oder ‘Karachiro’. Mit etwas googlen fand wir zunächst zu ‘Carachillio’, was auch tatsächlich einige Suchtreffer, darunter sogar eine Speisekarte, bringt. Allerdings kein Rezept bzw. keine brauchbare Beschreibung.

Ein ‘richtigerer’ und zumindest im deutschsprachigen Web häufiger genutzter Name ist Carachillo. Unter diesen Suchstichwort findet man dann schon einige hilfreiche Treffer.

Nachdem wir jetzt noch das ‘ch’ durch das im spanischen übliche ‘j’ ersetzten waren wir endlich am Ziel und erkannten. Wir haben also einen

C a r a j i l l o

getrunken! Unter diesem Namen findet man auch einen Eintrag in der Wikipedia. Uns interessiert dabei natürlich nicht die simple Variante, bei der einfach Espresso und Alkohol zusammengeschüttet werden. Wir wollen das volle Programm mit Feuer und Karamellgeschmack.

Rezepte im Internet

Beim googlen nach Carachillo findet man z. B. folgende Rezepte:

http://www.cocktaildreams.de/cooldrinks/cocktailrezept.carachillo.2446.html

Den Veterano in die Tasse (eine größere Espressotasse) geben und mit Wasserdampf erhitzen. Anschließend anzünden, um den richtigen Geschmack zu erhalten.
Jetzt zwei Kaffeebohnen und die Zitronenschale in die Tasse geben und den heißen Espresso hinzuschütten.

http://toprezepte.ch/index.php/rezepte-von-a-z/trinkbares/54-carachillo

4-6 cl Brandy vorzugsweise in ein kleineres, robustes Glas reingeben
2 Zuckerwürfel dazugeben
Brandy mit Dampf erhitzen und anschließend anzünden
Mit einem Löffel umrühren bis kein Zuckerkorn mehr sichtbar ist.
Ein Espresso in einem dünnen Strahl zum Brandy geben (es muss ein Knistern zu hören sein)
Gegebenenfalls Flamme ablöschen

Entscheidend ist das Verbrennen des Zuckers, damit ein Karamellgeschmack entsteht, der den Espresso erst zum Carajillo macht. Auch der Wikipedia Artikel betont dies.

Nicht ganz einig sind sich die unterschiedlichen Rezepte dabei, ob Kaffeebohnen und Zitronenschale hineingehören und wenn ja, ob sie schon mit dem Brandy zusammen erhitzt werden oder erst später dazu kommen.

Muss es Hochprozentiger sein?

Normalerweise brennen Spirituosen erst ab einem Alkoholgehalt von ca. 50%. Muss also für den Carajillo ein entsprechend potenter Brandy verwendet werden? Nein, denn durch das Erhitzen wird der Alkohol leicht freigesetzt und ein Brandy wie der Veterano mit 36% reicht aus.

Und so haben wir dann unsere ersten Carajillos gemacht

Zwei Teelöffel Zucker in eine unsere Cappuccino Tassen. Ca. 3cl, also etwas mehr als ein Carajillo © H. BrunePinnchen, Veterano dazu. Die 4-6cl aus einem der vorherigen Rezepte erschienen uns doch etwas sehr heftig.

Mit der Wasserdampfdüse der Espresso Maschine das Zucker-Brandy-Gemisch gut aufheizen. Vorsicht, es spritzt. Aber je heißer man das Ganze macht desto besser brennt der Brandy. Nach dem Erhitzen die Tasse etwas schwenken, meist löst sich der Zucker dann schon auf.

Nun ist es so weit: Ein Streichholz an den Tassenrand halten und sofort sollten blaue Flammen lodern. Wir hatten zunächst Angst dabei vielleicht zu viel Alkohl zu verbrennen, aber selbst bei dem Versuch mit der längsten Brenndauer war das Ergebnis immer noch sehr hochprozentig. Also: Brandy richtig heiß machen und ordentlich brennen lassen, damit auch Karamell entstehen kann.

Während des Brennens noch mit einem langen Löffel etwas rühren, vielleicht trägt dies dazu bei, dass der Zucker aufgelöst und richtig erhitzt wird. Auf Zitrone und Kaffeebohnen haben wir bisher verzichtet.

Bevor die Flammen erlöschen die Tasse unter die Espressomaschine und wie im Bild durch den hineinlaufenden Espresso ablöschen. Wir haben einen doppelten Espresso hinzugefügt, was für unseren Geschmack genau passend war. Wenn man es richtig macht (bei uns erst im 4. Versuch) hört man wirklich ein starkes Knistern in Moment, in dem der erste Kaffee in den heißen Brandy fließt.

Für uns ist das Besondere des Carajillos neben seinem Geschmack und dem bei der momentanen Schafskälte sehr hilfreichen Durchwärmeffekt vor allem die Ästhetik seiner Herstellung: Die blauen Flammen, die um die Espressomaschine wabern, geben der Sache einen einmaligen Reiz.

Ach ja: Beim Trinken SEHR vorsichtig sein, da die Tassenränder enorm heiß sind. Und anders als z. B. bei einem Sambuca sehen die Gäste am Tisch nicht, dass das Getränk einmal gebrannt hat und sind daher unvorbereitet.

Ähnliche Getränke

Dem Carajillo nicht unähnlich scheint der Café Royal zu sein. Ein Rezept findet sich z. B. hier:

http://www.lebensmittellexikon.de/c0001200.php

Oder bei Maggi:

http://www.maggi.de/Rezepte/kochstudio/rezepte/rezeptsuche/default.htm?id=12142&action=detail

Und auch hier:

http://www.ecocktail.de/de/ecocktail-cgi/datenbank/cocktail_rezept.cgi?cocktail_id=2790&cocktail_name=Cafe%20Royal%201

Mittwoch, 6. Mai 2009

GWT Lernen: Verwendung von H1, H2, etc. für Überschriften

GoogleDesktopPhotosPluginWallpaper Beim Aufbau meiner Seiten mit dem GWT möchte ich die üblichen HTML Tags H1-H6 verwenden. Wie geht das?

Die direkt Verwendung eines Labels funktioniert nicht. Eine Anweisung wie:

this.add(new Label("<h1>Titel</h1>"));

erzeugt im hosted mode die Darstellung

<h1>Titel</h1>

, die Tags werden also quotiert. So steht es auch in der Label Java Doc. Dort findet sich aber auch der Hinweis auf das HTML Widget. Damit lässt sich die Aufgabe lösen. Die Anweisung:

this.add(new HTML("<h1>Titel</h1>"));

erzeugt die gewünschte Darstellung. Berücksichtigen muss man nur, dass man so die Sicherheitsvorteile, die die automatische HTML Quotierung des Labels bietet, verliert.

Dienstag, 5. Mai 2009

Externe Klassen im GWT nutzen

P1090268bIn meiner GWT 1.6 Anwendung nutzen sowohl der client- wie auch der serverseitige Code die gleichen Geschäftsmodellklassen. Diese Klassen liegen allerdings in einem anderen Eclipseprojekt und sind daher nicht ohne weiteres in meinem mit dem Google Plugin angelegten Eclipse GWT Projekt verfügbar. Damit alles rund läuft sind folgende Konfigurationen notwendig:

Eclipse Konfiguration

Simpel: ‘Wie immer’ in den Project Properties den Java Build Path um das Projekt erweitern, in welchem das Geschäftsmodell entwickelt wird (also den CLASSPATH erweitern). Danach kann man sowohl in der client- und serverseitigen GWT Programmierung schon mal die entsprechenden Klassen in Eclipse nutzen.

Generierung des client-seitigen Codes

Das aus meiner Sicht Beste am GWT, nämlich die Übersetzung von Javascript Code aus Java Code, sorgt für eine kleine Komplikation: Der GWT Compiler braucht für alle in der Clientprogrammierung referenzierten Klassen den Quellcode. Ohne Quellcode keine JS Konvertierung und damit keine Clientanwendung. Man findet in der Fehlerausgabe des Hosted Modes Einträge wie den Folgenden, so lange die Geschäftsmodellklassen nicht verfügbar sind:

[ERROR] Line 21: No source code is available for type x.y.z.modell.KlasseXY; did you forget to inherit a required module?

Ähnliche Meldungen erhält man, wenn man in Eclipse das Projekt neu übersetzt:

Compiling module x.y.z.GWTProj
   Refreshing module from source
      Validating newly compiled units
         Removing units with errors
            [ERROR] Errors in 'file:/…/GWTProj/src/x/y/z/client/Modell.java'
               [ERROR] Line 5: The import x.y.z..modell.KlasseXY cannot be resolved

…..

[ERROR] Line 40: Missing code implementation in the compiler

Der Hinweis in der Fehlermeldung auf ein fehlendes inherit führt einen in die Beschreibung der module XML files. Eigentlich hatte ich gehofft, dass es mindestens im Fall von Klassen, die keine Widgets sind, nicht notwendig sein würde mein komplettes Geschäftsmodell mit speziellen module XML files zu durchsetzen. Dafür finde ich aber leider keinen Weg. Musste daher also so vorgehen:

Das Geschäftsmodell befindet sich im Paket org.xy.smod. In diesem Paket gründe ich die neue Modul-XML-Datei SMod.gwt.xml, die folgenden Inhalt hat:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 1.6.4//EN" "
http://google-web-toolkit.googlecode.com/svn/tags/1.6.4/distro-source/core/src/gwt-module.dtd">

<module rename-to='smod'>

<source path="" />

</module>

Ich lege also ein neues ‘Modul’ an mit dem Namen smod. Die source-Anweisung ist notwendig, damit alle Klassen direkt im Paket einbezogen werden. Ansonsten ist die Datei leer. Insbesondere müssen nicht die sonst üblichen GWT Module wie com.google.gwt.user.User eingebunden werden, auch gibt es natürlich keinen entry-point.

Jetzt muss ich in der Modul-XML-Datei meiner eigentlichen Anwendung nur noch dieses Modul über inherit einbinden. Dazu wird folgende Zeile eingefügt:


<inherits name='org.xy.smod.SMod'/>

Damit sind wir fertig! Zumindest in meiner Umgebung findet der GWT Compiler jetzt die Quellcodes der Geschäftsmodellklassen, die in einem anderen Eclipse Projekt liegen, und kann sowohl die Kompilierung wie auch den Start im hosted mode erfolgreich durchführen.

Hinweis: Eine gute Beschreibung des GWT Compilers im Allgemeinen findet man in den im Internet frei erhältlichen Abschnitt ‘Understand the GWT Compiler’ aus dem Buch GWT in Practice.

Testen der serverseitigen Programmierung / Deployment im Webserver

Sobald man die serverseitige Programmierung testen will bzw. bei der Installation des Anwendungs-WARs in Produktionsserver müssen den RPC Servlets ebenfalls die Geschäftsmodellklassen zur Verfügung stehen. Da es sich hier wieder um eine reine Java Angelegenheit handelt ist nur der Bytecode z. B. in Form eines entsprechenden JARs notwendig. Das Jar packt man ‘wie immer’ in /WEB-INF/lib/ und ist fertig.

Umgang mit java.io.Serializable

P1090074b Hier eine kleine Sammlung von Links zum Thema Serialisierbarkeit in Java, was insbesondere auf die Implementierung des Interfaces Serializable hinausläuft.

Kurzantwort auf  die Frage, wie man eine Java Klasse serialisierbar macht:

  1. Klasse muss Interface Serializable implementieren
  2. Klasse muss eine serialVersionUID enthalten, deren Wert am besten über das im JDK enthaltene Tool servialver  erzeugt wird (dabei ggf. schön die Hinweise zum CLASSPATH beachten und NICHT versehentlich das Source-Verzeichnis adressieren).
    Wenn man Eclipse verwendet, so kann auch die IDE zur Generierung genutzt werden. Hinweise dazu hier.

Die fertige Klasse sieht dann z. B. so aus:

public class SMAbschluss
    implements Serializable {

    static final long 
      serialVersionUID = -689428171468533962L;

….

Samstag, 18. April 2009

Wie kann man sich vor Phorm schützen?

Über die britische Unternehmung Phorm gibt es z. Z. viel Aufregung in den Medien, z. B. auch in diesem Artikel auf Heise Online.

Man kann es aber auch mit Angst zu tun bekommen, wenn man liest, das einem in Zukunft Cookies nicht mehr nur von den besuchten Webseiten (und den unzähligen dort ggf. integrierten GoogleAds, Bannern, Trackern usw.) untergeschoben werden können, sondern auch von einem ‘Dienst’, mit dem man nie etwas zu tun haben wollte und der offenbar auch noch mindestens temporär die Rolle des angesurften Webservers übernimmt.

Grundsätzlich ist es offenbar im wesentlichen eine Frage, ob der eigenen Internetprovider mit Phorm kooperiert.

Freitag, 17. April 2009

IE 8: 'Die Datei konnte nicht in den Zwischenspeicher geschrieben werden'

P1080645b Heute meldete sich eine Nutzerin unserer Dienste, die ein Problem beim Download einer Worddatei aus unseren Webseiten hat. Sie verwendet den Internetexplorer (Version und Betriebssystem unbekannt). Das Problem lässt sich aber im MSIE 8.0.6001 unter Windows XP nachvollziehen.

Eingrenzung 1: Problem nur mit SSL

Mit einer neu angelegten Testdatei kann das Problem ebenfalls verifiziert werden, es liegt also nicht an der speziellen Datei, welche die Nutzerin verwendet hat. Allerdings tritt das Problem nur dann auf, wenn der Zugriff auf die Webseite per SSL (HTTPS) erfolgt. Bei einem HTTP Zugriff gibt es kein Problem.

Eingrenzung 2: Problem nur mit bestimmten Formaten

Das Problem tritt mindestens auf bei Word (Endung *.doc) und PDF Dateien (Endung *.pdf). Bei Bilddateien (Endung *.jpeg) tritt es nicht auf.

Internetquellen

Es gibt eine Microsoft Seite, welche sich um ein ähnliches (identisches?) Problem kümmert, allerdings nur bis zum IE 6 reicht.

By the way: diese Seite ist ein interessantes Beispiel für die manchmal rätselhaften Ergebnisse automatischer Übersetzungen. So kann man über Wortschöpfungen wie ‘Cache-control:no- speichert’ grübeln:)

Hier ist noch eine englische MS Seite, die das Thema behandelt (auch nur bis zum IE 6).  Eine erhellendere Beschreibung findet man in the downside.

Lösung

Wie in dem von the downside zitierten Blog beschrieben folgende Zeilen in das Servlet eingefügt:

response.setHeader("Content-Disposition", "attachment; filename=\"" + dateiName + "\"");
response.setHeader("Pragma", "public");
response.setHeader("Cache-Control", "max-age=0");

Damit funktioniert es dann im IE 8 und auch weiterhin im Firefox und Chrome.

Donnerstag, 9. April 2009

Kleine Javascript/CSS Bastelei: Zusammenhängende Einträge in einer großen Tabelle kennzeichnen

P1020986 Wir haben eine oft recht umfangreiche Tabelle in der eKVV Suche nach freien Räumen. In jeder Zelle werden die freien Räume gezeigt, wobei der gleiche Raum in unterschiedlichen Zellen an unterschiedlicher Position stehen kann. Dies lässt sich nicht verhindern, da die Tabelle sonst noch größer werden würde.

Damit die BenutzerInnen trotzdem einfach sehen können, ob der Raum in anderen Zellen (also zu anderen Zeiten) verfügbar ist wird mit Javascript und CSS folgende Funktionalität implementiert:

  • Sobald der Mauszeiger auf einen bestimmten Raum geht werden alle Vorkommnisse des Raums in der Tabelle anders formatiert.
  • Sobald der Mauszeige den Raum wieder verlässt wird die alte Formatierung wieder hergestellt.

Vorbereitung der gemeinsamen Formatierung: CSS Klassen

Damit die Formatierung aller Vorkommnisse eines Raumes leicht per CSS gemacht werden kann, erhalten die Links der Räume eine weitere CSS Klasse, die sich aus dem Raumnamen ableitet. Beispiel:

<a href=”..” class=”… raum_h1”>…</a>

Da in den Raumnamen prinzipiell alle Zeichen zugelassen sind muss eine Bereinigung durchgeführt werden. In der Bereinigung werden alle Zeichen bis auf a-z und 0-9 entfernt.

Mit dieser Vorbereitung können über eine CSS Anweisung wie der Folgenden alle Stellen mit dem gleichen Raum markiert werden:

a.raum_h1 {
     border: 1px solid black;
}

Hier sind der Phantasie natürlich keine Grenzen gesetzt. Die Manipulation des Stylesheets hat gerade bei großen Tabellen Performancevorteile im Vergleich zur Manipulation jedes einzelnen zu formatierenden Elements, z. B. durch Hinzufügen/Entfernen einer besonderen Klasse zu allen Links des gleichen Raums in den onmouseover/onmouseout Events.

Javascriptfunktionen für die Events onmouseover und onmouseout

Alles was wir jetzt noch brauchen sind zwei Eventhandler auf dem Anchortag:

  1. onmouseover: Beim ‘Betreten’ des Raumbereiches mit dem Mauszeiger muss die Markierung per CSS aktiviert werden
  2. onmouseout: Beim Verlassen muss die Markierung deaktiviert werden, damit nicht nach und nach alle Räume gekennzeichnet bleiben

Da es sehr viele Links gibt lagern wir die beiden Aktionen in eigene Funktionsdefinitionen aus. Das spart viel Platz im generierten HTML Code. Die Anchortags sehen dann so aus:

<a class=".. raum_h11"
          onmouseover="javascript:zr('raum_h11');"
          onmouseout="javascript:vr();" …

Jetzt müssen wir nur noch die Funktionen zr (für zeige Raum) und vr (für verberge Raum) implementieren und im HTML Code der Seite unterbringen. Warum beim Aufruf von vr kein Parameter übergeben wird? Das sehen wir gleich.

Nützlich Hinweise zur Manipulation der Stylesheets per Javascript finden sich z. B. im DADABase Blog und in den javascript.faqts. Mit diesen Hilfen bekommt man schnell die notwendigen Funktionen hin. Das wesentliche DOM Element ist dabei document.styleSheets:

<!-- Dieses leere STYLE Tag wird gebraucht für die folgenden JS Funktionen -->
<style></style>
<script type="text/javascript">
    function zr(cssClass) {
        vr();
        var sSheet = document.styleSheets[document.styleSheets.length-1];
        var selector = "a." + cssClass;
        var rule = "border: 1px solid black;";
        sSheet.insertRule("" + selector + " { " + rule + " }", 0);
    }
    function vr() {
        var sSheet = document.styleSheets[document.styleSheets.length-1];
        while (sSheet.cssRules && sSheet.cssRules.length > 0) {
            sSheet.deleteRule(0);
        }
    }
</script>

Wir machen es uns hier einfach und greifen mit document.styleSheets[document.styleSheets.length-1] direkt auf das extra für diesen Zweck angelegte, zunächst leere STYLE Tag zu. Falls noch andere STYLE Tags folgen sollten funktioniert das natürlich nicht so einfach!

Dieses Konstrukt funktioniert wunderbar in Firefox 3.0.8, in Chrome 2.0.x, in Safari 4 Public Beta und in Opera 9.64. Aber natürlich funktioniert es deshalb noch lange nicht im Internet Explorer. Und das tut es auch in der Tat nicht im MSIE 8.

Der Grund liegt darin, dass der MSIE kein insertRule kennt, sondern eine addRule Funktion. Das gleiche gilt auch für die Funktionen zum Entfernen von Rules. Ein Beispiel, wie man damit umgehen kann, findet sich in CodingsForums.com. Ein noch raffinierterer Trick wird auf TUTORIALHELPDESK.COM beschrieben. Dort wird für den MSIE das Stylesheetobjekt einfach um die noch fehlenden Funktionen ergänzt. Dieser Ansatz der einheitlichen Methoden hat nur beim Hinzufügen der Rules seine Grenze, da insertRule weniger Parameter erwartet als addRule

Mit diesem Wissen ausgestattet kann man nun die Funktionen so erweitern, dass sie auch im MSIE funktionieren. Hier das Ergebnis, welches nun auch im MSIE 8 funktioniert:

   <!-- Dieses leere STYLE Tag wird gebraucht für die folgenden JS Funktionen -->
   <style></style>

    <script type="text/javascript"> 
        function zr(cssClass) {
        vr();
        var sSheet = provideSSheet();
        var selector = "a." + cssClass;
        var rule = "border: 1px solid black;";
        try {           
            sSheet.insertRule("" + selector + " { " + rule + " }", 0);
        } catch (msie) {
            try {
                sSheet.addRule(selector, rule);
            } catch (err) {
            }
        }
    }
    function vr() {
        var sSheet = provideSSheet();
        while (sSheet.cssRules && sSheet.cssRules.length > 0) {
            sSheet.deleteRule(0);
        }
    }
    function provideSSheet() {
        var sSheet = document.styleSheets[document.styleSheets.length-1];
        // Check to see if we are operating in Internet Explorer
        if (sSheet.rules)
        {
            // Map the standard DOM attributes and methods to the internet explorer
            // equivalents
            sSheet.cssRules = sSheet.rules; 
            sSheet.deleteRule = function(ruleIndex)
            {
                 this.removeRule(ruleIndex);
            };
        }
        return sSheet;
    }   
</script>

Damit sind wir fertig! Und so sieht es dann aus:

ScreenShot_eKVV_Raumfreisuche

Erfahrungen

Die Lösung scheint in verschiedenen Browsern unterschiedlich schnell zu reagieren. Hier ein paar erste Erfahrungen:

Firefox: Grundsätzlich am problemlosesten. Alles ist flüssig.

Opera: Ähnlich wie Firefox.

Chrome/Safari: Der allererste Aufruf der JS Funktionen braucht eine gewisse Zeit, danach ist alles flüssig

MSIE 8: Nie wirklich schnell, das rasche Scrolling über die Seiten wird manchmal behindert durch die JS Funktionen

Montag, 6. April 2009

Wie kam die schöne Flasche auf den Hankenüll

P1170443 Bei der letzten Wanderung mit dem Hund im nahen Teutoburger Wald fand ich eine alte Getränkeflasche mit interessanter Form. Ich habe sie bis nach Hause getragen um zu sehen wie weit ich mit Hilfe des Internets der Frage nachgehen kann:

Wie kam die schöne Flasche auf den Hankenüll?

Zum Fundort: Was  ist der Hankenüll?

Der Hankenüll ist meines Wissens die höchste Erhebung des Teutoburger Waldes, abgesehen von den Bergen an Anfang und Ende. Laut dem spärlichen Artikel in der Wikipedia ist er 307m hoch. Er wird gekennzeichnet durch einen alten Grenzstein, der im 19. Jhrd. die Grenze zwischen Hannover und Preußen kennzeichnete.

Über den Hankenüll  führt der Hermannsweg, jener Wanderweg, der immer entlang des Kammes des Teutoburger Walds verläuft. In einer schlammigen Stelle dieses Wanderwegs lag die Flasche nicht mehr als 10m vom ‘Gipfel’ entfernt.

Was ist das für eine Flasche?

P1170445 Die Form der Flasche ähnelt eine langgezogenen Variante der Orangina-Flasche. Es ist eine 0.2l Flasche. Am Hals hat sie jedoch eine 10 flächige Abflachung, ähnlich wie ein Schraubansatz. Gibt es eigentlich so etwas wie ein Flaschenformenarchiv oder –museum im Internet?

Leider hat das ‘europaweit einzigartige’ Flaschenmuseum von "Flaschen-Sepp" in der Käserei Kappelimatt offenbar keinen eigenen Internetauftritt. Da ist das Baby Bottle Museum schon viel besser aufgestellt, aber ich bin mir einigermaßen sicher, dass es sich bei meiner Flasche nicht um ein Utensil für Babies handelt.

Da ist die Googlesuche nach ‘Flaschenformen’ schon ergebnisreicher. So findet man viele Seiten, die sich mit Formen von Weinflaschen befassen. Dort kann man z. B. lernen was ein Fiasco ist. Meine Flasche ist allerdings kein Fiasko.

Am Boden der Flasche findet sich ein in Glas geprägter Herstellername:

P1170446

P1170447 

Die ‘Süssmoster Genossenschaft’! Leider scheint sie nicht im Internet vertreten zu sein. Hier findet man nur diverse Telefonbucheinträge der ‘Deutsche Süßmoster-Genossenschaft eG’. Ob die überhaupt etwas mit meiner Flasche zu tun hat?

Bleiben als letzter Hinweis noch die Reste des Etiketts. Es zeigt in einer Art Strahlenkranz ein flaches Römerglas. Darüber ist gerade noch der Schriftzug APFELSAFT in zwei Zeilen zu sehen, wobei ein Apfelblatt aus dem Text entspringt. Gibt es wohl so etwas  wie Etikettenarchive im Internet? Offenbar nicht…

Da sich die Identität der Flasche nicht wirklich feststellen lässt bleiben nur Vermutungen: Ich halte die Flasche für schon etwas älter. Das Etikettendesign wirkt nicht mehr top-aktuell und die Form der Flasche kommt mir auch unbekannt vor.

Wie könnte die Flasche zum Hankenüll gelangt sein?

P1170448

Hier bleibt dann wohl nur noch ganz wilde Spekulation.

Theorie A: Da die Flasche direkt am Hermannsweg lag könnte sie von Wanderern, die keine rechten Naturfreunde waren, einfach weggeworfen worden sein. Das müsste dann wohl vor der Einführung des Flaschenpfands gewesen sein.

Theorie B: Auch am letzten Samstag waren im Wald etliche Leute unterwegs, die ihrer Kettensäge etwas Auslauf gönnten und das Brennholz für die nächsten Winter fertig machten. Da man bei dieser anstrengenden und gefährlichen Arbeit kein Bier trinken kann wäre die zweite Theorie, dass die Flasche ein Überbleibsel von Holzarbeiten im Wald ist. Als Untertheorie erscheint es noch plausibel, dass die Waldarbeiter nicht mit den Waldbesitzern identisch waren, da diese wohl kaum ihren eigenen Wald vermüllt hätten.

Theorie C: Auch bei den Jagden in Herbst und Winter werden die beteiligten Personen durstig. Allerdings erscheint diese Theorie nicht ganz so plausibel, da hier eher Heißgetränke gereicht werden.

Nun, ich werde wohl keine dieser Theorien jemals verifizieren können. Falls mir aber jemand eine plausible Geschichte zu der Flasche präsentieren kann, so werde ich ihm einen Cappuccino in unserem Café ausgeben:)