Der Tntnet-Witzard unterstützt jetzt die wichtigsten Funktionalitäten

Standard

snapshot_tntwebwizardSeit Anfang Februar arbeite ich an einem Wizard mit Weboberfläche, um schnell und leicht Tntnet-Projekte zu erstellen. Das C++-Webframework Tntnet schreibt dem Anwender keinerlei Doktrin vor, wie er sein Projekt zu organisieren hat. Diese Freiheit macht es dem Einsteiger sehr einfach zu schnellen Ergebnissen zu kommen. Geht es aber darum, größere Projekte umzusetzen, braucht es dann schon etwas Erfahrung, um den Code sinnvoll zu strukturieren. Auf der Website von Tntnet gibt es eine ganze Reihe von hilfreichen Howtos und Beispielen zu verschieden Themen. Hier beginnt nun eine ziemlich steile Lernkurve.

Der TNT-WEB-WIZARD soll dabei helfen, diese Lernkurve etwas abzumildern. Er hilft dabei, ein neues Projekt zu erstellen und generiert den Code nach „best practice“. Es gibt eine Reihe von Funktionen, die Tntnet nicht von Hause aus mitliefert, zum Beispiel für CSRF (cross site request forgery). Es ist recht einfach, das mit wenig Code selbst zu implementieren, wenn man weiß wie e geht. TNT-WEB-WIZARD legt diesen Code für den Benutzer an und hilft ihm ihn richtig einzusetzen.

Darüber hinaus erstellt TNT-WEB-WIZARD eine Verzeichnisstruktur in der MVC (model view controller) klar erkennbar und gegliedert ist. Komponenten werden in separaten Verzeichnissen mit eigenem Namespace organisiert. Dies ist wichtig, um den Code großer Projekte langfristig pflegen zu können. Alles was TNT-WEB-WIZARD macht, kann man auch per Hand anlegen. Es hat aber zwei große Vorteile hierfür den TNT-WEB-WIZARD zu benutzen:

  • A) Alle Komponenten weisen die selbe Struktur auf (egal wer sie in einem Team anlegt). Das erleichtert das Zurechtfinden im Code anderer Team-Mitglieder
  • B) Es geht schneller. Denn um eine „full featured“ MVC-Komponente zu erstellen, kommen schnell einige hundert Zeilen Code zusammen.

Um TNT-WEB-WIZARD benutzen zu können, muss es nicht lokal installiert sein. Die Bedienung erfolgt über eine Weboberfläche und der generierte Code wird zum Schluss heruntergeladen. Bestehender Projekt-Code kann von GitHub hoch geladen werden und mit dem TNT-WEB-WIZARD um neue Komponenten erweitert werden.

TNT-WEB-WIZARD legt zwei Dateien im Basis-Verzeichnis an, um Projekte zu verwalten. Zum einen eine „tntwebwizard.pro“ und eine „Makefile.tnt“. In der „tntwebwizard.pro“ werden die Projekt-Vorgaben gespeichert. Existiert die Datei nicht, wird sie neu angelegt. Die zweite Datei die „Makefile.tnt“ beinhaltet die Regeln, wie das Projekt übersetzt werden muss. Diese Datei wird von TNT-WEB-WIZARD angelegt und gepflegt. Wenn man das Build-Tool Tntmake verwendet, ist das Übersetzen sehr einfach. Möchte man lieber GNU Make, CMake, QMake oder was auch immer verwenden, muss man sich selbst darum kümmern, diese Tools mit Tntnet zum Laufen zu bekommen. TNT-WEB-WIZARD verwendet Tntmake, da das Dateiformat Json ist und sich so sehr gut mit dem TNT-WEB-WIZARD parsen und bearbeiten lässt.

Ich glaube, wenn man den TNT-WEB-WIZARD geschickt einsetzt, kann man bei der Implementierung seiner Ideen einige Zeit sparen und sich besser auf Geschäftslogik konzentrieren. In TNT-WEB-WIZARD sind noch nicht alle meiner Ideen umgesetzt. Den aktuellen Stand kann man auf http://peruschim.de:8010/core/about sehen. Sicher sind auch noch einige Fehler unentdeckt. Möglicherweise gibt es auch noch ein paar gute Ideen für weitere Funktionalitäten. Wem etwas auffällt kann entweder den Issues-Trecker nutzen oder mir eine Mail schreiben (briefkasten@olaf-radicke.de).

Die Dokumentation ist nur in Form von Doxygen-Kommentaren vorhanden. Mit dem Befehl „doxygen ./Doxyfile“ kann man sich eine API-Doku generieren. Ich habe versucht das Programm so zu gestalten, dass man es auch ohne Doku durch „trial and error“ zum Ziel kommen kann. Natürlich ist Doku nie verkehrt und wer mich dabei unterstützen mag, ist dazu herzlich eingeladen. Als nächstes plane ich „Black-Box-Tests“ um TNT-WEB-WIZARD besser und automatisch zu testen. Dazu werden noch Artikel erscheinen, in den ich von meinen Erfahrungen dabei berichten werde.


Creative Commons Lizenzvertrag

Fortschritte des Tntnet-Witzard

Standard

Aktuell arbeite ich an dem webbasierten Wizard-Tool für Tntnet-Projekte. Wie vor einiger Zeit in dem Artikel „Proof of concept für ein alternatives Build-System für Tntnet“ angekündigt, habe ich das Build-Tool reinimplementiert, und zwar in Python. Ausschlaggebend war die Erfahrung, dass mein Ruby-Code unter RHEL6/CentOS6 nicht so einfach zum Laufen zu bekommen ist. Das führte dazu, dass ich meinen Tntnet-Wizard nicht auf den Server übersetzen und installieren konnte. Die Reimplementierung ging einigermaßen zügig,  hat mich jedoch trotzdem einige Tage Zeit gekostet.

Mit dem eigentlichen Programm bin ich jetzt soweit, dass man den Kern einer Tntnet-Application zusammenklicken und herunterladen kann. Derzeit arbeite ich daran, das der Projekt-Kern durch Komponenten erweitert werden kann. Dies soll ebenfalls graphisch über eine Weboberfläche gehen. Wenn der Schritt fertig ist, plane ich eine GitHub-Unterstützung zu intrigieren. So das man Code aus GitHub in das Programm laden, neue Komponenten hinzufügen, den generierten Code herunterladen, den Code in der IDE, die man sonst verwendet, erneut prüfen und wieder in GitHub commiten.

Das Buildtool, das ich mir gebaut habe, heißt Tntmake und ist hier zu bekommen: https://github.com/OlafRadicke/tntmake

Das Tool zum Generieren von Tntnet-Projekten trägt den Namen Tntwebwizard und ist hier zu finden: https://github.com/OlafRadicke/tntwebwizard

Tntmake verwende ich seit ein paar Wochen und es arbeitet (für meine Zwecke) sehr gut. Woran ich mir ein bisschen die Zähne ausgerissenen habe, war das Thema „uppercase in c++„. In dem Artikel umlautproblemen in Tntnet-Applicationen mit Hilfe von cxxtools aus dem weg gehen habe ich bereits festgestellt, dass meine Funktion, die aus Kleinbuchstaben Großbuchstaben machen soll, mit Umlauten nicht funktioniert. Jetzt fand ich mich an dem Punkt wieder, dass ich für meinen Tntwebwizard erneut eine Funktion brauchte, die Strings in uppercase formatiert.

Also habe ich versucht das Problem mit cxxtools::toupper() zu lösen. Es ist, mir leider nicht gelungen. Mangels Dokumentation habe ich nicht herausgefunden, ob und wie es geht. Ich habe mir die Headerdateien von der Classe cxxtools::Char angeschaut und zwei Stunden herumprobiert, jedoch keine Lösung gefunden. Mir ist es zwar gelungen, alle Zeichen in Großbuchstaben umzuwandeln, ohne dass die Umlaute zerbrechen, aber die Umlaute werden leider nicht in Großbuchstaben umgewandelt. Ein“ä“ bleibt ein „ä“ und wird nicht zu einem „Ä“.

Der Code sieht so aus:


#include <cxxtools/utf8codec.h>
#include <cxxtools/string.h>
#include <cxxtools/char.h>
[...]
01| std::string NewModelData::toUpper( std::string _mixedString ){
02|     std::ostringstream upperString;
03|     cxxtools::String lastLetter;
04|     cxxtools::String uMixedString = cxxtools::Utf8Codec::decode( _mixedString );
05|     for ( cxxtools::String::size_type i=0;
06|         i < uMixedString.length();
07|         ++i
08|     ){
09|         upperString << cxxtools::toupper(uMixedString[i]);
10|     }
11|     return upperString.str();
12| }

Dann habe ich nach einer Lösung mit reinem C++Standard-Libs gesucht, und bin dann auf diese kompakte Lösung gekommen:


01| std::string toUpper( std::string _mixedString ){
02|     std::ostringstream upperString;
03|     std::locale loc;
04|     for (std::string::size_type i=0; i<_mixedString.length(); ++i)
05|         upperString << std::toupper(_mixedString[i],loc);
06|     return upperString.str();
07| }

Die String-Verarbeitung in C++ ist immer ziemlich krampfig, zwar nicht ganz so schlimm wie in C, aber trotzdem unbefriedigend. Die cxxtools bringen hier leider auch keine befriedigenden Lösung.

Ich habe gesehen, dass es wohl noch eine Lösung mit Boost gibt. Allerdings würde ich die Abhängigkeit zu Boost gerne vermeiden. Vielleicht komme ich noch auf eine andere Lösung, dann werde ich es berichten.


Creative Commons Lizenzvertrag

Kurztipp: Modularer Seiten mit dem Tntnet Template-System ecpp

Standard

In aller Regel hat man auf einer Seite immer wiederkehrende Elemente, wie den Kopfbanner, ein Navigation oder eine Fußzeile. Um die DRY-Regel „Don’t repeat yourself“ zu befolgen, müssen wir also ein Weg finden, wie wir diese immer wiederkehrenden Elente auslagern und in die betreffenden Seiten als externe Elemente wieder einbetten. In Tntnet gibt es verschiedenen Möglichkeiten dieses zu tun. Diese beruhen auf zwei grundlegenden Technologien, die ich hier kurz vorstellen möchte.

Einbetten von Code

Die erste Variante ist die, den Code, der sich immer wiederholt, in eine externe ecpp-Datei auszulagern, und den Code per include an die betreffenden Stellen der ecpp-Views einzubetten. Dieses geschieht über:

<%include>src/view/part/head.ecpp</%include>

Hier wird der Code der Datei „src/view/part/head.ecpp“ eingefügt. Beim Übersetzen der ecpp-Datein wird der Code 1:1 an die betreffende Stelle kopiert. Das Verfahren ist sehr simpel.

Der Vorteil ist, dass schon bei dem Übersetzen des Codes fehlende Abhängigkeiten erkannt werden, z.B. wenn die Datei mit dem zu includierenden Code nicht vorhanden ist.

Der Nachteil ist, dass die Komponenten/Dateien keine eigenen Namensräume haben. Das kann dazu führen, dass es zu unerwünschten Nebeneffekten kommt, z.B. wenn zufällig die gleichen variablen Namen verwendet werden. Ein weitere Nachteil ist, dass das Build-System (GNU Make z.B.) nichts von dem Code-Include weiß. Wenn der Code, der includiert wird, von dem Entwickler geändert wird, weiß das Build-System nicht, dass es alle ecpp-Dateien neu übersetzen muss, in denen der Code includiert wurde.  Also muss man händisch ein „make clean“ ausführen, das wiederum für längere Übersetzungszeiten sorgt. Alternativ kann man versuchen, diese Abhängigkeiten mit Tools wie Autotools abzubilden. Das setzt jedoch ein gesteigertes Wissen über das Build-System voraus.

Einbetten von Komponentenausgaben

Ein anderer Weg ist, die Elemente, die man immer auf verschiedenen Seiten einbetten will, als eigenständige Komponenten umzusetzen. Dazu werden die diese Elemente (Menüs, Banner, Fußzeile, etc.) als eigenständige Komponenten übersetzt. Diese werden dann wiederum zur Laufzeit in der Seite aufgerufen und deren Rückgaben werden dann in die (Gesamt-)Ausgabe eingebettet. Das geschieht über diesen Tag:

 <& core_part_head qparam >

„core_part_head“ ist die Komponente die aufgerufen wird. „qparam“ sind die Request-Parameter des Ausrufs, die an die (Teil-)Komponente weitergereicht wird.

Der Vorteil dieses Verfahren ist, dass das Build-System nicht die Abhängigkeiten kennen muss. Ein weitere Vorteil ist, dass die Komponenten völlig autonom sind und sich untereinander nicht in die Quere kommen können.

Ein Nachteil ist, dass bei dem Aufruf der Teil-Komponente die Aufruf-Parameter weitergereicht werden müssen. Ein weiterer Nachteil ist, dass die Verknüpfung nur sehr locker ist, und nicht zur Übersetzungszeit geprüft wird. Das bedeutet, dass bei einem Schreibfehler des Komponentennamen, dies leider erst zur Laufzeit zu einem Fehler führt.

Ein weiteres Merkmal dieser Variante kann man als Vor- und als Nachteil sehen: Die Teil-Komponenten, also Menüs, Banner, Fußzeilen usw. lassen sich direkt aufrufen, wenn man die URL der Teil-Komponente kennt. Im obigen Beispiel „http://domainname.de/core_part_head„. Zu sehen bekommt man dann das Menü oder die Fußzeile oder was auch immer die Teilkomponente ist.

Das ist kein Drama und lässt sich durch rewrite rules abfangen, nur man muss halt daran denken. Etwas fataler könnte es sein, wenn man seine ACLs (Access Control List) URL-Basiert realisiert. Dann könnte es sein, dass sich ein User quasi „unter dem Radar“ Zugang zu Funktionen verschaffen kann, die für ihn nicht gedacht sind.

Weiterführendes: ecpp manpage: http://www.tntnet.org/man7/ecpp.7.html


Creative Commons Lizenzvertrag

Kurztipp: Routing in Tntnet

Standard

Gestern habe ich eine Weile an einem Routing-Problem geknobelt. Für statische Dateien (statisches html, Bilder, css usw.) gibt es in Tntnet die Möglichkeit diese fest einzukompilieren. Das Verfahren ist in einem Howto ganz gut beschrieben: Static files with Tntnet. Ich hatte nur das Problem, dass ich nicht genau wusste, mit welchem Pfad ich die einkompilierte Datei erreichen kann.

Ich habe jetzt herausgefunden, dass man in die .cpp-Datei vom ecppc hinein schauen kann. Dort findet sich dann dieser Abschnitt:


static const char* urls[] = {
  "./src/core/resources/favicon.ico",
  "./src/core/resources/normalize.css",
  "./src/core/resources/tntnet.png",
  "./src/core/resources/tntwebwizard.css",
};

Das sind die Pfade, unter denen die Dateien später zu erreichen sind. Wurden die Ressourcen beispielsweise in der Datei „resources.cpp“ zusammengestellt, würde die Route so aussehen:


    app.mapUrl("^/core/tntnet.png$", "resources")
        .setPathInfo("./src/core/resources/tntnet.png");

Sie ist im Browser unter der URL „http://meineseite.de/core/tntnet.png&#8220; das Bild „tntnet.png“ zu erreichen ist. Unschön ist der relative Pfad. Das sind die Pfade, die vom ecppc-Kompiler übernommen werden, wenn man ihn so aufruft:


ecppc -bb -z -n resources -p -o bulid/resources ./src/core/resources/favicon.ico ./src/core/resources/normalize.css ./src/core/resources/tntnet.png ./src/core/resources/tntwebwizard.css

Mit dem „-I./src/core/resources“-Flag lässt sich das jedoch ändern:


ecppc -bb -z -I./src/core/resources -n resources -p -o bulid/resources favicon.ico normalize.css tntnet.png tntwebwizard.css

Mit dem „-I“-Flag wird der Pfad angegeben, in dem die Sourcen gesucht werden. Deshalb kann der Pfad verkürzt werden. Jetzt sieht das Routing auch besser aus:


    app.mapUrl("^/core/tntnet.png$", "resources")
        .setPathInfo("tntnet.png");

Dieser Umgang mit statischen Inhalten ist jedoch nur für Dinge geeignet, die sich selten ändern. Das Logo einer Website zum Beispiel. Der Vorteil ist, dass die Inhalte extrem schnell ausgeliefert werden, so dass alle Elemente sich in einer Datei befinden und  Hacker nicht so einfach fremde Inhalte unterschieben können.


Creative Commons Lizenzvertrag

Proof of concept für ein alternatives Build-System für Tntnet

Standard

Ich habe in letzter Zeit viel darüber nachgedacht, wie man Tntnet attraktiver machen kann. Angeregt von dem Buch „Open Source Projektmanagement. Softwareentwicklung von der Idee zur Marktreife“ (Von Michael Prokop, Open Source Press; Auflage: 1. Aufl. 27. September 2010, ISBN-10: 3937514600), habe ich mir deshalb Gedanken über die Zielgruppen gemacht.

Herausforderung

Ich sehe für Tntnet potenziell User, die sich grob in zwei Gruppen teilen lassen. Die erste Gruppe besteht aus Usern, die schon sehr viel Erfahrung mit C++ haben und seit vielen Jahren damit arbeiten. Diese User haben vielleicht bisher einen weiten Bogen um Webprogrammierung gemacht, weil sie alles was sie bisher in diesem Bereich gesehen haben für grausame Pfuscherei hielten.

Die zweite Gruppe, die ich sehe, hat sehr viel Erfahrung mit den klassischen Vertretern der Webprogrammierung, wie PHP, Python, Ruby on Rails, Java und den ganzen Frameworks, die damit in Zusammenhang stehen. Diese User haben bisher um C++ einen weiten Bogen gemacht, weil sie glauben, die Sprache sei unelegant und würde die Produktivität hemmen.

Für die erste Gruppe hat Tntnet schon viel getan, um den Einstiegt attraktiv zu gestalten. Für die Entwickler, die aus der C++ Ecke kommen, dürfte Tntnet viele bekannte Konzepte aufweisen. Tntnet bietet die Freiheiten, die ein C++-Programmierer gewohnt ist. Tntnet macht nicht viele Vorschriften, wie ein Projekt umzusetzen ist. Durch Tntnet wird Webprogrammierung für C++-Entwickler abstrahiert und vereinfacht.

Bei der zweiten Gruppe mit Usern, die aus der Welt der hochabstrakten Skriptsprachen mit ihren mächtigen Frameworks kommen, sehe ich noch erhebliche Barrieren. Selbst Java-Entwickler, denen Compiler nicht gänzlich fremd sind, werden sich anfänglich mit dem Buildsystem von C++ schwer tun. Ich fürchte, dass Viele über diese Hürde nicht hinweg kommen.

Lösungsansatz

Diese Überlegung hat mich dazu angeregt, darüber nachzudenken, wie man das Buildsystem vereinfachen kann, um die Barriere für Umsteiger zu senken.

Ich habe mir GNU Make, Autotools, CMake und QMake anschaut. Das sind mächtige Werkzeuge, aber viel zu kompliziert und mit zu viel „Magie“, die irgendwas im Hintergrund macht, das man nicht versteht. Ich habe mir auch kurz das Buildsystem von Code::Blocks angesehen. Code::Blocks ist eine IDE für C++. Es gibt eine sehr schöne Doku darüber, wie man Tntnet mit Code::Blocks verwenden kann. Es gibt nur ein Haken: Man kann keine Tntnet-Projekt mit Code::Blocks übersetzen ohne X, also auch nicht skriptgesteuert durch ein Continuous Integration Server wie Jenkins.

Nachdem ich diese Möglichkeiten alle verworfen habe, bin ich zu dem Entschluss gekommen, etwas eigenes zu probieren und mit einem proof of concept zu beginnen. Zuerst versuchte ich ein Wrapper oder eine „Premake“ zu schreiben, der aus seiner vereinfachten yaml-Konfiguration ein Automake-File baut. Nach ein paar Versuchen stellte ich fest, dass der Weg zu kompliziert ist, da versucht wird, die verwirrende „Magie“ von Automake & Co durch noch mehr „Magie“ zu verstecken. Also habe ich probier ein Build-Tool zu schreiben, das ohne andere Tools auskommt, um die Komplexität zu reduzieren.

Umsetzung

Herausgekommen ist ein Tool, dem ich den Namen Tntmake gegeben habe. Der Code ist auf GitHub zu finden.

Das Tool ist in Ruby geschrieben. Ich habe Ruby nur gewählt, weil ich mich beruflich derzeit damit beschäftigen muss. Als Proof of concept ist das auch okay. Aber die richtige Umsetzung werde ich eher in einer anderen Sprache realisieren, da schon einige Probleme aufgetreten sind. Zum einen ist da die Abhängigkeit zu einer weiteren Programmiersprache, die standardmäßig nicht immer vorhanden ist. Das habe ich zum Beispiel auf Pidora gemerkt. Dann nervt mich die mangelnde Kompatibilität unter den Ruby-Versionen, da ich von C++ anderes gewohnt bin.

Überrascht war ich beim Thema Geschwindigkeit. Im Vergleich zu C++ kriecht Ruby, wie zu erwarten, nur so dahin. Verglichen mit Autotools hingegen, ist mein Proof of concept ohne Optimierungen doppelt so schnell wie Automake. Damit habe ich nicht gerechnet. Um mein eigenes Projekt Peruschim zu übersetzen, brauchte Autotools ca. 2 Min. 11 Sek, Tntmake hingegen nur 53 Sek.

Geschwindigkeit ist aber nicht das vorrangige Ziel von Tntmake. Es soll auch nicht das Bessere Automake sein oder CMake, QMake und Co ersetzen. Ziel ist allein Tntnet-Projekte möglichst einfach zu übersetzen. Ich denke, Umsteiger,  die vom Skriptsprachenuniversum kommen, interessiert das Buildsystem erst mal nicht, solange es ihnen nicht im Weg steht. Für große Projekte mit besonderen Anforderungen wird der Ansatz von Tntmake nicht reichen. Diese Projekte realisieren aber in aller Regel Fachleute, die nichts anderes tun haben, als sich um den Build-Prozess zu kümmern und für die dann Tools wie CMake, Autotools und Co kein Hindernis darstellen.

Wie geht es weiter?

Während der Arbeit an Tntmake habe ich das Konfigurationsformat von yaml auf json umgestellt. tntnet unterstützt yaml (noch) nicht. Ich werde im nächsten Schritt probieren, ob es machbar ist, mit Tntnet einen webbasierten Projekt-Wizard zu schreiben, der Tntmake als Backent verwendet. Deshalb war es mir wichtig, das die Konfiguration (gut) Maschinenlesbar ist.

Ursprünglich wollte ich Tntmake multithreading-fähig machen. Ich habe das bereits im Code umgesetzt. Mit dem Flag „tntmake -tb“ kann man das auch ausprobieren. Ich dachte, dass ich das bräuchte, weil Ruby so langsam ist. Aus irgendwelchen Gründen funktioniert Multithreading aber leider nicht. Da ich festgestellt habe, dass Tntmake derzeit schnell genug ist, verwende ich darauf zunächst keine weitere Zeit.

Ich werde in der nächsten Zeit den Code, in Vorbereitung auf eine Reimplementierung in eine andere Programmiersprache, noch etwas aufräumen.


Creative Commons Lizenzvertrag

Umlautproblemen in Tntnet-Applicationen mit Hilfe von cxxtools aus dem Weg gehen

Standard

In der Vergangenheit hatte ich, dank der strenge Typisierung, mit C++ selten Probleme mit UTF-8 und Umlauten. Ganz anders in Python2.x, Ruby und Bash. Hier habe ich schon Stunden und Tage damit zugebracht, Fälle abzufangen, in denen ich einen unerwarteten String-Codierung bekam. Die – vermeintliche – Zeitersparnis, die ich durch Duck-Typing habe, bezahle ich später durch Laufzeit-Typ-Prüfung doppelt und dreifach wieder zurück. Zum Einen ist der Code mit Duck-Typing ineffizienter und langsamer, zum Anderen schreibe ich dann doch wieder mehr Code, weil ich Duck-Types auf ihren jeweiligen Type testen muss, um ggf nicht ASCII-Code mit Unicode vergleichen bzw. verarbeiten zu müssen. So wird Duck-Typing zu einer „Informatik-Schuldenfalle“, in der man am Ende soviel drauf zahlt, das nie wieder erwirtschaften kann. Zur Ehrenrettung von Skriptsprache: Für Prototyping haben Python, Ruby(-on Rails) und Co ihre Berechtigung, aber für den produktiven Betrieb von Software, die einen längeren Lebenszyklus hat, ist C++ nach wie vor ökonomischer.

Jetzt  zu C++ und meinem Ausgangsproblem. In meiner eccp-View-Datei durchlaufe ich in einer for-Schleife eine Liste mit Objekten die eine Eigenschaft „Name“ haben. Diese Namen sind alphabetisch in der Liste vorsortiert. Ich will sie als Liste auf einer Website ausgeben.

Um die Liste etwas übersichtlicher zu gestalten, habe ich mir überlegt, dass ich für jeden Anfangsbuchstaben einen Abschnitt mit einer Überschrift anlegen möchte, so das alle Namen, die mit „A“ anfangen, die Überschrift „A“ bekommen und die mit „C“ beginnen eine Überschrift „C“ erhalten… Der Algorithmus ist recht überschaubar und leicht verständlich:


01| % string lastLetter = "";
02| % for ( unsigned int i=0; i<s_keywordTitlesCounts.size(); i++) {
03| %     if ( lastLetter != s_keywordTitlesCounts[i].Name.substr (0,1) ) {
04| %         lastLetter = s_keywordTitlesCounts[i].Name.substr (0,1);
05|          <h3 id="letter_" >
08| % } 

In Zeile 01 definiere ich eine Variable, in der ich den letzten Anfangsbuchstaben speichere. In Zeile 02 beginnt der Schleifendurchlauf. In Zeile 03 überprüfe ich, ob der Anfangsbuchstabe schon aufgetaucht ist. Die Methode ".substr(0,1)" gehört zur Klasse std::string und gibt das erste Zeichen des String zurück. Wurde der Anfangsbuchstabe noch nicht verwendet, wird er in Zeile 04 in der Variable "lastLetter" gespeichert. Danach wird in Zeile 05-07 die Überschrift angelegt.

Der Algorithmus funktioniert recht gut, bis ein Name mit einen Umlaut oder Sonderzeichen beginnt. Dann sieht man an der Stelle, an der das
Sonderzeichen stehen sollte ein ? oder ein anderes sonderbares Zeichen. Was ist passiert?

Ein "std::string" ist de facto ein "std::basic_string<char>" und der „char" ist der da runter liegende Basis-Datentyp. Der char-Type hat nur 1 Byte. Der Buchstabe „ä“ braucht jedoch in der utf-8 Form 2 Bytes, um gespeichert werden zu können, nämlich „\xc3\xa4„. Mit der string-Funktion ".substr(0,1)" wird aber nur das 1. Zeichen, bzw. das 1. Byte zurückgegeben, also der char, mit dem Bytewert „\xc3„. Das ist aber nur die Hälfte von dem Umlaut. Die zeichenweise Verarbeitung inklusive substr-Funktion macht bei einem std::string deshalb keinen Sinn, wenn man utf-8 verwendet. Genauso verhält es sich übrigens auch mit stringstreams.

Es gibt eine ganze Reihe von Lösungsansätzen für dieses Problem. Dabei kann auf die Boost-Lib zurückgegriffen werden;  oder C++11-Fatures genutzt werden;  das Qt-Framework hat ebenfalls eine eigene String-Implementiierung namens „QString„, die ich auch ziemlich cool finde und mit der sich das Problem in den Griff kriegen lässt.

Eine weitere Lösung für Unicode-Strings findet sich im cxxtools-Framwork. Dazu gibt es die „cxxtools::String", welche auf std::basic_string<cxxtools::Char> basieren. Und die „cxxtools::Char" können auch Unicode Zeichen repräsentieren und somit auch ein ‚ä‘ beinhalten. Mit der Funktion „cxxtools::utf8codec::encode(cxxtools::String)" bekommt man den Utf-8 Repräsentanten des Strings. Mit „cxxtools::utf8codec::decode(std::string)" wird ein utf-8-kodierten String in „cxxtools::String" konvertiert.

Wenn man Tntnet verwendet, hat man cxxtools automatisch dabei, von daher wäre es naheliegend cxxtools::String zu verwenden. Das sähe in meinem Beispiel so aus:


01| 
02|     #include <cxxtools/utf8codec.h>
03|     #include <cxxtools/string.h> 
04| 
[...]
05| %     cxxtools::String lastLetter;
06| %     cxxtools::String uName = cxxtools::Utf8Codec::decode( s_keywordTitlesCounts[i].Name );
07| %     if ( lastLetter != uName.substr (0,1) ) {
08| %         lastLetter = uName.substr (0,1);
09|          <h3 id="letter_" >
12| % } 

In Zeile 01-04 werden die nötigen Header-Datein eingebunden. In Zeile 05 kommt cxxtools::String zu ersten Mal zum Einsatz. In der nächsten Zeile 06 konvertiere ich den normalen C++-String in einen UTF8-Cxxtools-String. Jetzt bekomme ich auch in Zeile 10 den Wert bzw. den Buchstaben zurück,, den ich erwartet habe. Wird ein cxxtools::String auf einem std::ostream ausgegeben, dann wird er automatisch utf-8-kodiert. Genau das passiert in tntnet in den -Tags (Zeile 10).

Idealerweise verwendet man in der ganzen Applikation cxxtools::Strings. In meiner eigenen Anwendung ist mir jetzt aufgefallen, dass dieser Algorithmus ebenfalls nicht mit Umlauten funktioniert:


01| std::string OString::LowerCase ( std::string keywords ) {
02|     keywords = StrReplace ( "A", "a", keywords);
03|     keywords = StrReplace ( "B", "b", keywords);
04|     keywords = StrReplace ( "C", "c", keywords);
[...]
05|     keywords = StrReplace ( "X", "x", keywords);
06|     keywords = StrReplace ( "Y", "y", keywords);
07|     keywords = StrReplace ( "Z", "z", keywords);
08|     keywords = StrReplace ( "Ä", "ä", keywords);
09|     keywords = StrReplace ( "Ö", "ö", keywords);
10|     keywords = StrReplace ( "Ü", "ü", keywords);
11|     return keywords;
12| }
13| 
14| std::string OString::StrReplace (
15|     const std::string s_string,
16|     const std::string r_string,
17|     const std::string i_string
18| ) {
19|   int pos;
20|   // return string
21|   std::string b_string = i_string;
22|   while (true) {
23|     pos = b_string.find(s_string);
24|     if (pos == -1) {
25|       break;
26|     } else {
27|       b_string.erase(pos, s_string.length());
28|       b_string.insert(pos, r_string);
29|     }
30|   }
31|   return b_string;
32| }

Das Problem lauert hier wieder in Zeile 27-28. Alternativ könnte hier wieder cxxtools::String helfen. Ich habe jedoch gesehen, dass es noch eine undokumentierte Funktion namens CXXTOOLS_API Char cxxtools::toupper( const Char & ch) gibt. Möglicherweise brauche ich meinen eigenen Algorithmus gar nicht. Wenn ich es herausgefunden habe, werde ich es hier berichten.

Siehe auch:


Creative Commons Lizenzvertrag

Tntnet: Formular-Reloads verhindern

Standard

Es gibt mehrere Gründe dafür den Reload eines HTML-Formulars zu verhindern:

  • Man will vermeiden, dass durch einen Page-Reload der User versehentlich Daten mehrmals speichert und unter Umständen damit Duplikate anlegt. Das passiert typischerweise wenn Seiten sehr langsam reagieren und der Benutzer ungeduldig auf die F5-Taste drückt oder den Reload-Button des Browsers anklickt.
  • Ein anderes Szenario wäre, dass der User in verschiedenen Browser-Fenstern das selbe Formular geöffnet hat. Dadurch könnte der User ungewollt seine zuvor im ersten Fenster gespeicherten Daten mit den auf dem zweiten Fenster überschreiben. Oder die Datenänderungen beeinflussen sich sogar wechselseitig, weil die Formulardaten in der Session zwischengespeichert wurden.
  • Möglicherweise ist aber auch die Motivation möglichst wenig Angriffsfläche für Hacker zu bieten. Um ein Angriff in Form von Session Fixation oder Session Hijacking vorzubeugen.

Im Folgenden wird beschrieben wie eine Lösung dieses Problems aussehen könnte. Der Ansatz orientiert sich am MVC-Konzep, wie er hier im Blog schon und in den Artikeln aufgezeigt wurde:

Der View

Ausgangspunkt wird wahrscheinlich immer ein View mit einem Formular sein. Das Formular könnte etwar so aussehen (Auszug aus der ecpp-Datei):

<form method="post" >
    <$$  SessionForm::Manager::getFormToken( request )  $>
    <input
        type="hidden"
        name="arg_delete_account_id"
        value="<$ s_accountList[i_account].getID() $>" >
    <button name="arg_delete_account_button"
            value="pushed"
            type="submit">Löschen
    </button>
</form >

Die Besonderheit ist der Aufruf der statischen Funktion SessionForm::Manager::getFormToken(). Hier mit wird das Formular um ein automatisch generiertes Element erweitert. Request ist eine C++ Scoped-Variable, die zur Laufzeit in der ecpp-Umgebung, also dem View zur Verfügen steht. Diese Variable muss der Funktion getFormToken() mitgegeben werden. Der SessionForm::Manager wird den Token, den er in dem Webformular einbaut als session shared Variable ablegen, um später entscheiden zu können, ob die gesendeten Daten zu einem veralteten Formular gehören. Die Klasse SessionForm::Manager wurde von uns selbst erstellt. Der Aufbau der Klasse wird weiter unten erklärt.

Wird in einer Seite mit mehreren HTML-forms gearbeitet muss immer der selbe Token verwendet werden. Zum Beispiel so:

% std::string formToken = SessionForm::Manager::getFormToken( request );
<form method="post" action="EditAccount">
    <$$ formToken $>
    <i><b>"<$ s_accountList[i_account].getLogin_name() $>"</b></i>
    <input
        type="hidden"
        name="arg_edit_account_id"
        value="<$ s_accountList[i_account].getID() $>" >
    <button name="arg_edit_account_button"
            value="pushed"
            type="submit">Bearbeiten
    </button>
</form >
<form method="post" >
    <$$ formToken $>
    <input
        type="hidden"
        name="arg_delete_account_id"
        value="<$ s_accountList[i_account].getID() $>" >
    <button name="arg_delete_account_button"
            value="pushed"
            type="submit">Löschen
    </button>
</form >

Da mit jedem Aufruf der Funktion getFormToken() ein neuer, allein gültiger, Token generiert wird, werden mit jedem Aufruf alle vorige Token annulliert (ungültig gemacht). Natürlich kann man die Generierung des Token auch in den Controller verlegen und über TNT_REQUEST_SHARED_VAR an den View übertragen. Gerade bei komplexeren Applikationen wird das helfen, die Aufgaben zwischen den Code-Teilen zu strukturieren.

Manager

Die Implementierung der Manager-Klasse, die wir oben in den View-Code schon verwendet haben, könnte so aussehen:


std::string Manager::getFormToken( tnt::HttpRequest& request )
{
    TNT_SESSION_SHARED_VAR( std::string, SESSIONFORM_AVAILABLE_TOKEN, () );
    std::string token = genRandomToken(16);
    token += ":" + OString::IntToStr( request.getSerial() );
    std::string formTokenInput = "";
    SESSIONFORM_AVAILABLE_TOKEN = token;
    return formTokenInput;
}

std::string Manager::genRandomToken ( const int len) {
    std::string randomString = "";
    static const char alphanum[] =
        "0123456789"
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        "abcdefghijklmnopqrstuvwxyz";
    for ( int i = 0; i < len; ++i ) {
        int randNo = srand() % (sizeof(alphanum) - 1) ;
        randomString.push_back ( alphanum[randNo] );
    }
    log_debug( "randomString (Token): " << randomString );
    return randomString;
}

Interessant ist die Funktion request.getSerial(). Diese gibt nämlich eine fortlaufende Nummer zurück. Für jedem request wird eins hochgezählt. Damit wird verhindert, dass der große Zufall mal eintreten könnte, dass wir zweimal die selbe Zufallszahl haben.

Um die Qualität der Zufallszahlen zu erhöhen, sollte der folgende Funktionsaufruf in der main-Funktion (oder in einem anderen Teil des Codes der zu Initialisierung geeignet ist) stehen:

/* initialize random seed: */
srand (time(NULL));

Andernfalls wird man sich darüber wundern, das die „Zufallszahlen“ gar nicht so zufällig sind und damit die die Sicherheitsbemühungen wieder zunichte machen.

Routing

Was jetzt noch fehlt, damit die Komponente alle Formular-Daten überprüft, ist eine Route, die dafür sorgt, dass der Controller, der den Formular-Torken prüft, immer aufgerufen wird. Dazu muss die folgende Route gesetzt werden.

// controller rout for SessionForm token check.
app.mapUrl( "^/(.*)", "SessionForm::Controller" );

Sollen nur bestimmte Formulare-Seiten überwacht werden, muss die Route dementsprechend angepasst werden.

Weiterleitungen

Sollten Formulardaten von veralteten Seiten stammen, wird der Besucher auf die Fehler-Seite /SessionForm/NoAvailabeToken umgeleitet. Möchte man statt der Standard Seite eine eigene Seite gestalten, ist das Routing anzupassen:

// controller rout for SessionForm token check.
app.mapUrl( "^/SessionForm/NoAvailabeToken", "MyTokenErrorView" );

Controller

Der Controller der, die Verarbeitung prüft, könnte so aussehen:

unsigned SessionForm::Controller::operator() (
    tnt::HttpRequest& request,
    tnt::HttpReply& reply,
    tnt::QueryParams& qparam
) {
    TNT_SESSION_SHARED_VAR( std::string, SESSIONFORM_AVAILABLE_TOKEN, () );
    if ( qparam.has("SESSIONFORM_TOKEN") ) {

        std::string SESSIONFORM_TOKEN =
            qparam.arg<std::string>("SESSIONFORM_TOKEN");

        if ( SESSIONFORM_TOKEN != SESSIONFORM_AVAILABLE_TOKEN ) {
            return reply.redirect ( "/SessionForm/NoAvailabeTokenView" );
        }
    };
    return DECLINED;
}

Wer ein vollständiges Beispiel der Implementierung sehen will, kann sich den Code von meinem Projekt Peruschim auf GitHub anschauen.


Creative Commons Lizenzvertrag

Tntnet hinter einem Apache betreiben

Standard

Es gibt viele Gründe, die eigene Tntnet-Applikation hinter einem Apache betreiben zu wollen. Hier ein paar Beispiele die mir spontan einfallen:

  • Der Port 80 ist bereits von einer anderen Applikation belegt, die in einem Apache-Webserver läuft (klassischerweise PHP)
  • Das ssl/https-Handling möchte man lieber in einem Apache-Webserver abbilden, da sich das Personal damit besser auskennt.
  • Es soll eine Funktionalität vorgeschaltet werden, die als Apache-Modul implementiert wurde. Ich denke da z.B. an die Web Application Firewall ModSecurity
  • Das Load Balancing-Konzept basiert auf Apache (-Technologie)

Ein Revers Proxy für eine Tntnet-Applikation einzurichten, ist schnell erledigt. Aber zunächst noch kurz erklärt, was ein Revers Proxy macht. Der Revers Proxy in Apache nimmt die Anfragen für die Tntnet-Applikation entgegen und reicht die Anfrage an die Tntnet-Applikation weiter. Diese verarbeitet die Anfrage und gibt sie dem Apache zurück, der die Daten an den Browser ausliefert.

Die Tntnet-Applikation kann dafür jeden beliebigen Port verwenden und auf jedem beliebigen Server laufen, der für den Apache (dem Revers Proxy) erreichbar ist (also auch private IPs aus dem internen Netz). Wenn die Tntnnet-Anwendung auf dem selben Server wie der Apache läuft, kann Tntnnet nicht den Port 80 verwenden, das ist klar.

Hier ist eine Beispielkonfiguration. Die Konfiguration für den Revers Proxy wird am Besten in einer separaten Datei und nicht in die Hauptonfiguration von Apache abgelegt. Alle Dateien die auf .conf enden und auf dem Verzeichnis  /etc/httpd/ liegen, werden automatisch vom Apache gelesen. In diesem Beispiel werden zwei Szenarien abbildet: Es gibt zwei Applicationen,  die auf den selben Host laufen, jeweils auf Port 8001 und 8002. Auf Port 8001 soll die Tntnet-Applikation laufen und auf Port 8002 läuft noch eine Applikation in Tomcat (Jave-Applikation-Server).

Die Anfragen, die von Außen kommen und nach Domain „peruschim.de“ fragen, werden von dem Revers Proxy weitergeleitet auf "http://localhost:8001/". localhost:8002/ander-applikation. Wird wiederum nach "peruschim.de/ander-applikation" gefragt, wird der Revers Proxy die Anfrage auf "localhost:8002/ander-applikation" weiterleiten. Hier die vollständige Datei:

#NameVirtualHost *:80
#NameVirtualHost *:443

<VirtualHost *:80>
    ServerName peruschim.de
    ServerAlias www.peruschim.de

    ProxyPass /ander-applikation   http://localhost:8002/ander-applikation
    ProxyPassReverse /ander-applikation http://localhost:8002/ander-applikation

    ProxyPass /   http://localhost:8001/
    ProxyPassReverse / http://localhost:8001/

</VirtualHost>

<VirtualHost *:443>
    ServerName peruschim.de
    ServerAlias www.peruschim.de

    SSLEngine On
    SSLCertificateFile /etc/httpd/ssl/httpd.pem
    SSLCertificateKeyFile /etc/httpd/ssl/httpd.key

    ProxyPass /ander-applikation   http://localhost:8002/ander-applikation
    ProxyPassReverse /ander-applikation http://localhost:8002/ander-applikation

    ProxyPass /   http://localhost:8001/
    ProxyPassReverse / http://localhost:8001/</VirtualHost>

Im zweiten Abschnitt, der den Post 443 definiert, wird die ssl/https Verbindung konfiguriert,  wenn vom Browser das https-Protokoll über Port 443 angefordert wird, z.B die URL "https://peruschim.de" aufgerufen wird. In diesem Fall nutzt die Tntnet-Application und die Tomcat-Application das selbe Zertifikat. Die ssl-Verbindung wird hier von Apache gemanagt und nicht von Tntnet oder Tomcat. Das ginge zwar auch, aber dann müsste die ssl-Verwaltung mit drei völlig unterschiedlichen Systemen und Konzepten einrichtet werden.

Das hier gezeigte ist erweitertes Admin-Wissen. Da Entwickler selten mit dem Betrieb ihrer Application betraut sind, würde ich dieses Wissen nicht als selbstverständlich voraussetzen. Es gibt noch einen wichtigen Punkt, den vor allem Entwickler wissen müssten, damit ihre Application hinter einem Revers Proxy funktionieren kann. Bei ihren internen Links (die auf Ziele innerhalb der Application zeigen) dürfen sie keine IPs oder Domains verwenden, die nicht von außen erreichbar sind. Wenn man also absolute Pfade wie "http://localost/irgendwas" oder "http://127.0.0.1/irgendwas" verwendet, kann dies Browser aus dem Internet (per DNS) nicht auflösen. Es müssen also relative Pfade wie „/irgendwas“ verwendet werden. Möglich ist es auch, die korrekte Domain zu verwenden. Also wenn wir beim obigen Beispiel bleiben, wäre dies: "http://www.peruschim.de/irgendwas".  Wenn die Tntnet-Applikation selbst nicht weiß, wie sie für das Internet hinter dem Revers Proxy heißt, wird das keine Option sein. Zudem wäre die Lösung sehr unflexibel. Der Domainname könnte nicht geändert werden, ohne den Code anzupassen.

Man kann auch versuchen, den Reverse Proxy in den auszuliefernden HTML-Code schauen zu lassen und die IPs/Domains auszutauschen, die nur Intern bekannt sind. Das ist aber umständlich und fehlerträchtig. Mein Rat an die Entwickler: besser immer relative Pfade verwenden! Für die Admins: wer diesen steinigen Weg aber trotzdem gehen will/muss, der sei auf diesen Artikel über mod_proxy_html verwiesen.


Creative Commons Lizenzvertrag

MVC-Architektur in Tntnet (Variante II.)

Standard

Wie schon im Artikel „MVC-Architektur in Tntnet (Variante I.)“ angekündigt, kommt hier eine alternative Variante, um eine MVC-Architektur in Tntnet zu realisieren. Diese Variante ist etwas einfacher in der Handhabung. Dafür stellt sie eine nicht ganz 100%-ig saubere Lösung dar. Denn hier wird erst der View aufgerufen; dieser ruft den Controller auf, der wiederum mit dem Model arbeitet. Klassischer Weise wird erst der Conroller und dann der View aufgerufen. Ein weitere Punkt ist sowohl ein Vor- wie ein Nachteil. Der Aufruf des Controllers wird nicht über das Routing gesteuert, da der View den Controller aufruft. Der Vorteil ist, dass sich eine einzige Route mit der Komponente verbindet. Das macht es einfacher und transparenter über URLs Zugriffsrechte zu kontrollieren oder generische Komponenten-Link-URLs zu verwenden. Dazu zu einem späterem Zeitpunk mehr. Jetzt erst mal zur konkreten Umsetzung der alternativen MVC-Variante.

Contoller

Die Controller-Klasse ist eine ganz normale C++-Klasse. Diese Klasse wird von dem View, der in der ecpp-Auszeichnungssprache erstellt wird, eingebunden. Bevor gezeigt wird, wie der View die logische Verarbeitung der Benutzereingaben an den Controller delegiert, wird hier erst mal ein exemplarischer Controller-Code gezeigt. Der gezeigte Controller soll die Kontrolle über den Login-Prozess übernehmen: Die Datei AlterLogInController.h:

#ifndef ALTERLOGINCONTROLLER_H
#define ALTERLOGINCONTROLLER_H

#include <Core/models/UserSession.h>
#include <tnt/httprequest.h>
#include <tnt/httpreply.h>
#include <iostream>

    class AlterLogInController
    {
        public:
            AlterLogInController (UserSession& userSession_): userSession(userSession_){};
            void operator() (
                tnt::HttpRequest& request,
                tnt::HttpReply& reply,
                tnt::QueryParams& qparam
            );
            std::string feedback;
            UserSession& userSession;
    };
#endif

Und die Datei AlterLogInController.cpp:

#include <Core/controller/AlterLogInController.h>
#include <Core/manager/WebACL.h>
#include <Core/models/UserSession.h>
#include <cxxtools/log.h>
#include <tnt/httprequest.h>
#include <tnt/httpreply.h>

log_define("Core.AlterLogInController")

void AlterLogInController::operator() (
    tnt::HttpRequest& request,
    tnt::HttpReply& reply,
    tnt::QueryParams& qparam)
{

    // define the query parameters
    std::string  arg_name     = qparam.arg<std::string>("arg_name");
    std::string  arg_password = qparam.arg<std::string>("arg_password");
    bool  arg_login_button    =  qparam.arg<bool>("arg_login_button");

    log_debug("authUser(" << arg_name << ", ***)");

    if ( arg_login_button ) {
        if ( WebACL::authUser ( arg_name, arg_password ) )
        {
            userSession.setUserName ( arg_name );
            userSession.addRoll (  WebACL::getRoll ( arg_name ) );
            reply.redirect ( "/home" );
        }
        else
        {
            log_debug("fail");
            feedback = "Login fehlgeschlagen!";
        };
    }
}

Das einzig Besondere an der Klasse ist, dass sie eine Methode "operator()" aufweist, der bestimmte Referenzen übergeben werden. Das sind Objekte der Klassen tnt::HttpRequest, tnt::HttpReply und tnt::QueryParams die in der ecpp-View-Umgebung zur Verführung stehen. Dem Controller werden die Referenzen auf diese Instanzen übergeben, damit dieser mit den Werten und Informationen weiter arbeiten kann. So z.B. stecken in tnt::QueryParams& qparam die übergebenen http-request-Parameter mit ihren Werten. In unserem Fall ist das der Name, das Passwort und ob der Button,  für das Abschicken des Login-Formulars geklickt wurde. Die Art wie die Parameter aus den übergebenen Objekt-Reverenzen heraus gelesen werden, ist die selbe wie im Artikel „MVC-Architektur in Tntnet (Variante I.)“ beschrieben. Nur ein kleiner Hinweis auf die Besonderheit des Funktion-Aufrufes reply.redirect ( "/home" );. Diese Funktion der Klasse tnt::HttpReply wird vm Tntnet-Framework bereitgestellt und dient dazu, auf eine andere Seite weiter zu leiten. In unserem Fall auf die Startseite /home. Wird der Login abgelehnt,  so wird eine Nachricht in der Klasseneigenschaft feedback für den View (/home) bereitgehalten. Dies geschieht in der Zeile

feedback = "Login fehlgeschlagen!";

Model

Für das Model gilt in dieser Variante II das Gleiche, wie in der I. Variante.

View

Im View gibt es bei dieser Variante einen entschiedenen Unterschied zur I. Variante:

<%session
    scope="shared"
    include="Core/models/UserSession.h" >
        UserSession userSession;
</%session>
<%request
    scope="shared"
    include="Core/controller/AlterLogInController.h">
        AlterLogInController  alterLogInController(userSession);
</%request>
<%cpp>
    alterLogInController.operator(
        request,
        reply,
        qparam
    );
</%cpp>

<!DOCTYPE HTML>
<html>
<head>
    <meta charset = "UTF-8" />
</head>
<body>
        <form method="post" >
            <h2>Login</h2>
% if ( alterLogInController.feedback != "" ) {
            <div class="feedback-box">
                <b><$ alterLogInController.feedback $> </b>
            </div>
% }
            <p>Für den gewählten Bereich muss du angemeldet sein. Bitte
            authentifiziere dich...
            </p>
            <p>Benutzer:<br>
                <input
                    class="full-size"
                    name="arg_name"
                    type="text"
                    size="40"
                    maxlength="40">
            </p>
            <p>Passwort:<br>
                <input
                    class="full-size"
                    name="arg_password"
                    type="password"
                    size="40"
                    maxlength="40"></p>
            <p>
                <button name="arg_login_button"
                            value="pushed"
                            type="submit">Login
                </button>
            </p>
        </form>
        </p><a href="NewAccount">Hier</a> kannst du dir ein Account erstellen</p>
</body>
</html>

Dem Konstruktor der Klasse AlterLogInController wird die Instanz der Kasse UserSession als Parameter mitgegeben. Die Klasse AlterLogInController braucht die Klasse UserSession nämlich, um die Information über einen erfolgreichen Login zu hinterlegen, damit andere Komponenten auf diese Information zugreifen können. In dem Abschnitt,der mit dem Tag <%cpp> umschlossen ist, wird der Controller dazu aufgefordert den Request entgegen zu nehmen und zu verarbeiten. In den <%cpp>-Tags kann man ganz normalen C++-Code einbetten. Weiter unten im ecpp-Code werden die Controller-Eigenschaften genutzt, um den View mit generischen Werten zu befüllen.

% if ( alterLogInController.feedback != "" ) {

Routing

Damit der View unter der gewünschten Rute vom Browser gefunden wird, muss diese noch gesetzt werden: alterLogInController.feedback app.mapUrl( "^/LogIn", "LogInView" ); Jetzt wird das Loginfenter unter http://DieDomain.de/LogIn zu erreichen sein. Es ist möglich mehrere Routen zu ein und der selben Komponente zu definieren. Allerdings sollte man sein ACL-Konzept nicht auf Basis von URLs realisieren, weil man dann u.U. seine eigenen ACLs ungewollt austrickst. Die Regel „halte es einfach“ ist bei dem Thema Routing sicher zu empfehlen.


Creative Commons Lizenzvertrag

MVC-Architektur in Tntnet (Variante I.)

Standard

MVC und Tntnet

Tntnet macht dem Entwickler keine Vorschrift, wie er sein Projekt zu organisieren hat oder welche Paradigmen zur Anwendung kommen sollen. Tntnet konzentriert sich auf die Basisfunktionalität. Wenn eine höhere Abstraktion gewünscht ist, muss der Entwickler diese selber implementieren. C++ gibt es lange bevor es das heute sehr popoläre model view controller(MVC) pattern gab.  C++ wird es höchstwahrscheinlich noch geben, wenn das MVC-Pattern durch andere Konzepte abgelöst oder weiterentwickelt wurde. Das ist der Grund, warum das MVC auch nicht fester Bestandteil von Tntnet ist. Das heißt aber nicht, dass MVC nicht mit Tntnet gehen würde. Hier zeige ich eine denkbare Implementierung von MVC in Tntent.

Der Controller

Die Controller-Klasse wird von tnt::Component abgeleitet, und muss eine Funktion „operator()“ implementieren:

class MyCopmonentController : public tnt::Component
{
    public:
        unsigned operator() (
            tnt::HttpRequest& request,
            tnt::HttpReply& reply,
            tnt::QueryParams& qparam
        );
};

Da die Klasse kein Interface hat über die sie angesprochen wird, entfällt die Header-Datei. Es ist nur eine *.cpp erforderlich. Mit „qparam.arg<TYPE>(KEYWORD)“ kann ein Argument ausgelesen, das dem HTML-Requeset mitgegeben wurde. Als Beispiel:

// URL arguments
std::string arg_login_name =
    qparam.arg<std::string>("arg_login_name");

„TYPE“ ist der variable Type, den man zurück bekommen möchte. „KEYWORD“ ist der Bezeichner, mit dem der Wert übergeben wird. Möchte man eine Liste von Werten zurückbekommen, (z.B. aus Listen in HTML-Formularen, in denen eine Mehrfachauswahl erlaubt ist), muss eine andere Funktion genutzt werden. Diese heißt „args“ statt „arg“ und gibt einen Vector von Typen zurück, den man angibt:

std::vector<std::string>  args_userroles =
    qparam.args<std::string>("args_userroles");

Um nicht mit Argumenten und shared Variablen durcheinander zu kommen, kann es hilfreich sein, sich auf die Konvention zu einigen, dass Argumente mit den Präfix „arg_“ beginnen. Die Namen in den HTML-Formularen (bzw. der View in den *.ecpp-Datein) müssen natürlich der gleichen Konvention folgen.

<p>
 <label for="login_name">Login*:</label>
 <br>
 <input 
    class="full-size" 
    name="arg_login_name" 
    type="text" 
    value="<$ accountData.getLogin_name() $>" 
    maxlength="80"> 
</p>

Empfohlene Argumenten Typen

Hier eine tabellarische Übersicht, welcher C++-Cast für welchen HTML-Typ sinnvoll ist.

HTML-Type C++-Type
button bool
input/text string
input/password string
input/number int, long, short…
input/checkbox bool
select/multiple vector

Gemeinsam genutzte Variablen

Um Werte an den View zu übergeben, nutzt man shared-Opjekte und -Variablen. Diese müssen mit einem Macro registriert und initialisiert werden:

 
// shared variables
TNT_REQUEST_SHARED_VAR( UserSession, s_userSession, ());

Der erste Parameter ist der Typ; der zweite Name und der Dritte ist der aufzurufende Constructor. Wenn dieser einen Parameter braucht, kann dieser hier angegeben werden. Es empfehlt sich für  die Übersicht die Namenskonvention zu verwenden, die shared Variablen ein „s_“ als Präfix voranstellen.

Es gibt für die shared Opjekte verschiedene Gültigkeitsbereiche bzw. Lebensdauer. So werden über „TNT_SESSION_GLOBAL_VAR“ die Objekte die gesamte Session überdauern. Es gibt noch „TNT_REQUEST_SHARED_VAR“. Hier haben die Objekte nur eine Lebensdauer für ein Request. Es ist ratsam mit „TNT_SESSION_GLOBAL_VAR“ sehr sparsam umzugehen und wenn möglich, nur mit „TNT_REQUEST_SHARED_VAR zu“ arbeiten. Andernfalls kann es zu ungewollten Effekten kommen, wenn Objekte von einer vorigen Request-Prozedur noch mit einen altem Wert belegt sind.

Damit der Controller tatsächlich beim Routing berücksichtigt wird, muss die Klasse der Component-Factory bekannt gemacht werden:

static tnt::ComponentFactoryImpl<MyCompController>
     factory("MyCompController");

Model

Im Controller (und später im View) wird eine Klasse UserSession als shared variables verwendet. Es ist im Prinzip möglich jeden Typ, also auch primitive Typen wie int zu benutzen. Davon ist jedoch abzuraten, weil es keine Namensräume gibt, die die einzelnen Komponenten trennen können, wenn sie von der Tntnet-Component-Factory verwaltet werden. Die Wahrscheinlichkeit bei sehr großen Projekten aus Versehen einen Variablennamen zu verwenden, der schon an anderer Stelle verwendet wird, ist sehr groß.

Kapselt aber jede Komponente (logische Einheit einer Website) ihre Session-Daten in eine eigene Klasse (bzw. Model) – wie hier – können verschiedene Komponenten, die selben Variablennamen haben, verwenden. Durch die Verwendung unterschiedlicher Typen (Klassen) werden sie von Tntnet als unterschiedliche Variablen verwendet. Zur Veranschaulichung:

Ist in dem Controller der Komponente A folgender Code zu finden

TNT_SESSION_SHARED_VAR(CompA::SessionShareds, sessionInfo, ());

und in dem Controller der Komponente B folgender

TNT_SESSION_SHARED_VAR(CompB::SessionShareds, sessionInfo, ());

so benutzen beide Komponenten zwar den gleichen Instanznamen aber unterschiedliche Klassen-Typen, so dass sie sich nicht gegenseitig ihre Werte überschreiben.

View

Der View wird in Tntnet mit einer erweiterten HTML-Auszeichnungssprache namens epcc erstellt und vom Precompiler ecppc in C++-Code umgewandelt. Ich werde bestimmt noch in einem späteren Artikel detaillierter auf diese Aufzeichnungssprache eingehen. Bis dahin sei vorerst noch auf die offizielle eng. Doku verwiesen: http://www.tntnet.org/man7/ecpp.7.html An diese Stelle gehe ich erst mal nur soweit darauf, ein wie es das MVC-Thema berührt.

Damit die shared Variablen des Controllers auch dem View zur Verfügung stehen, müssen diese der View-Umgebung bekannt gemacht werden. Das geschieht auf die folgende Weise:

<%session
    scope="shared"
    include="models/UserSession.h">
        UserSession s_userSession;
        std::vector<std::string> s_allRolls;
</%session>

<%request
    scope="shared">
            std::vector<std::string> sh_allRolls;
</%request>

Mit dem scope-Wert „shared“ wird angezeigt, dass es sich um shared Variablen handelt. Mit „include“ können benötigte Header-Dateien eingebunden werden. In diesem Fall includieren wir die Klasse „UserSession“. Diese wird gebraucht, damit der Type „UserSession“ dem Compiler bekannt ist. Zwischen den Tags werden die eigentlichen Variablen aufgelistet bzw. bekannt gemacht. In dem Beispiel sieht man auch, dass hier bei der Lebensdauer der shared Variablen differenziert wird. Im Tag „session“ kommen alle Werte, die zuvor mit dem Makro „TNT_SESSION_GLOBAL_VAR“ deklariert wurden. In „request“ kommen alle Variablen, die in dem Controller „TNT_REQUEST_SHARED_VAR“ initialisiert wurden.

Routing

Damit der View und der Controller tatsächlich gemeinsam eine Anfrage bearbeiten, müssen sie noch mit einer gemeinsamen Route verknüpft werden.

    app.mapUrl( "^/(.*)$", "$1Controller" );
    app.mapUrl( "^/(.*)$", "$1View" );

Diese Regel sagt aus, dass jede URL einmal um „Controller“ und einmal um „View“ ergänzt wird, und damit zuerst der Conntroller und dann der View aufgerufen wird. Lautet nun unser Controller z.B. „MyCompController“ und der Viel „MyCompView“, so wird die neue Kompnent über die URL „MyComp“ aufgerufen.

Sicherheitsaspekt

Der View sollte keinerlei Logik enthalten. Sollte einmal wegen eines Fehlers oder einer Fehlkonfiguration der Controller nicht aufgerufen werden, liefert der View trotzdem seinen Inhalt ab. Wenn die Zugriffsrechte im Controller realisiert sind, greifen diese nicht mehr und der View könnte Daten preisgeben, die nicht für den Besucher gedacht sind. Deshalb sollte IMMER der Controller für die Befüllung mit Inhalt und Daten zuständig sein.

Troubleshooting

Der Controller beim MVC wird (scheinbar) nicht aufgerufen

Problem: Man hat eine Komponente mit dem MVC-Konzept erstellt, aber der Controller scheint nicht aufgerufen zu werden.

  • Überprüfen, ob das wirklich zutrifft, indem z.B. eine Testausgabe mit std::cout auf die Standartausgabe ausgegeben wird
  • Kontrollieren, ob die Route stimmt, die über app.mapUrl() oder über das Konfigurations-File von Tntnet gesetzt wurde
  • Prüfen, ob der Name des Controller korrekt in der Component-Factory angegeben wurde: static tnt::ComponentFactoryImpl factory(„MyController“);
  • Kontrollieren, ob die Komponente compiliert und gelinkt wurde, indem das Makefile überprüft wird
  • Prüfen, ob ggf. form-Tag im View  die richtige Componente aufruft

Nachwort

Es werden sich schon einige Leser Gedanken gemacht haben, warum im Titel „(Variante I.)“ steht. Tntnet gibt den Entwickler die Freiheit, die Dinge so zu tun, wie man sie für richtig hält. Was „richtig“ ist, da gehen die Meinungen meist auseinander. Deshalb gibt es auch noch andere Lösungen. Ich werde noch in ein weiteres MVC-Pattern für Tntnet vorstellen. Auch das Thema „Verzeichnisstruktur in Tntnet-Projekten“ werde ich noch ein mal separat behandeln.


Creative Commons Lizenzvertrag