Ihr Browser versucht gerade eine Seite aus dem sogenannten Internet auszudrucken. Das Internet ist ein weltweites Netzwerk von Computern, das den Menschen ganz neue Möglichkeiten der Kommunikation bietet.

Da Politiker im Regelfall von neuen Dingen nichts verstehen, halten wir es für notwendig, Sie davor zu schätzen. Dies ist im beidseitigen Interesse, da unnötige Angstzustiände bei Ihnen verhindert werden, ebenso wie es uns vor profilierungs- und machtsächtigen Politikern schützt.

Sollten Sie der Meinung sein, dass Sie diese Internetseite dennoch sehen sollten, so können Sie jederzeit durch normalen Gebrauch eines Internetbrowsers darauf zugreifen. Dazu sind aber minimale Computerkenntnisse erforderlich. Sollten Sie diese nicht haben, vergessen Sie einfach dieses Internet und lassen uns in Ruhe.

Die Umgehung dieser Ausdrucksperre ist nach §95a UrhG verboten.

Mehr Informationen unter www.politiker-stopp.de.

0 10. März 2010
0 9. März 2010
Komponentendiagramm 4 5. März 2010
0 22. Februar 2010


Archiv der Kategorie 'Coding'

Identifikation und Authentifikation in RESTful APIs

Mittwoch, den 10. März 2010

Ja, viele von euch (srsly?) haben sicher schon einmal ein (semi) RESTful API entwickelt. Ein haeufiges Problem ist herauszufinden, ob ein Aufruf auch durchgefuehrt werden darf. Das Problem besteht darin, dass wir es dabei mit einem zustandslosen Protokoll (RFC 2616) zu tun haben, also fallen die Elgamal- und Diffie-Hellman-Kryptosysteme aus.

Um nun festzustellen, ob die Person (bzw. eine Applikation, stellvertretend fuer eine Person), die die Anfrage sendet auch wirklich die Person ist, fuer die sie sich ausgibt (Identifikation) und um gleichzeitig festzustellen, ob die Anfrage nicht manipuliert wurde (Authentifikation), gibt es zum einen das freie OAuth Protokoll, was aber relativ kompliziert zu implementieren ist, und zum anderen gibt es aehnliche Ansaetze, wie zum Beispiel das Flickr Auth Protokoll. Beide Protokolle bieten ausserdem die Moeglichkeit externen Programmen (Clients) den Zugriff auf das System (Service Provider) zu authorisieren.

Sofern letzteres nicht noetig ist, reicht jedoch ein abgespecktes Protokoll, das dem Flickr Protokoll sehr nah kommt. Hierbei faellt der Aufwand der sicheren Schluesseluebertragung weg. Und so einfach kann es gehen:

  • Der Service Provider generiert einen oeffentlichen Schluessel (API Key) und einen privaten Schluessel (Secret). Ich habe fuer den oeffentlichen Schluessel die SHA1 Pruefsumme verschiedener Variablen und fuer den privaten Schluessel die MD5 Pruefsumme von Variablen, Zufallszahlen und Timestamps genutzt.
  • Der Client wird mit beiden Keys ausgestattet. Wie gesagt: Der unwiretapped Schluesselaustausch muss anders sichergestellt werden. Bei meinem System kann der Benutzer die Keys einsehen, wenn er sich zuvor mit seinen Zugangsdaten eingeloggt hat. Das ist auch gaengige Praxis.
  • Bei einer Anfragegenerierung werden die Variablen inkl. oeffentlichem Schluessel (die Bezeichner, nicht die Werte!) alphabetisch sortiert und dann mit ihrem Wert konkateniert. Dann werden die Key-Value-Strings konkateniert und der private Schluessel wird vorangestellt. Das Ganze wird jetzt gehasht (e.g. MD5). Nun werden alle Variablen und die zusaetzlich generierte signature-Variable uebertragen.
  • Der Service Provider empfaengt alle Variablen, nimmt davon den oeffentlichen Schluessel und sucht in seiner Datenbank nach dem dazu passenden privaten Schluessel. Jetzt kann der Service Provider das selbe Spiel spielen, das der Client schon durchgemacht hat: Variablen sortieren, Key-Value-Strings bauen, konkatenieren, den privaten Schluessel voranstellen und hashen. Der Vergleich der uebermittelten Signatur und der generierten Signatur erlaubt die Authentifizierung des Zugriffs.

Beispiel

Der API Key ist fd40e3589bbb58ec358122c5cd32fcc6481707bb, das Geheimnis ist 162c9d446beb754b804b904772ff87b6. Die Variablen sind (Bezeichner = Wert):

  • method = deleteEntries
  • rangeFrom = 99
  • rangeTo = 101

Die Anfrage wird nun wie folgt Client-seitig generiert:
Sortieren:

apiKey vor method vor rangeFrom vor rangeTo

Konkatenieren:

apiKeyfd40e3589bbb58ec358122c5cd32fcc6481707bbmethoddeleteEntriesrangeFrom99rangeTo101

Private Key voranstellen:

162c9d446beb754b804b904772ff87b6apiKeyfd40e3589bbb58ec358122c5cd32fcc6481707bbmethoddeleteEntriesrangeFrom99rangeTo101

Hashen:

9ad5100448f0549472b0e70722aa06bb

Variablen fuer die Anfrage zusammensetzen:

http://example.com/rest/?apiKey=fd40e3589bbb58ec358122c5cd32fcc6481707bb&method=deleteEntries&rangeFrom=99&rangeTo=101&signature=9ad5100448f0549472b0e70722aa06bb

Das wird nun ausgefuehrt und auf der Service Provider Seite geht es dann so weiter:
API Key auslesen:

fd40e3589bbb58ec358122c5cd32fcc6481707bb

In der Datenbank nach dem passenden privaten Schluessel suchen:

162c9d446beb754b804b904772ff87b6

Sortieren, Konkatenieren, Private Key voranstellen, Hashen (s.o.):

9ad5100448f0549472b0e70722aa06bb

Ergebnisse vergleichen:

9ad5100448f0549472b0e70722aa06bb = 9ad5100448f0549472b0e70722aa06bb

That’s it. Ist sehr einfach zu implementieren und ist dafuer extrem sicher.

Just Married: Zend Framework & Flickr

Dienstag, den 9. März 2010

Vorweg: Ich glaube dieser Post wird lang…

Also zur Zeit bastel ich an einem Projekt, das aus einigen Social Networks Daten beziehen und diese auf Public Displays anzeigen soll. Da nur-Text etwas oede ist, habe ich mich also an die Flickr API gesetzt. Eigentlich bietet das Zend Framework dafuer die Klasse Zend_Service_Flickr an, aber leider ist die nicht dafuer ausgelegt Applikationen mit Flickr zu verbinden, sondern lediglich fuer eine direkte Benutzerauthentifizierung via Benutzername/Passwort.

Mein erster Ansatz war also erstmal in die API von Flickr zu schauen, denn die bietet ein OAuth aehnliches Verfahren zur Anwendungsauthentifizierung an. Der Trick war nun Zend_Service_Flickr damit zu verheiraten. Und so geht’s:

Als erstes registriert man eine Anwendung bei Flickr und richtet eine Callback URL ein. In meinem Fall wird damit die callbackAction meines FlickrControllers getriggert. Ich brauchte also einen FlickrController…

class FlickrController extends Zend_Controller_Action
{
    public function indexAction() { }
    public function authenticateAction() { }
    public function callbackAction() { }
}

Die Authentifizierung laeuft so ab:

  1. Der Benutzer oeffnet die index-Seite. Dort ist ein Link zur authenticate-Seite, wenn alles nicht schon gelaufen ist.
  2. Die authenticate-Action prueft zunaechst, ob die Anwendung bereits mit Flickr verknuepft ist. Wenn ja, dann wird zur index-Seite weitergeleitet. Wenn nein, dann wird eine Authentication Request URL gebaut und dorthin wird weitergeleitet.
  3. Auf der Flickr-Seite muss der Benutzer der Anwendung nun den Zugriff erlauben. Ist das geschehen, wird der Benutzer von Flickr direkt zur callback-Seite geleitet. Flickr haengt dabei noch eine Variable mit dem Namen frob an.
  4. Die callback-Action sendet daraufhin eine Anfrage an Flickr, die u.A. auch die frob-Variable enthaelt. Ausserdem wird eine Signatur angehangen.
  5. Flickr antwortet mit einem Token, der dann gespeichert wird und fuer zukuenftige Calls verwendet wird.

So, bevor ich alle mit Erklaerungen langweile: Jetzt kommt Code!

class SenScreen_Service_Flickr extends Zend_Service_Flickr
{
    protected $_secret;
    protected $_perms;
    protected $_token;
 
    const PATH_AUTH = '/services/auth/';
    const PATH_REST = '/services/rest/';
 
    public function __construct(array $options = null)
    {
        parent::__construct(null);
 
        $this->setOptions($options);
    }
 
    public function setOptions(array $options = null)
    {
        if ($options == null) {
            return;
        }
 
        $validOptions = array('api_key', 'secret', 'perms', 'token');
 
        $this->_compareOptions($options, $validOptions);
 
        if (isset($options['api_key'])) {
            $this->apiKey = $options['api_key'];
        }
        if (isset($options['secret'])) {
            $this->_secret = $options['secret'];
        }
        if (isset($options['perms'])) {
            $this->_perms = $options['perms'];
        } else {
            $this->_perms = 'read';
        }
        if (isset($options['token']) && $options['token'] instanceof SenScreen_Service_Flickr_AuthToken) {
            $this->_token = $options['token'];
        }
    }
 
    public function setApiKey($apiKey)
    {
        $this->apiKey = $apiKey;
        return $this;
    }
 
    public function getApiKey()
    {
        return $this->apiKey;
    }
 
    public function setSecret($secret)
    {
        $this->_secret = $secret;
        return $this;
    }
 
    public function getSecret($secret)
    {
        return $this->_secret;
    }
 
    public function setPerms($perms)
    {
        $this->_perms = $perms;
        return $this;
    }
 
    public function getPerms()
    {
        return $this->_perms;
    }
 
    public function setToken($token)
    {
        $this->_token = $token;
        return $this;
    }
 
    public function getAuthToken()
    {
        return $this->_token;
    }
 
    public function getApiSignature(array $params)
    {
        $plainSignature = $this->_secret;
        ksort($params);
        foreach ($params as $key => $value) {
            $plainSignature .= $key . $value;
        }
        return md5($plainSignature);
    }
 
    private function _getSignedUrl($path, array $params)
    {
        $url = self::URI_BASE . $path . '?';
        $urlParams = array();
        foreach ($params as $key => $value) {
            $urlParams[] = $key . '=' . $value;
        }
        $url .= implode('&', $urlParams);
        $url .= '&api_sig=' . $this->getApiSignature($params);
        return $url;
    }
 
    public function redirect()
    {
        $params = array(
            'api_key' => $this->apiKey,
            'perms'   => $this->_perms
        );
        $redirectUrl = $this->_getSignedUrl(self::PATH_AUTH, $params);
        header('Location: ' . $redirectUrl);
        exit(1);
    }
 
    public function getFrob()
    {
        return $_GET['frob'];
    }
 
    public function requestToken()
    {
        $params = array(
            'method'  => 'flickr.auth.getToken',
            'api_key' => $this->apiKey,
            'frob'      => $this->getFrob(),
            'perms'      => $this->_perms
        );
 
        $httpClient = new Zend_Http_Client($this->_getSignedUrl(self::PATH_REST, $params));
        $httpClient->resetParameters();
        $httpClient->setParameterGet($params);
        $httpClient->setHeaders(array(
            'Accept-encoding' => ''
        ));
        return $this->_parseToken($httpClient->request('GET'));
    }
 
    private function _parseToken(Zend_Http_Response $response)
    {
        $xmlElement = new SimpleXMLElement($response->getBody());
        return new SenScreen_Service_Flickr_AuthToken(
            (string) $xmlElement->auth->token,
            (string) $xmlElement->auth->user['username'],
            (string) $xmlElement->auth->user['fullname']
        );
    }
 
    public function isRestrictedMethod($method)
    {
        $restrictedMethods = array(
            'flickr.photos.getContactsPhotos'
        );
 
        return in_array($method, $restrictedMethods);
    }
 
    private function _execute($method, $defaultOptions, $options)
    {
        $options = $this->_prepareOptions($method, $options, $defaultOptions);
 
        $restClient = $this->getRestClient();
        $restClient->getHttpClient()->resetParameters();
        $response = $restClient->restGet('/services/rest/', $options);
 
        if ($response->isError()) {
            throw new Zend_Service_Exception('An error occurred sending request. Status code: '
                                           . $response->getStatus());
        }
 
        $dom = new DOMDocument();
        $dom->loadXML($response->getBody());
 
        self::_checkErrors($dom);
 
        return new Zend_Service_Flickr_ResultSet($dom, $this);
    }
 
    public function getContactsPhotos(array $options = array())
    {
        static $method = 'flickr.photos.getContactsPhotos';
        static $defaultOptions = array(
            'count'           => 100,
            'just_friends' => 0,
            'single_photo' => 0,
            'include_self' => 0,
            'extras'       => 'license, date_upload, date_taken, owner_name, icon_server, original_format, last_update.'
        );
        return $this->_execute($method, $defaultOptions, $options);
    }
 
    public function getRecent()
    {
        static $method = 'flickr.photos.getRecent';
        static $defaultOptions = array(
            'per_page' => 10,
            'page'     => 1,
            'extras'   => 'license, date_upload, date_taken, owner_name, icon_server, original_format, last_update.'
        );
        return $this->_execute($method, $defaultOptions);
    }
}

Diese Klasse erweitert Zend_Service_Flickr, sodass diese auch mit Authentifizierung umgehen kann. Als naechstes muss der Zend_Http_Client ausgetauscht (bzw. erweitert) werden, sodass auch er mit Signaturen und dem Kram umgehen kann:

class SenScreen_Service_Flickr_Http_Client extends Zend_Http_Client
{
    protected $_flickr;
 
    public function __construct()
    {
        parent::__construct();
 
        $this->setHeaders(array('Accept-Encoding' => ''));
    }
 
    public function setFlickr(SenScreen_Service_Flickr $flickr)
    {
        $this->_flickr = $flickr;
    }
 
    public function request($method = null)
    {
        if ($this->_flickr->isRestrictedMethod($this->paramsGet['method'])) {
            $this->paramsGet['auth_token'] = $this->_flickr->getAuthToken()->getToken();
 
            $this->paramsGet = array_merge($this->paramsGet, array(
                'api_sig' => $this->_flickr->getApiSignature($this->paramsGet)
            ));
        }
        return parent::request($method);
    }
}

Als naechstes Brauchen wir noch eine Klasse, die unser Token aufbewahrt und serialisierbar in der DB ablegbar ist:

class SenScreen_Service_Flickr_AuthToken
{
    private $_token;
    private $_username;
    private $_fullname;
 
    public function __construct($token, $username, $fullname)
    {
        $this->_token    = $token;
        $this->_username = $username;
        $this->_fullname = $fullname;
    }
 
    public function getToken()
    {
        return $this->_token;
    }
 
    public function getUsername()
    {
        return $this->_username;
    }
 
    public function getFullname()
    {
        return $this->_fullname;
    }
}

Und dann koennen wir endlich den Controller fuellen und jetzt kommen auch mal Kommentare in den Code, damit ich nicht mehr so viel quarken muss:

class FlickrController extends Zend_Controller_Action
{
    protected $_config = array(
        'api_key' => 'myapplicationkey',
        'secret'  => 'mysecret'
    );
 
    protected $_flickr;
 
    public function init()
    {
        $this->_flickr = new SenScreen_Service_Flickr();
        $httpClient = new SenScreen_Service_Flickr_Http_Client();
        $httpClient->setFlickr($this->_flickr);
        Zend_Rest_Client::setHttpClient($httpClient);
    }
 
    public function indexAction()
    {
        $userId = Zend_Auth::getInstance()->getIdentity()->id;
        /**
         * Check whether the current user has an access
         * token for Flickr.
         */
        $userSettingModel = new Model_UserSetting();
        if ($userSettingModel->get($userId, 'flickr_token')) {
            /**
             * Get the access token.
             */
            $access_token = unserialize($userSettingModel->getValue());
 
            /**
             * Configure the Flickr instance.
             */
            $this->_flickr->setOptions(array_merge(
                $this->_config,
                array('token' => $access_token)
            ));
 
            $this->view->contactPhotos = $this->_flickr->getContactsPhotos(array('include_self' => 1, 'extras' => ''));
 
            $this->view->showAuthenticationButton = false;
        } else {
            $this->view->showAuthenticationButton = true;
        }
    }
 
    public function authenticateAction()
    {
        $userId = Zend_Auth::getInstance()->getIdentity()->id;
 
        /**
         * Check whether the current user has an access
         * token for Flickr.
         */
        $userSettingModel = new Model_UserSetting();
        if ($userSettingModel->get($userId, 'flickr_token')) {
            /**
             * Get the access token.
             */
            $this->_helper->redirector('index');
        } else {
            /**
             * The user is not authenticated with Flickr.
             * Request Flickr auth frob and redirect to
             * Flickr's authentication page.
             */
            $this->_flickr->setOptions($this->_config);
            $this->_flickr->redirect();
        }
    }
 
    public function callbackAction()
    {
        $userId = Zend_Auth::getInstance()->getIdentity()->id;
 
        $this->_flickr->setOptions($this->_config);
 
        /**
         * Check whether a frob is present for this user.
         */
        if ($this->_flickr->getFrob()) {
            /**
             * A frob is present. Request a token and
             * save it in the database.
             */
            $token = $this->_flickr->requestToken();
            $userSettingModel = new Model_UserSetting();
            $userSettingModel->setUserId($userId)
                             ->setKey('flickr_token')
                             ->setValue(serialize($token))
                             ->save();
 
            /**
             * Everything is done, redirect to the index
             * page.
             */
            $this->_helper->redirector('index', 'flickr');
        } else {
            /**
             * The callback has been called without having
             * a request token. Redirect to the index page.
             */
            $this->_helper->redirector('index', 'flickr');
        }
    }
}

Sollte soweit selbsterklaerend sein. Viel Spass damit. ;-)

Update:
Wie ihr vielleicht bemerkt habt, nutze ich das Abstract Data Mapper Design Pattern.

Abstract Data Mapper Pattern

Freitag, den 5. März 2010

Manchmal ist es frustrierend, wenn man mit dem Zend Framework arbeitet. Es kann zwar so ziemlich alles, aber die Dokumentation ist nicht besonders zu empfehlen. Darin wird zwar jedes leidige Thema angesprochen, jedoch nie in einem globaleren Kontext. Beispiel: PHP kann mit einer speziellen __autoload Funktion Klassen dynamisch aus Dateien laden, sofern sie benoetigt werden. Darauf basierend arbeiten Autoloader von Zend, die es verschiedene Aufgabenbereiche gibt. Die Dokumentation sagt nun, wie man Autoloader erzeugt. Es fehlen aber einige wichtige Informationen: Wo wird das gemacht, wieso wird das gemacht, welchen Nebeneffekte hat das und wie sieht ein Standard-Use Case aus? Es gibt natuerlich eine Begruendung dafuer, wieso die Dokumentation so anscheinend wichtige Dinge weglaesst: Das Zend Framework ist so konzipiert, dass man es auf beliebige Weise anwenden kann: Ob es als Model-View-Controller Framework oder fuer noch komplexeren Spaghetticode genutzt wird ist so ziemlich egal und diese Flexibilitaet ist durchaus erwuenscht. Best Practice sieht aber anders aus.

Best Practice

Zur Best Practise hat sich mittlerweile die auch von Zend Studio automatisch generierbare Model-View-Controller-Struktur gemausert. Dazu wird zunaechst eine Verzeichnisstruktur in folgender oder aehnlicher Weise mit einigen Standarddateien genutzt:

application
    configs
        application.ini
    controller
        ErrorController.php
        IndexController.php
    models
    views
        scripts
            error
                error.phtml
            index
                index.phtml
    Bootstrap.php
public
    index.php

Hierbei wird der Webserver so konfiguriert, dass die Domain auf das public-Verzeichnis zeigt. Dadurch wird vor allem die Sicherheit der Applikation erhoeht, da der Zugriff auf die einzelnen Dateien nicht mehr moeglich ist. Die Datei application.ini enthaelt die wichtigsten Informationen, die fuer das Deployment benoetigt werden. Das controller-Verzeichnis enthaelt alle Controller und das models-Verzeichnis enthaelt analog die Modellabstraktionen. Jeder Controller besitzt mindestens eine Action in Form einer Methode, mit der Namenskonvention actionnameAction. Zu jedem Controller gibt es im views-Unterverzeichnis scripts ein Verzeichnis, in dem fuer jede Action eine Datei mit der Namenskonvention actionname.phtml vorliegt. Diese Dateien definieren die an den Client zurueckzusendenen Darstellungsinformationen.

Soweit so gut. Wenn Projekte wachsen wird man irgendwann anfangen das Gesamtsystem in kleine Haeppchen zu zerhacken. Divide and Conquer – Teile und herrsche. Wir (angehenden) Software Systems Engineers nennen sowas auch gerne Modularisierung. Das Zend Framework ist fuer diesen Fall natuerlich auch gewappnet und ermoeglicht es Module zu definieren, wobei jedes Modul eine Verzeichnisstruktur aufweist, die identisch zum application-Verzeichnis ist. Es muessen natuerlich nicht alle Verzeichnisse existieren, aber Minimum ist in den meisten Faellen controller, models und views. Fuer jedes Modul gibt es ein Verzeichnis im Verzeichnis modules und das ganze sieht dann in etwa so aus:

application
    configs
        application.ini
    modules
        default
            controller
                ErrorController.php
                IndexController.php
            models
            views
                scripts
                    error
                        error.phtml
                    index
                        index.phtml
            Bootstrap.php
        foobarmodule
            controller
                ErrorController.php
                IndexController.php
            models
            views
                scripts
                    error
                        error.phtml
                    index
                        index.phtml
            Bootstrap.php
    Bootstrap.php
public
    index.php

Hierbei hat default eine spezielle Rolle, denn es wird von Zend als Standard verwendet, sofern kein anderes Modul explizit angegeben wurde.

Models

Wie oben bereits mehrfach erwaehnt kann jedes Modul Models besitzen. Und hierbei gehen die Geister auseinander. Lange Zeit wurde im Quick Start Tutorial vom Zend Framework darauf gepocht, dass man die Klasse Zend_Db_Table_Abstract erweitert. Diese abstrakte Klasse ermoeglicht es sehr einfach eine Datenbanktabellenabstraktion zu erstellen, indem man einfach von ihr erbt und lediglich den Tabellennamen angibt, auf der operiert werden soll. Da selbst das einigen Menschen wohl zu viel Schreibarbeit war, wurde in frueheren Versionen des Zend Frameworks Inflection benutzt (hier steht ein bisschen dazu und hier ganz unten auch). Das funktioniert wie folgt: Man erweitert Zend_Db_Table (nicht die abstrakte Klasse!) und benennt sie nach einem bestimmten Schema (meist Default_Model_DatenbankTabellenName, je nach Autoloader-Konfiguration mit oder ohne Default_-Prefix). Der Name der Klasse wird dann zum Namen der zugehoerigen Tabelle transformiert (hier waere es datenbank_tabellen_name).

Ich sehe in diesem Ansatz jedoch einen Widerspruch zum Begriff Datenbankabstraktion. Man abstrahiert, um vor allem Komplexitaet zu verringern. Zusaetzlich – und nicht zu vernachlaessigen – abstrahiert man aber auch, um eine Entkopplung von tieferliegender Implementierung zu hoeheren Komponenten zu erreichen. Stellt man nun eine direkte Abhaengigkeit zwischen dem Klassennamen und den Tabellennamen her, so ist man gezwungen jede Namensaenderung der Datenbanktabelle auf alle Verwendungen des Models anzuwenden. Epic fail!

Ich habe bereits gesagt, dass dies so alles mal in der offiziellen Dokumentation vom Zend Framework stand bzw. noch steht. Dem gewiefte Leser mag nun aufgefallen, dass ich im vorherigen Absatz erwaehnt habe, dass jede Verwendung des Models anzupassen waere, wenn Inflection genutzt wird und sich der Tabellenname in der Datenbank aendert. Wenn es jedoch nur eine einzige Verwendung gibt: Who cares? Aber hier geht es weiter: Die Dokumentation geht an vielen Stellen davon aus, dass man direkt auf Instanzen von Zend_Db_Table oder Instanzen von Unterklassen von Zend_Db_Table_Abstract arbeitet. Das hat natuerlich zur Folge, dass lose Kopplung zum Fremdwort wird.

Is-A vs. Has-A

Plump unterscheidet man haeufig (unter anderem) zwischen einer Ist- (Is-A) und einer Hat-Beziehung (Has-A) zwischen Entitaeten. Was hat das nun mit Models zu tun? Die Weiterfuehrung des Gedankens nicht direkt auf Datenbankabstraktionen zu arbeiten hat zur Folge, dass man Datenbankabstraktionen zu Modelabstraktionen abstrahiert. Beispiel: Es gibt eine Benutzerverwaltungssystem, also gibt es eine Tabelle von Benutzern in der Datenbank (users). Dafuer gibt es nun eine Tabellenabstraktion. Diese Klasse heisse nun Default_Model_Users. Wenn wir nun fuer einzelne Datensaetze, also fuer einzelne Benutzer auch eine Abstraktion verwenden, so gibt es die Wahl zwischen den beiden Beziehungsarten. Fuer eine Ist-Beziehung haetten wir in diesem Beispiel eine Klasse Default_Model_User und wir wuerden Instanzen davon erhalten, wenn wir ueber Default_Model_Users (mit Oberklasse Zend_Db_Table_Row_Abstract) bestimmte Datensaetze auswaehlen. Da man nun die Datenbank wegabstrahieren moechte hat man zunaechst Gateways eingefuehrt.

Gateways besitzen eine Instanz von Zend_Db_Table (bzw. eine Instanz einer Unterklasse von Zend_Db_Table_Abstract), auf der Operationen ausgefuehrt werden, um einzelne Datensaetze zu erhalten. Diese Datensaetze sollen nun nicht in der Form einer Unterklasse von Zend_Db_Table_Row (bzw. Zend_Db_Table_Row_Abstract) vorliegen, da wir ja die Datenbank verschleiern moechten. Haeufige Herangehensweise dabei ist es, fuer jedes Feld einer Tabelle ein Feld in einem Objekt zu generieren. Wir sind endlich bei der Hat-Beziehung angelangt.

Mehr Informationen zu den Beziehungsarten in diesem Kontext (viel besser erklaert, als ich es in aller Kuerze machen koennte) findet ihr bei Rob Allen’s Dev Notes: On models in a Zend Framework application.

Service Layer

Hab ich schonmal erwaehnt, dass ich Zwiebeln jeder Art sehr gerne esse? Nicht? Ist auch nicht wichtig. Wichtig ist jedoch, dass wir es in der Software Entwicklung haeufig mit Zwiebeln zu tun haben. Heisst soviel wie: Wir bauen etwas um etwas anderes herum, um u.A. (1) ein gewuenschtes Abstraktionsniveau zu erhalten, (2) lose gekoppelte Systeme schreiben zu koennen und (3) damit wir auf Aenderungen besser reagieren koennen. Es gibt natuerlich auch die Gefahr, dass diese Zwiebelmethode angewendet wird, weil das Wissen ueber die Komponente nicht ausreichend ist oder weil man mit der Komponente nicht zufrieden ist. Dann wird das ganze schnell zu dem Anti-Pattern, das natuerlich auch Onion heisst.

Wenn wir mit Persistenzschichten arbeiten, wollen wir haeufig eine Schnittstelle fuer verschiedene Persistenzsysteme. Wir wollen also zum Beispiel ohne grossen Aufwand ein auf CSV oder XML basiertes Persistenzsystem auf eine Datenbank umaendern. Das ist einfach, wenn wir in unserer Business Logic auf Repraesentationen von den eigentlich betrachteten Objekten arbeiten und nicht direkt auf den Persistenzsystemen. Gateways ermoeglichen das bereits im Ansatz, jedoch besteht noch immer eine direkte Verbindung zwischen den Feldern eine Tabelle in einer Datenbank und den Attributen eines Business Logic Objects.

Und jetzt kommt der Clue: Statt Bottom-Up zunaechst an die Implementierung der Datenbankanbindung zu denken, besinnen wir uns zunaechst daran, dass wir ja auf bestimmten fiktiven oder realen Objekten wie z.B. einem Benutzer arbeiten moechten. Top-Down: Wir definieren uns als erstes die Objekte als Schnittstellen, um zu gewaehrleisen, dass alle Operationen existieren, die wir benoetigen. Da wir Attribute nicht in Schnittstellen definieren koennen, nutzen wir Getter und Setter. Alternativ koennen wir auch mit abstrakten Klassen arbeiten, dann ist es auch moeglich Attribute vorzugeben, es ist jedoch schwieriger sicherzustellen, dass die Attribute auch allen Vorbedingungen, Nachbedingungen und Invarianten des Systems folgen bzw. ueberhaupt genutzt werden. Vorteil von abstrakten Klassen ist jedoch, dass wir Standardimplementierungen vorgeben koennen.

Nachdem wir alle Objekte zumindest strukturell spezifiziert haben, koennen wir nun beginnen sie zu implementieren. Da ich jetzt schon viel zu viel geschrieben habe ohne ein Beispiel zu nennen kommt erstmal eins, bei dem ich mich auf die grundlegenden Dinge beschraenke:

class Default_Model_User
{
    protected $_id;
    protected $_name;
 
    public function getId()
    {
        return $this->_id;
    }
 
    public function setId($id)
    {
        $this->_id = (int) $id;
        return $this;
    }
 
    public function getName()
    {
        return $this->_name;
    }
 
    public function setName($name)
    {
        $this->_name = (string) $name;
        return $this;
    }
}

Weiter geht’s: Jetzt wo wir alle Schnittstellen definiert haben, auf denen wir unabhaengig von der Persistenzschicht arbeiten moechten, faellt auf: Wir haben einen Schicht gebaut, die uns zugleich als Service dienst, daher auch Service Layer genannt.

Komponentendiagramm

Data Mapper Pattern

Top Down: Vom Model zu den Daten, oder: Von Modelspezifikation zur Anbindung an die Persistenzschicht. Es ist nun moeglich auf Repraesentationen von unseren Business Logic Objects zu arbeiten. Allerdings kommen die Daten bisher anscheinend aus dem Nirvana. Die Idee ist nun, einen Mapper zu konstruieren, der die Daten auf das Model projizieren kann und der das Model auf die Daten projizieren kann. Wenn wir nun unser echtes Objekt, mit der Datenentsprechung und einem Mapper als Strukturmuster definieren, dann erhalten wir das Data Mapper Pattern, das dank Matthew Weier O’Phinney nun auch im Quick Start Tutorial von Zend verwendet wird (er hat da laut dieser Quelle Diskussionen mit dem Autor des Tutorials gefuehrt).

Wenn ihr mehr Informationen zum Data Mapper Pattern haben moechtet, schaut auch mal das Webinar Play-Doh: Modelling Your Objects von Matthew Weier O’Phinney an oder lest euch die Slides dazu durch.

Ich bau mal an dem Beispiel weiter und hoffentlich wird dann auch klar, wie der Mapper funktioniert:

class Default_Model_DbTable_User extends Zend_Db_Table_Abstract
{
    protected $_name = 'users';
}
 
class Default_Model_Mapper_User
{
    protected $_dbTable;
 
    public function setDbTable($dbTable)
    {
        if (is_string($dbTable)) {
            $dbTable = new $dbTable();
        }
        if (!$dbTable instanceof Zend_Db_Table_Abstract) {
            throw new Exception('Invalid table data gateway provided');
        }
        $this->_dbTable = $dbTable;
        return $this;
    }
 
    public function getDbTable()
    {
        if (null === $this->_dbTable) {
            $this->setDbTable('Default_Model_DbTable_User');
        }
        return $this->_dbTable;
    }
 
    public function save(Default_Model_User $user)
    {
        $data = array(
            'name' => $user->getName()
        );
 
        if (null === ($id = $user->getId())) {
            $this->getDbTable()->insert($data);
        } else {
            $this->getDbTable()->update($data, array('id = ?' => $id));
        }
    }
 
    public function delete($where)
    {
        return $this->getDbTable()->delete($where);
    }
 
    public function find($id, $user)
    {
        $result = $this->getDbTable()->find($id);
        if (0 == count($result)) {
            return;
        }
        $row = $result->current();
        $user->setId($row->id)
             ->setName($row->name);
    }
 
    public function fetchAll()
    {
        $resultSet = $this->getDbTable()->fetchAll();
        $entries   = array();
        foreach ($resultSet as $row) {
            $entry = new Default_Model_User();
            $entry->setId($row->id)
                  ->setName($row->name)
                  ->setMapper($this);
            $entries[] = $entry;
        }
        return $entries;
    }
 
    public function getAdapter()
    {
        return $this->getDbTable()->getAdapter();
    }
}

Und am User muessen wir auch noch etwas aendern, sodass es wie folgt aussieht:

class Default_Model_User
{
    protected $_id;
    protected $_name;
    protected $_mapper;
 
    public function __construct(array $options = null)
    {
        if (is_array($options)) {
            $this->setOptions($options);
        }
    }
 
    public function __set($name, $value)
    {
        $method = 'set' . $name;
        if (('mapper' == $name) || !method_exists($this, $method)) {
            throw new Exception('Invalid model property');
        }
        $this->$method($value);
    }
 
    public function __get($name)
    {
        $method = 'get' . $name;
        if (('mapper' == $name) || !method_exists($this, $method)) {
            throw new Exception('Invalid model property');
        }
        return $this->$method();
    }
 
    public function setOptions(array $options)
    {
        $methods = get_class_methods($this);
        foreach ($options as $key => $value) {
            $method = 'set' . ucfirst($key);
            if (in_array($method, $methods)) {
                $this->$method($value);
            }
        }
        return $this;
    }
 
    public function setMapper($mapper)
    {
        if (is_string($mapper)) {
            $mapper = new $mapper();
        }
        if (!$mapper instanceof Default_Model_Mapper_Abstract) {
            throw new Exception('Invalid data mapper provided');
        }
        $this->_mapper = $mapper;
        return $this;
    }
 
    public function getMapper()
    {
        if (null === $this->_mapper) {
            $this->setMapper($this->_mapperClass);
        }
        return $this->_mapper;
    }
 
    public function getAdapter()
    {
        return $this->getMapper()->getAdapter();
    }
 
    public function save()
    {
        return $this->getMapper()->save($this);
    }
 
    public function delete($where)
    {
        return $this->getMapper()->delete($where);
    }
 
    public function find($id)
    {
        $this->getMapper()->find($id, $this);
        return $this;
    }
 
    public function fetchAll()
    {
        return $this->getMapper()->fetchAll();
    }
 
    public function getId()
    {
        return $this->_id;
    }
 
    public function setId($id)
    {
        $this->_id = (int) $id;
        return $this;
    }
 
    public function getName()
    {
        return $this->_name;
    }
 
    public function setName($name)
    {
        $this->_name = (string) $name;
        return $this;
    }
}

Endlich! Wir haben das Data Mapper Pattern komplett. Hier mal ein Usage Example:

/* somewhere in your controller */
 
$user = new Default_Model_User();
$users = $user->fetchAll();
 
foreach ($users as $u) {
    if ($u->getName() == 'unknown') {
        $u->setName('unbekannter Nutzername')
          ->save();
    }
}

Pretty easy, huh? Aber stopp! Im Titel steht doch noch irgendwas von Abstract. Was soll denn der Quark? …

Reusability: Abstract Data Mapper Pattern

Wenn wir das Data Mapper Pattern jetzt mehrfach verwenden, stellen wir sehr sehr schnell fest, dass wir unglaublich viel Code doppelt und dreifach haben. Jetzt kommt mein Ansatz: Wir packen alles bekannte in abstrakte Oberklassen und erben davon, setzen die benoetigten Meta-Informationen fuer das Pattern und sind fertig. Ohne grosse Umschweife (just kidding, das war ja jetzt wohl genug Umschweife):

class Default_Model_DbTable_User extends Zend_Db_Table_Abstract
{
    protected $_name = 'users';
}
 
abstract class Default_Model_Abstract
{
    protected $_mapper;
    protected $_mapperClass;
 
    public function __construct(array $options = null)
    {
        if (is_array($options)) {
            $this->setOptions($options);
        }
    }
 
    public function __set($name, $value)
    {
        $method = 'set' . $name;
        if (('mapper' == $name) || !method_exists($this, $method)) {
            throw new Exception('Invalid model property');
        }
        $this->$method($value);
    }
 
    public function __get($name)
    {
        $method = 'get' . $name;
        if (('mapper' == $name) || !method_exists($this, $method)) {
            throw new Exception('Invalid model property');
        }
        return $this->$method();
    }
 
    public function __call($method, array $arguments)
    {
        if (!method_exists($this->_mapper, $method)) {
            throw new Exception('Inaccessible method called');
        }
        return call_user_func_array(array($this->_mapper, $method), $arguments);
    }
 
    public function setOptions(array $options)
    {
        $methods = get_class_methods($this);
        foreach ($options as $key => $value) {
            $method = 'set' . ucfirst($key);
            if (in_array($method, $methods)) {
                $this->$method($value);
            }
        }
        return $this;
    }
 
    public function setMapper($mapper)
    {
        if (is_string($mapper)) {
            $mapper = new $mapper();
        }
        if (!$mapper instanceof Default_Model_Mapper_Abstract) {
            throw new Exception('Invalid data mapper provided');
        }
        $this->_mapper = $mapper;
        return $this;
    }
 
    public function getMapper()
    {
        if (null === $this->_mapper) {
            $this->setMapper($this->_mapperClass);
        }
        return $this->_mapper;
    }
 
    public function getAdapter()
    {
        return $this->getMapper()->getAdapter();
    }
 
    public function save()
    {
        return $this->getMapper()->save($this);
    }
 
    public function delete($where)
    {
        return $this->getMapper()->delete($where);
    }
 
    public function find($id)
    {
        $this->getMapper()->find($id, $this);
        return $this;
    }
 
    public function fetchAll()
    {
        return $this->getMapper()->fetchAll();
    }
}
 
class Default_Model_User extends Default_Model_Abstract
{
    protected $_mapperClass = 'Default_Model_Mapper_User';
 
    protected $_id;
    protected $_name;
 
    public function getId()
    {
        return $this->_id;
    }
 
    public function setId($id)
    {
        $this->_id = (int) $id;
        return $this;
    }
 
    public function getName()
    {
        return $this->_name;
    }
 
    public function setName($name)
    {
        $this->_name = (string) $name;
        return $this;
    }
}
 
abstract class Default_Model_Mapper_Abstract
{
    protected $_dbTable;
    protected $_dbTableClass;
 
    public function setDbTable($dbTable)
    {
        if (is_string($dbTable)) {
            $dbTable = new $dbTable();
        }
        if (!$dbTable instanceof Zend_Db_Table_Abstract) {
            throw new Exception('Invalid table data gateway provided');
        }
        $this->_dbTable = $dbTable;
        return $this;
    }
 
    public function getDbTable()
    {
        if (null === $this->_dbTable) {
            $this->setDbTable($this->_dbTableClass);
        }
        return $this->_dbTable;
    }
 
    public function getAdapter()
    {
        return $this->getDbTable()->getAdapter();
    }
 
    abstract public function save($entry);
 
    abstract public function delete($where);
 
    abstract function find($id, $entry);
 
    abstract public function fetchAll();
}
 
class Default_Model_Mapper_User extends Default_Model_Mapper_Abstract
{
    protected $_dbTableClass = 'Default_Model_DbTable_User';
 
    public function save($user)
    {
        $data = array(
            'name' => $user->getUsername()
        );
 
        if (null === ($id = $user->getId())) {
            return $this->getDbTable()->insert($data);
        } else {
            $this->getDbTable()->update($data, array('id = ?' => $id));
            return $id;
        }
    }
 
    public function delete($where)
    {
        return $this->getDbTable()->delete($where);
    }
 
    function find($id, $user)
    {
        $result = $this->getDbTable()->find($id);
        if (0 == count($result)) {
            return false;
        }
        $row = $result->current();
        $user->setId($row->id)
             ->setName($row->name);
       return true;
    }
 
    public function fetchAll()
    {
        $resultSet = $this->getDbTable()->fetchAll();
        $entries   = array();
        foreach ($resultSet as $row) {
            $entry = new Default_Model_User();
            $entry->setId($row->id)
                  ->setName($row->name)
                  ->setMapper($this);
            $entries[] = $entry;
        }
        return $entries;
    }
}

Das Pattern nenn ich jetzt einfach mal wie oben genannt und schenke es der Welt.

Update

Ich habe mal ein Demo-Projekt fuer Zend Studio zusammengebaut. In etwa so sollte es aussehen (Modulstruktur und kleine Anpassungen inklusive): AbstractDataMapperPatternDemo-1.2.zip.

Impact

Montag, den 22. Februar 2010

Wenn man ueber etwas nachdenkt, dann ist es schwierig solange man sich mittendrin befindet. Wenn man jedoch am Ende steht, scheint es fuer Menschen wesentlich einfacher zu sein Dinge zu reflektieren. Anscheinend laesst sich das auch auf das Leben anwenden. Den Beweis fuer diese These hat Prof. Randy Pausch (* 23. Oktober 1960; † 25. Juli 2008) erbracht.

Pausch war Professor an der Carnegie Mellon University in Pittsburgh, Pennsylvania. Sein Forschungsgebiet war die Human-Computer Interaction (HCI), vor allem aber Virtual Reality. Er begruendete u.A. das Alice-Project, bei dem eine offensichtlich funktionierende Idee einfach einmal realisiert wurde: Wenn man Menschen dazu bringt Spass zu haben, dann fokussieren sie ihr Interesse darauf und beginnen dabei Dinge zu lernen, die keine offensichtlichen Beziehungen zum eigentlichen Geschehen haben. Das Alice-Project erleichtert es auf spielerische Weise Programmiersprachen zu lernen und wird (Stand 2008) von 10% der Studierenden von IT-Bereichen in den USA genutzt.

Bei Pausch wurde im September 2006 Bauchspeicheldruesenkrebs diagnostiziert. Er kaempfte mit vielen Behandlungen dagegen an, starb aber im Juli 2008. Zehn Monate vor seinem Tod hielt Randy Pausch seine Last Lecture. Eine letzte Vorlesung zu halten ist Brauch an vielen Universitaeten. Als die CMU allerdings zu seiner letzten Vorlesung lud, war bereits klar, dass der Begriff woertlich zu nehmen ist.

Seine offiziell letzte Vorlesung nutzte Pausch, um auf brilliante Art ueber sein Leben zu sprechen. Er stellte die Vorlesung unter einen Titel (Achieving Your Childhood Dreams), der durchdachter kaum sein koennte. Er nannte seine Kindheitstraeume und wie er sie erreicht hat bzw. wieso es gut ist, dass er auch manche Kindheitstraeume nicht erreicht hat. Er erklaerte, wie er anderen Menschen dabei geholfen hat ihre Kindheitstraeume zu erfuellen und was er aus Allem gelernt hat.

Um die Komplexitaet des Inhalts und der Botschaft seiner Last Lecture zu verstehen reicht es nicht aus darueber zu schreiben. Man muss sie sehen. Es lohnt sich und hat mich sehr beeindruckt.

Das Video der letzten Vorlesung:

(Entweder JavaScript ist nicht aktiviert, oder Sie benutzen eine alte Version von Adobe Flash Player. Installieren Sie bitte den aktuellsten Flash Player. )

Der lange Weg zum NAS

Donnerstag, den 21. Januar 2010

Wie ich waehrend des 26C3 festgestellt habe, mag ich Netzwerk Storages. Ich habe also bei eBay herumgeschaut und musste wiederum feststellen, dass ich mir eine richtige Kiste nicht leisten kann. Also habe ich mich prompt dazu entschieden, mir ein NAS zu improvisieren. Die Zutaten:

  • ein Thin Client
  • ein paar USB Sticks
  • ein paar externe Festplatten
  • Linux

Ich habe mir also bei eBay zwei (just in case…) Thin Clients (FSC Futro S300 und HP Thin Client t5510) fuer insgesamt rund 70 EUR gekauft. Da der HP schoener aussieht und als erstes geliefert wurde, habe ich ihn auserkoren zum NAS zu werden.

Der naive Versuch

Da der HP (und auch der FSC) kein CD Laufwerk hat, bleibt nur USB. Das war mir ja klar, also habe ich auch direkt ein paar (2x 4GB, 10x 2GB, weil die so guenstig waren) USB Sticks gekauft und den Ubuntu Installer auf einen der 4GB Sticks geknallt. Da der Installer aber nach 30 Minuten noch immer nicht komplett geladen war, habe ich das mit dem Ubuntu ad acta gelegt.

Kleine Pueppchen

Nun musste also eine Ersatzdistro her, die am Besten auf Debian basiert (Debian ist sexy). Nach ein paar brutalen Stunden, die ich mit Google verbracht hab, war ich von dem Linux Terminal Server Project etwas enttaeuscht. Dann bin ich endlich auf Puppy Linux gestossen. Die knapp 100MB grosse Linux Distribution habe ich dann gepflegt in den Downloadmanager geladen und froehlich 1,5 Stunden(!!!) gewartet. Tolle Mirrors….

Aber endlich war es dann soweit. USB Stick mit Puppy Linux beladen und gib ihm… Haettstewohlgern! Die Fehlermeldung beim Booten haette genauso gut auch EPIC FAIL heissen koennen. Blieb mir nur eins:

dd if=/dev/null of=/dev/sda

Verdammt klein

… soll ja Damn Small Linux sein. Da es auch auf Debian basiert, war es meine naechste Wahl. Saugen,  auf den Stick knallen, booten. Endlich! Innerhalb von Sekunden(!!!) startete Fluxbox. Coole Sache. Das hat mich an meine (spaete, da ich anfaenglich nur mit CLI gearbeitet habe) OpenBSD Zeit erinnert. Da gab es tatsaechlich auch die Moeglichkeit ein Pendrive Linux zu installieren. Also steckte ich einen weiteren USB Stick in den Thin Client, der zum Glueck 4 USB Slots hat, und startete sogleich auch den Pendrive Installer. EPIC FAIL. Abgeschmiert. Zu wenig RAM… Damn Small aber dennoch Damn Memory-leaky. Diesmal habe ich mir dann mit…

shred -n 10 -v /dev/sda

…geholfen. *facepalm*1

Verzweiflung

Zu dem Zeitpunkt war ich doch schon sehr verzweifelt, da ich schon mehrere Tage (da ich ja immer erst spaet von der Uni komme) daran gehockt habe und noch zu keinem Ergebnis gelangt bin. Unter Windows lief mal wieder mein VMware-Ubuntu im Unity-Mode (sexy, wenn auch von Parallels abgeguckt). Da Ubuntu von Debian abstammt, kommt man auch sehr schnell auf die Idee debootstrap anzuschmeissen und sich sein Debian manuell zusammenzubauen. Gesagt, getan: Nach ein paar Stunden Frickelei (USB+ext3 = keine gute Idee, USB+vfat+Linux = noch schlechtere Idee, USB+Grub = beschissene Idee) habe ich es dann auch geschafft mir ein Linux zu bauen, das ich halbwegs booten konnte. Halbwegs. Kernel Panic. Aus die Maus.

cfdisk -z /dev/sda

… und auf Wiedersehen Partitionstabelle.

RTFM

Manchmal ist es doch so einfach: Die Debian Doku enthaelt einen netten Abschnitt ueber das Vorbereiten von Dateien fuer das Booten von einem USB Stick. Die boot.img.gz-URL hat mir Google sofort geliefert. wget, zcat, mount und eine sowieso in meinem Dateiarchiv existierende und aktuelle Debian ISO draufgeschoben, brachte mich nun endlich zum Erfolg. Der Debian Installer im Textmodus startete problemlos und hat es zugelassen die Installation auf einen anderen USB Stick vorzunehmen. Nur das mit dem Grub ist da so eine Sache… Der Installer dachte “ich USB, ich = sda, sdb = Ziel, (hd2,0) also Bootpartition” (hd0 ist die interne 32MB Flash Card, die so fest verbaut ist, dass man sie nicht austauschen kann). Dementsprechend pflanzte der Installer eine Grub Config (in (hd1,1), also /dev/sda1, wtf?) und die Flash Card habe ich als MBR-Bettchen erkoren. Beim Rebooten (und gleichzeitigem Entfernen des Installer-Sticks) war es natuerlich klar, dass Grub etwas verwirrt war. Interessanterweise hat er die Config gefunden. Nunja, jetzt zwei Zeichen im Bootmenue zu aendern war nicht das Problem und so bootete endlich(!!!) ein jungfraeuliches Debian.

Fileserver

… sind auch so eine Sache fuer sich. Samba (jaja, oldschool – aber soll ja jeder hier im Haus nutzen koennen) hat eine sehr interessante fluide Syntax fuer die Config. Wichtig hierbei war lediglich root preexec zum Mounten der Festplatte (sofern nicht bereits geschehen). Da meine Externe einen netten Stromsparmodus hat und sich selber abschaltet, wenn sie laenger nicht genutzt wurde, konnte ich mir postexec sparen.

Wenn man nun schon einen Fileserver hat, dann kann der ruhig auch hier und da mal dezentrales Filesharing (ohja, man kann auch legale Sachen damit herunterladen!) betreiben. rtorrent hatte ich noch im Hinterkopf, aber das habe ich ganz schnell wieder verworfen. Drecksteil. Das hat mehr Shortcuts als screen und vi zusammen. Oergs. Dann wollte ich noch torrentflux, eine Webschnittstelle zu rtorrent, nutzen… Ein guter Tipp: MySQL und 128MB RAM vertragen sich gar nicht. Letztendlich war MySQL an, dpkg dachte es sei deinstalliert (sogar purged) und das Init-Script mochte es nicht stoppen, weil es dazu ersteinmal initial-gestartet werden muesste, um die Configs zu generieren. Argh! Uebrigens, folgendes hilft:

dpkg --remove --force-remove-reinstreq mysql-server-5.0

Wo war ich? Achja, torrentflux. MySQL + Low Memory = Dreck. Apache ging lustigerweise. Aber ohne torrentflux brauch ich es nicht. Also weg mit dem Mist. Dann blieb ich erstmal bei Transmission. Dank babels Tipp habe ich mir dann doch (das per Default bereits installierte) BitTornado bzw. btdownloadcurses angeschaut. Nach knapp 1,5 Stunden Spielereien mit screen und btdownloadcurses kam dann ein nettes Bash-Script heraus, das Torrents herunterladen, sie archivieren, sharen und gleichzeitig in eine wunderbare Multiwindow-Screen-Umgebung packen kann. Das moechte ich natuerlich niemanden vorenthalten (ja, inkonsistente Schreibweise, aber ich bin atm zu faul):

#!/bin/bash
 
### CONFIG ###
 
DEVICE="/dev/sdb1"
MOUNTPOINT="/media/storage"
TORRENTDIR="/root/torrents"
DOWNLOADDIR="/media/storage/public"
PORTFILE="/root/torrents/.ports"
SCREEN_SESSION_NAME="__DEFAULT"
SCREEN_DATETIME="$(date +%Y%m%d_%H%M%S)"
MAX_UPLOAD_RATE=30
MIN_PORT=53535
MAX_PORT=53559
 
### CODE ###
 
if [ "$(cat /proc/mounts | grep /media/storage | wc -l)" -eq "0" ]; then
    echo -n "Mounting $DEVICE on $MOUNTPOINT..."
    mount -t ntfs-g3 $DEVICE $MOUNTPOINT > /dev/null 2>&1
    if [ "$?" -eq 0 ]; then
        echo " done."
    fi
fi
 
FILE=""
 
if [ -f "$1" ]; then
        echo "Loading $1..."
        FILE="$1"
else
        echo -n "Downloading torrent..."
        rm -f tmp.torrent
        wget -O tmp.torrent $1 > /dev/null 2>&1
        echo " done."
        if [ -f "tmp.torrent" ]; then
                echo -n "Name: "
                read NAME
                FILE="$NAME.torrent"
                mv tmp.torrent "$FILE"
        else
                echo "Torrent file does not exist"
                exit 1
        fi
fi
 
cd $DOWNLOADDIR
 
SCREEN_EXISTS=$(screen -ls | grep ${SCREEN_SESSION_NAME} | wc -l)
 
if [ "$SCREEN_EXISTS" -eq "0" ]; then
        /usr/bin/screen -S "${SCREEN_SESSION_NAME}" /usr/bin/btdownloadcurses --max_upload_rate $MAX_UPLOAD_RATE --minport $MIN_PORT --maxport $MAX_PORT "$TORRENTDIR/$FILE"
else
        COMMAND="screen /usr/bin/btdownloadcurses --max_upload_rate $MAX_UPLOAD_RATE --minport $MIN_PORT --maxport $MAX_PORT \"$TORRENTDIR/$FILE\""
        /usr/bin/screen -r "${SCREEN_SESSION_NAME}" -X eval "$COMMAND"
fi

Last but not least muss ich sagen: Es hat sich (bisher) gelohnt.

  1. Ich lese definitiv zu viel Fefe []

Relevante Informationen

Sonntag, den 22. November 2009

Da Netstumbler unter Windows 7 definitiv nicht funktioniert, dachte ich mir: “Hey, bau dir deinen eigenen Stumbler!” Und das habe ich auch getan. Dabei ist mir aufgefallen, dass ich gar nicht weiss, welche Daten fuer mich eigentlich relevant sind. Ich habe mir angeschaut, wie inSSIDer arbeitet und habe mir dort abgeschaut, wie ich Daten erfasse:

Zunaechst verbinde ich mich mit dem GPS Geraet und beginne zeilenweise die Daten zu parsen. Dann starte ich den Wifi-Scan. Jedesmal, wenn ein Scan abgeschlossen ist, schaue ich nach, ob ich das jeweilige Netzwerk schon kenne. Wenn ich es bereits kenne, dann ueberpruefe ich, ob der alte RSSI Wert geringer ist, als der neue. Ist das der Fall, dann setze ich die Position des gefundenen Netzes auf die aktuelle GPS Position, ansonsten lasse ich die alte bestehen. Kannte ich das Netzwerk noch nicht, dann habe ich es in die Liste der gefundenen Netze aufgenommen und die aktuelle GPS Position als Netzposition gesetzt.

Positionsapproximation

Positionsapproximation

Letztendlich erhalte ich so eine nette Liste von Wifi-Infrastrukturen, und die GPS Koordinaten von dem Ort, an dem man sich am Nächsten an der Infrastruktur befunden hat. Natuerlich koennte man das noch verbessern, indem man sich der Mathematik bedient, aber da die Reichweite von Access Points noch weit unter Sichtweite ist, ist das erstmal egal.

Bei der Programmierung ist mir aufgefallen, dass ich Wifi-orientiert arbeite, d.h. ich finde Netze und suche mir dazu die Positionen. Es geht natuerlich auch andersherum: Jedesmal, wenn mir der GPS Receiver eine Koordinate rauswirft, also meist mit einem ausreichenden Takt von 1Hz, dann kann ich die zur Zeit verfuegbaren Netze dazu assoziieren. Letztlich geht es hierbei um zwei Betrachtungsweisen, die sich zwei verschiedenen Fragen widmen:

  1. Welche Infrastrukturen gibt es an der Position X?
  2. Welche Position hat die Infrastruktur Y?

Beide Fragen sind durchaus interessant. Die Frage, die von den meisten Geoprogrammen beantwortet wird, ist die zweite, da gerade diese beim Wardriving interessant ist. Was ich mich jetzt frage ist:

Gibt es Anwendungsfaelle, bei denen es wichtiger ist, zu wissen welche Infrastrukturen an einer bestimmten Position sind? Wenn ja: Ist ein Hybridverfahren* eventuell geeignet?

* Das Hybridverfahren, das ich mir vorstelle, laesst sich wohl am Besten durch ein Event-basiertes System und einer Datenbank entwickeln:

Hybriduebersicht

Uebersicht: Hybridverfahren

Hierbei registriert sich die Klasse Main bei zwei Listenern, und implementiert deren Interfaces. Beide Methoden networkFound() und positionChanged() machen jetzt im Prinzip das Gleiche:

  • Gefundene Netzwerke (im networkFound()-Fall genau eins) werden in die Datenbank geschrieben (z.B. in die Tabelle networks).
  • Die Position wird in die Datenbank geschrieben (z.B. in die Tabelle positions).
  • Es werden Relationen in eine dritte Tabelle (z.B. network_positions) geschrieben. Darin enthalten sind nur die jeweiligen IDs der Netzwerke bzw. Positionen.

Mit dem Verfahren lassen sich beide Fragen beantwortet. Dieses Verfahren ist jedoch nicht verbreitet (keins der bekannten Tools arbeitet auf diese Weise). Aber wieso nicht?

GPS Device?

Montag, den 9. November 2009

Zur Zeit suche ich ein geeignetes GPS Geraet, das folgende Eigenschaften erfuellen soll:

  • Bluetooth + USB
  • Logging + Real-Time Navigation
  • Manuelle Speicherung der Position (Push-To-Log)
  • mind. 50 Kanaele
  • lange Akkulaufzeit (Minimum: 10 Stunden)
  • mind. 1 Hz (Logging & Receiving)
  • AGPS Unterstuetzung
  • NMEA via COM Port lesbar
  • Real-Time Navigation via Google Earth soll moeglich sein
  • Speicher fuer mind. 100.000 Wegpunkte
  • geringe Cold/Warm/Hot-Startzeiten
  • geringer Preis (< 80 EUR)

Hat jemand zufaellig Erfahrungen und Vorschlaege?

Nachtrag (10.11.2009):
Ich habe mir heute einen GPS Receiver von Nokia bei Amazon gekauft, weil ich das Teil heute life testen konnte und positiv ueberrascht war. Das werde ich erstmal durchtesten. Vorschlaege fuer andere Geraete sind immernoch willkommen, da ich einen Logger haben moechte. Ich habe das Geraet aber nur gekauft, weil es extrem guenstig und dafuer sehr gut ist. Gekauft habe ich es bei Amazon: Nokia LD-4W Bluetooth GPS Modul.

ZoneTrak

Freitag, den 30. Oktober 2009

Mjam, wie Einige schon mitbekommen haben, entwickle ich ein Real-Time Tracking System fuer Katastrophenschutzorganisationen am Pervasive Computing and User Interface Lehrstuhl von Prof. Albrecht Schmidt an der Universitaet Duisburg-Essen… Bevor ich alle langweile: Hier gibt es weitere Informationen fuer die jenigen, die mehr erfahren wollen:

ZoneTrak @ Tomorrow Lab

Var ist nicht var

Dienstag, den 4. August 2009

Ich bin nun mehrfach darauf gestossen, wie fehlgeleitet manche Personen in Bezug auf Designentscheidungen sind.

C# fuehrt mit der Version 3.0 das Keyword var ein. In der offiziellen C# Reference wird klargestellt, dass var nicht von variant, sondern von variable kommt. Alles andere wuerde die Natur von C# veraendern: C# ist strongly typed, d.h. jede Variable ist von einem Typ. Typdeklarationen und Objektdeklarationen sind eindeutig.

Was ich nun gelesen habe, sind Sachen wie…

I would say use var wherever you can use it

var has a lot of benefits. It forces initializing to not null value, and it introduces flexibility inside the method scope as Chris illustrated in his example.

oder

  1. It induces better naming for local variables.
  2. It induces better API.
  3. It induces variable initialization.
  4. It removes code noise.
  5. It doesn’t require the using directive.

Gegen Letzteres wurde richtigerweise wie folgt argumentiert:

It’s interesting how not only are almost all of these “benefits” mainly stylistic but how they contradict each other. For example, the claim that it leads to “better naming for local variables” really means it compels developers to use LONGER HUNGARIAN STYLE VARIABLE NAMES. Funny enough, these long variable names add more noise to the code overall since they show up everywhere the variable is used compared to a single type name showing up when the variable is declared. The argument that it leads to “better API” is another variation of this theme since it argues that if you are compelled to use LONGER MORE DESCRIPTIVE PROPERTY NAMES (e.g. XmlNode.XmlNodeName instead of XmlNode.Name) then this is an improvement. Someone should inform the ReSharper folks that encoding type information in variable names sucks, that’s why we’re using a strongly typed programming language like C# in the first place.

One more thing, the claim that it encourages variable initialization is weird given that the C# compiler already enforces that. More importantly, the common scenario of initializing a variable to null before it is used isn’t supported by the var keyword.

Zu letztem Absatz musste ich dann noch folgendes lesen:

Usually I don’t like initializing to null, but sometimes I find myself forced to because of some language limitations. So I can agree with you.

So. Zur Klarstellung: Erstens ist es keine Sprachlimitierung. Im Gegenteil: Es ist gewuenscht, dass Variablen, die nicht mit einem Wert belegt wurden, zumindest mit null belegt werden muessen, um keine undefinierten Zustaende zu erreichen. Ein Compiler sollte keine Annahmen ueber die Vorbelegung treffen muessen, da das zu undefinierten Zustaenden fuehren kann. Zweitens: var wurde zu Deklarationen von Variablen mit anonymen Typen geschaffen. Es wurde zeitgleich zu LINQ (und Lambda Expressions) eingefuehrt, was es C# ermoeglicht mit Sprachelementen Datenbankabfragen oder XML Abfragen in der selben Form durchzufuehren (ohne diese Anfragen in SQL umzuwandeln o.ae. Die Idee dahinter ist verwandt mit der von Embedded SQL). Eine solche Abfrage kann wie folgt aussehen (Auszug aus msdn):

var custQuery = from cust in customers
                where cust.City == "Phoenix"
                select new { cust.Name, cust.Phone };

Dieser Codeausschnitt deklariert die Variable custQuery und weist ihr einen Typ zu, der aus dem LINQ Ausdruck ermittelt wird. Aus dem IEnumerable implementierenden Interface customers werden alle Elemente gewaehlt, deren Property City den Wert Phoenix haben. Die Ergebnisrelation besteht aus Elementen, die anonyme Typen sind. Sie enthalten zwei Attributwerte, die aus einer Zeile der LINQ-Abfrage aggregiert werden. Es ist an dieser Stelle unmoeglich festzulegen, von welchem Typ new { cust.Name, cust.Phone } ist (heisst ja nicht umsonst anonymer Typ).

Genau hier und nur bei solchen Gelegenheiten, sollte var verwendet werden.

Nochmal zum Thema null-Initialisierung: Variablen mit null vorzubelegen zeugt von gutem Code. Jede nicht mit einem Objekt belegte Variable sollte den Wert null erhalten, um Fehlerbehandlung durch Ueberpruefung auf null erst moeglich zu machen. Wer damit argumentiert, dass ja an irgendwelchen Stellen NullReferenceExceptiosn geworfen werden, der gehoert erschossen. Exceptions sind keine Compilerfehler. Sie sollen auch gar keine Compilerfehler ersetzen, solange kein Algorithmus gefunden wurde, der in polynomieller Zeit berechnen kann, ob eine Variable bei fester Eingabe eines Programmes mit null belegt wurde (das Problem ist auf das Halteproblem reduzierbar; demnach ist das Problem unentscheidbar, solange P = NP nicht bewiesen wurde). Sie dienen viel mehr dazu Ausnahmen(!), die zur Laufzeit(!) auftreten behandeln zu koennen. Sollte (bei Vorbelegung mit null) dennoch regelmaessig eine NullReferenceException auftreten, so hat man beim Programmieren wohl Mist gebaut.

Uebrigens basiert LINQ auf dem generischen Interface IEnumerable. Klassen, die IEnumerable implementieren, bieten mittlerweile auch Methoden an, die mit Lambda Expressions genutzt werden koennen. Der oben angegebene Code wird (zur Compiletime) in folgenden Code uebersetzt:

var custQuery = customers.Where(cust => cust.City == "Phoenix").Select(cust => new { cust.Name, cust.Phone });

So und nicht anders. Argh.

Architekturentscheidungen

Donnerstag, den 30. Juli 2009

Im Moment haenge ich an einer Designentscheidung… Letztendlich gibt es bei meinem Problem viele Loesungen und alle funktionieren auch.

Ich arbeite zur Zeit mit OpenGL in .NET und da treffen zwei voellig verschiedene Paradigmen aufeinander. OpenGL verwaltet seine Stacks selbststaendig; jede Methode ist static. Das passt nicht zu objektorientierter Programmierung, aber man kann OpenGL ja nicht einfach biegen, wie man es gerne haette. Die Favoriten sind im Moment folgende (wobei ich bei jedem die Nachteile hasse):

Klassendiagramm

Klassendiagramm

Ganz links wird eine verschachtelte Fassade mit einem Singleton (Nested Facaded Singleton) genutzt, um die Zustandsbehaftung und gleichzeitig die statische Natur von OpenGL zu erhalten, indem die aeussere Klasse die selben Methoden anbietet, wie die innere Klasse. Nur die aeussere Klasse kann auf die Methoden der inneren Klasse zugreifen. Die aeussere Klasse bietet statische Methoden an, die innere bietet nicht-statische Methoden ueber ein Singleton (d.h. nur einmalige Instanzierung moeglich) an. Die Klasse NestedFacade macht nun z.B. in ihrer Methode Method folgendes:

public static void Method()
{
    Singleton.Instance.Method();
}

Und da ist auch direkt der Knackpunkt: Ich verdopple somit die Anzahl der Methoden ohne die Funktionalitaet zu erhoehen. Hierdurch erzeuge ich eine hohe Kopplung ohne die Kohaesion zu veraendern. Das allein reicht fuer mich eigentlich schon als Totschlagargument.

In der Mitte ist ein klassisches Singleton Pattern (mit Threadsicherheit durch Locking*) abgebildet. Im Moment habe ich meine Klasse auf die Art modelliert. Eigentlich stoert mich daran auch nur, dass ich jedesmal via Instance darauf zugreifen muss:

Singleton.Instance.Method();

Bisher hat mich das sehr gestoert, da ich das Singleton doch sehr haeufig benutzen muss.

Naja * Locking ist auch so eine Sache. In C# gibt es da viele Herangehensweisen: Einfaches Locken, Double-Lock Idiom, Static Constructor, Instance Container, Delegation statt Locken, Verwendung anonymer Delegate… Jedes (Not-)Locking Idiom macht meist nur in einem Pattern Sinn und ist ansonsten ein Anti-Pattern. Argh.

Letzteres Diagramm zeigt eine einfache Facade, die die OpenGL Methoden kapselt. Problem hierbei: Da ich das Objekt an mehreren Stellen benoetige, muesste ich die selbe Referenz an mehreren Orten verwalten. Das ist mir viel zu viel Overhead und die Kopplung explodiert ins bodenlose. Der Vorteil dabei waere jedoch, dass die Kohaesion bei 100% liegen wuerde.

Lustigerweise hatte ich bei der Modellierung des restlichen Systems keinerlei Schwierigkeiten, da alles sehr offensichtlich in die diversen Patterns gepastet hat.

Copy & Paste via PuTTy in vim

Montag, den 13. Juli 2009

Des Oefteren hatte ich das Problem, dass ich in vim Text kopieren und wieder einfuegen wollte. Dabei hat vim den Text immer sehr merkwuerdig eingerueckt. Jetzt habe ich endlich die Loesung fuer das Problem gefunden.

vim ist automatisch im auto-indentation Modus. Jedem Text wird eine Einrueckung vorangestellt, die sich auf die zu letzt eingerueckte Zeile bezieht. Dadurch entsteht haeufig Mist, den man manuell korrigieren muss. Hier ist die Loesung:

Auto-Einrueckung abschalten:

:set paste

Auto-Einrueckung wieder einschalten:

:set nopaste

Mal wieder in der Kiste gekramt…

Freitag, den 29. Mai 2009

…und das gefunden:

Vor allem das letzte Frage-Antwort-Tupel ist beeindruckend.

UI Design und Prototyping

Mittwoch, den 27. Mai 2009

Viele machen es, manche hassen es, manche moegen es: UI Design. Ich gehoere zu der letzten Kategorie, solange keine Anforderungsspezifikationen existieren, die mir von Anfang an suggerieren, dass sie Bockmist sind. Hier ist mal ein {schoener|interessanter|lesenswerter} Blog mit vielen (teilweise kuriosen) Beispielen aus den genannten Bereichen:

Ab zu Wireframes.

Festplatten-Crash und die Superbar

Sonntag, den 24. Mai 2009

Festplatte meines Notebooks hat Hardwareschaden. Stopp.

Windows 7 Superbar rockt. Stopp.

Siehe auch Windows 7 programming @ Sergey Zwezdin. Stopp.

Windows 7 programming: Taskbar. Part 1 – Progress Bar. Stopp.

Windows 7 programming: Taskbar. Part 2 – ThumbButtons. Stopp.

Windows 7 programming: Taskbar. Part 3 – OverlayIcon. Stopp.

Windows 7 programming: Taskbar. Part 4 – Custom OverlayIcon. Stopp.

Windows 7 programming: Taskbar. Part 5 – CustomWindowsManager. Stopp.

Windows 7 programming: Taskbar. Part 6 – AppId. Stopp.

PHP Template Engines: Must-Have oder Crap?

Mittwoch, den 18. Februar 2009

Wieder mal muss ich in alten Diskussionen wuehlen und meinen Senf dazu abgeben. Template Engines… Braucht man sie zu Zeiten von PHP5 noch? Muessen Logik und Praesentation getrennt sein? Wieso sollte ich meine Architektur so waehlen, nur weil es Andere machen? Zu Zeiten vor PHP5 war die Hochzeit der Template Engines. Die meisten professionellen Produkte nutzten Template Engines, vor allem weil mehrere Personen an der Softwareentwicklung beteiligt waren. So konnten Designer unabhaengig von den Entwicklern arbeiten. Mittlerweile kann doch aber jeder Designer PHP, oder nicht? Also ist eine Trennung von Logik und Praesentation nicht mehr noetig, oder?

Nein, auch heute kann der grosse Teil der Designer kein PHP. Es gibt zwar eine Menge, die hier und da ein wenig Code verstehen, aber solche Leute sollte man nicht an die Entwicklung von Software lassen. Jeder kann mit ein bisschen Uebung PHP Code schreiben. Aber das ist noch sehr weit entfernt von professioneller Softwareentwicklung.

Viele der neueren Template Engines (Smarty & Co.) bieten die Moeglichkeit Makros zu verwenden, um Bedingungen, Schleifen und andere Konstrukte anzuwenden. Diese Konstrukte werden spaeter durch ein PHP Script ausgewertet und ausgefuehrt. Meines Erachtens loesen solche Makros jedoch die strikte Trennung der Architekturbausteine auf.

In einem Artikel den ich auf phpguru.org mit dem Namen “Simple PHP templating class” und auf hotscripts.com mit dem Namen “Templating – the correct way” gefunden habe, argumentiert der Autor unsachlich gegen die Verwendung von Template Engines mit Saetzen, im folgenden Stil:

Any HTML monkey that can’t handle the above should kill themselves right now, and allow the collective IQ of the world rise a few points.

Mittlerweile bin ich fuer mich persoenlich zu dem Schluss gekommen, dass Logik und Praesentation strikt getrennt sein sollten. Gruende dafuer sind zum Beispiel:

  • PHP Code ist wartbarer und Debugging ist einfacher. Die Komplexitaet einer Codedatei ist niedriger.
  • Aenderungen am Aussehen lassen sich schneller durchfuehren, da der Aufwand die (X)HTML-Bloecke im Code zu finden entfaellt.
  • “Designers don’t do code”.

Gegen die Argumente, dass eine hochkomplexe, dynamische Webseite nicht ohne Logik im Layout auskommt, kann ich nur meine Herangehensweise bei Template Engines anfuehren:

  1. Templates sind hierarchisch angeordnet.
  2. Ein Template enthaelt Platzhalter.
  3. Platzhalter koennen durch ein Template, durch mehrere Templates oder durch Text ersetzt werden.

Dadurch ergibt sich, dass ich auch Schleifen mit Templates verwenden kann, ohne Schleifen in den Templates zu haben. Ich habe in der letzten Zeit alle meine alten Template Engines wiedergefunden und dabei festgestellt, dass ich jede Stufe der Komplexitaet einmal ausprobiert habe. Meine erste Template Engine aehnelt der, die ich im Moment verwende. Im Laufe der Zeit erhoehte sich die Komplexitaet. Ich machte Schleifen und Bedingungen moeglich, fuegte Filterfunktionalitaeten und einen Cache hinzu. Es folgten Kleinigkeiten, wie z.B. ein Rotierer fuer Zeichenketten in Schleifen, den ich vor allem fuer abwechselnd gefaerbte Zeilen verwendet habe.  Ich musste mit der Zeit feststellen, dass die groesseren Moeglichkeiten sich proportional zur Komplexitaetssteigerung verhielten, die ich mit Template Engines eigentlich zu vermeiden versuchte. So wurden die Features meiner Template Engine immer geringer.

Hier mal ein Beispiel fuer die Anwendung meiner Template Engine:

$index = new Template("index");
$table_row = new Template("table_row");
while ($row = mysql_fetch_object($result))
{
    $table_row->set("KEY", $row->key);
    $table_row->set("VALUE", $row->value);
    $index->addChild("ROWS", $table_row);
}

Eine Auszug aus der Beispiel index.tpl:

<table>
    <tr>
        <th>Key</th>
        <th>Value</th>
    </tr>
{ROWS}
</table>

Und die table_row.tpl:

    <tr>
        <td>{KEY}</td>
        <td>{VALUE}</td>
    </tr>

Ich bitte all diejenigen, die Interesse an meiner Template Klasse haben, sich noch etwas zu gedulden. Ich baue mir gerade ein kleines PHP Framework aus Funktionen und Methoden zusammen, die mir das Leben immer sehr vereinfacht haben. Dazu gehoert auch dei Template Klasse.

Achja, bevor ich es vergesse: Es gibt natuerlich auch Template Engines, die so maechtig sind, dass man ihnen nur noch Datenquellen angeben muss, woraus sie automatisiert Tabellen o.ae. generieren. Das sind aber eine ganz andere Klasse von Template Engines, die ich aus dieser Diskussion erstmal ausklammern moechte.