The MVC Framework is an action-based web-framework, which does not want to reinvent the wheel, but builds on top of existing and proven technologies. With focus on the Request-Response cycle, where we have free choice in how we want to handle it, it combines technologies like JAX-RS and CDI and new MVC features to reach a given goal:
To build a lightweight web-framework, which is action-based, provides the functionality to work on a lower level of abstraction and to cover non-standard conditions.
Hereinafter we will show an example-application, deployed on a Wildfly 15.0 application-server, based on the reference implementation of MVC 1.0 API, aka Krazo.
The MVC-Archetype we are using is available at the MVC-Spec website. Since we are working with a Wildfly application server, we need the RESTEasy implementation of JAX-RS. At First, let’s look at the class model to get an overview of our application.
The following is our project structure:
After building a project from the archetype, we create the web.xml file in the src/main/webapp/WEB-INF folder and define a welcome page to show when the base URL is called.
The MVC-API provides a wide field of possible View-technologies. With Krazo we can use different views like Thymeleaf, Jade, StringTemplate, Facelets and many more. JSP is the View technology of choice for this project. The *.jsp-file that represents our welcome-page is the entry point to our application, hence it is enough if it only contains a link to the registration page (more specific „mvc/register“). Just create the file within the webapp folder.
Initial Configuration:
Before we can launch the application, let’s do some configuration first. The Archetype created a class in the “controller” package which contains one method. It additionally created a class “App” that extends javax.ws.rs.core.Application in package “app”. We now could override the getClasses()-method and add the AppController class to the Set we need to return (as shown in figure 3), but this is no longer explicitly needed in final release.
Next, we create a simple JSP that contains a form asking for lastname, birthdate (type=’date’) and gender (radio buttons).
This .jsp-file should be in src/main/webapp/WEB-INF/views since this is the default location specified by MVC 1.0.
The AppController
In this class a method is declared with the @GET annotation which we will modify like this:
If our link (<a href=“mvc/register“>) now sends us from the welcome page to the registration page, it triggers a GET-Request, which will be handled by the corresponding Controller in the GET-method and returns the registration View. The View can be returned as String, as it is in our example, as Response object or if the GET-method is specified as void, we must add the @View annotation like this: @View(“registration.jsp”). The View Annotation can be placed on class level as well as on single methods.
Figure 3 also shows three Fields initialized by Dependency Injection. We will come back to this later.
UserForm
Now we need a Bean, to convert and validate our user data. Therefore, we create a simple POJO UserForm like this:
The @MvcBinding annotation applies MVC specific rules on JAX-RS and Bean Validation annotations also stated for the field. If the validation fails, it continues validating and collects all validation errors in the BindingResult object mentioned before to process them in our Controller (e.g. transporting them to the View as error-messages).
The Annotation @FormParam(<formComponentName>) takes the name (attribute name=’<name>’ within the input-tag in our JSP) of the associated input tag. This enables the field to bind the relevant data. It is possible to use even more Annotations to validate the given data. Hence this is not a MVC technology, we will not dig into these annotations, if you are interested in this topic please refer to the Java Bean Validation documentation.
Conversion
As some have already noticed, the variable date is of type WrapperLocalDate. To convert String from the Request to the correct datatype, one of three conditions must be met:
- A constructor taking a single String as argument.
- The method .valueOf(String s) taking a single String as argument (as in Integer.valueOf(“5”) )
- The method .fromString(String s) taking a single String as argument
When met, this grants us conversion without us having to do anything about it. If we now specify the gender as Enum and use the Enums as Value-Attributes of the radio buttons from our form, we can simply convert the Strings to Enums.
But the LocalDate does not meet any of these conditions. To fix that, we can build a wrapper class around the LocalDate and add a Constructor that takes a single String as argument.
This is just a quick and dirty way of solving this problem. One other solution would be to write a ParamConverterProvider which returns a ParamConverter to convert our parameter. But this solution works quite well in this example.
The Named Bean User
The Named Bean User is a Backing-Bean which contains all fields our UserForm has, the only difference is: We now use the LocalDate without wrapper. The Bean User is RedirectScoped, this means it will be alive until the Post-Redirect-Get is done. This allows us to access the data in our View with Expression Language.
This Bean is injected in the AppController for later use in the POST-method.
The AppController also contains a field BindingResult result and Models models. Models is a Map, meant to transport data to the View, without creating a Bean. The BindingResult object allows us to access validation errors so we can process them.
The Post method in the AppController class
In the AppController we create a @POST method processForm(…). There are two annotations in the arguments.
The Annotation @BeanParam belongs to JAX-RS and @Valid is part of BeanValidation. These annotations initiate data injection and validation. Transmitted data will be extracted from the Request, converted and validated while binding.
If validation errors occur, the validation is failed. We can identify a failed validation by invoking the getter method isFailed() from the BindingResult object. After a failed validation we can access the ParamError objects by calling the BindingResult method getAllErrors(), it returns a Set<ParamError> object. In our example we write the names of the fields where a validation error happened and the error messages in the Models Map for later use in the View. The names of the fields can be used as keys and the messages can be put as values.
Accessing these errors messages from the View can look like this:
As long as the key is not existing, the output is empty. If there is a validation error and it is put in the map, we see the message displayed in the View.
Back in the AppController post method we return the registration.jsp to display the validation errors. While we can apply many validation annotations to our fields, we recommend to use as much client-side validations as possible to avoid unnecessary traffic between client and server.
If the validation finished without errors, we assign all values from the userForm-bean to the CDI injected user-bean. As a return-value we build a String performing a redirect:welcome to redirect to the welcome page and avoid multiple Post-requests when reloading the welcome page. We now need a new Controller responsible for the path /welcome.
The MessageController class
The new Controller will be matched for the new Get request and returns the personalizedWecome.jsp as View. This time the method has return type void and uses the @View(“view.xy”) annotation.
The @GET method has return type void, hence we need to add the @View-annotation to specify a standard View to display. We now only need to write the View to return. When writing this JSP-file we can access the redirectScoped named bean user via Expression Language and display a personalized welcome message containing the name, the gender and the birthdate. Our newly created View should be located at the views folder like the ones before. That’s it! We now can deploy and test the Application.
Cross-Site-Request-Forgery Protection
Another Feature of MVC 1.0 is for security purpose, more specific to protect against Cross-Site-Request-Forgery. MVC 1.0 introduces the @CsrfProtected annotation which is used on method tier and is mainly meant for Post methods. If this annotation is used, we expect a token to be send with our form data. If the token is not valid or not existing, the request will be denied. Therefore, we need to add a hidden field in the View, like this:
The token is generated while runtime and the View can access it with Expression Language. If somebody catches our request and sends his own request without the generated token, the connection is denied.
Have Fun!
For better understanding, the same in German:
MVC 1.0 Framework
Das MVC Framework ist ein aktionsbasiertes Web-Framework, das das Rad nicht neu erfinden will, sondern auf bestehenden und bewährten Technologien aufbaut. Das Zentrum bildet der Request/Response Zyklus, bei dessen Bearbeitung uns möglichst viel Freiheit gelassen wird. Hierfür werden Technologien wie JAX-RS und CDI verbunden und mit neuen MVC-Funktionen verfeinert, um ein gesetztes Ziel zu erreichen:
Ein leichtgewichtiges Web-Framework zur Verfügung stellen, welches aktionsbasiert ist und uns die Möglichkeit gibt, in Details einzugreifen und Anforderungen abzudecken, die nicht Standard sind.
Im nachfolgenden werde ich eine kleine Beispielapplikation vorführen, die auf einem standardisierten Wildfly 15.0 Application-Server deployed wird und auf der Referenzimplementierung der MVC-API, namens Krazo basiert.
Das MVC-Archetype ist auf der MVC-Specification Webseite zu finden. Für den Wildfly wird die JAX-RS Implementierung RESTEasy verwendet. Zu Beginn zeige ich euch eine Übersicht über die Klassen, die ich in meinem Beispiel erstellen werde.
Mit folgender Ordnerstruktur:
Nachdem ein Projekt aus dem Archetype erzeugt wurde, müssen wir im Ordner src/main/webapp/WEB-INF die web.xml anlegen, und darin eine Startseite bei Aufruf der Base-URL festlegen.
Das MVC Framework bietet uns ein breites Spektrum an möglichen View-Technologien. Über Krazo können wir verschiedene View-Technologien verwenden, wie Thymeleaf, Jade, StringTemplate und weitere. Für dieses Beispiel beschränke ich mich allerdings auf JSPs. Das *.jsp-File, das unsere Willkommensseite darstellt, ist unser Einstiegspunkt in die Applikation. Für das Beispiel genügt es, wenn die Startseite lediglich einen Link zum Registrieren enthält (also die URL „mvc/register“). Wir können dieses File einfach in dem webapp Ordner ablegen.
Erstkonfiguration:
Bevor wir nun unser Programm starten können, müssen ein paar Änderungen vorgenommen werden. Der Archetype hat uns im Package „controller“ eine Klasse angelegt, die eine einzige Methode besitzt. Außerdem hat uns der Archetype im Package „app“ eine Klasse angelegt, die von javax.ws.rs.core.Application erbt. Diese Klasse können wir so lassen wie sie ist, in früheren Versionen mussten wir hier die getClasses()-Methode überschreiben und unsere Controller als JAX-RS-Ressourcen bekannt machen, wie es im nächsten Bild zu sehen ist. Im Final-Release ist das nun nicht mehr notwendig.
Im nächsten Schritt bauen wir uns eine JSP mit einem Formular das die Daten Name, Geburtsdatum (der Einfachheit halber als input type=“date“) und Geschlecht (über Radiobuttons) abfragt.
Diese .jsp Datei soll im Verzeichnis src/main/webapp/WEB-INF/views liegen. Dieser Ordner ist die Standard Ablage für Controllerverwaltete JSPs.
Der AppController
In der Controllerklasse AppController befindet sich eine Methode mit der Annotation @GET. Diese schreiben wir wie folgt um:
Wenn nun unser link (<a href=“mvc/register“>) von der Willkommensseite uns weiterleitet und einen GET-Request auslöst, wird dieser vom Controller in der entsprechenden @GET-Methode bearbeitet und gibt die registration.jsp als View zurück. Die View kann als String, wie in diesem Beispiel, als Response-Objekt oder bei void-Methoden über die @View-Annotation zurückgegeben werden.
Auf dem Bild sind zusätzlich noch drei Felder zu sehen die per Dependency Injection initialisiert werden. Auf diese werden wir später zurückkommen.
UserForm
Denn vorher brauchen wir eine Bean, um unsere User Daten zu konvertieren und zu validieren. Hierfür erzeugen wir ein POJO namens UserForm, nach folgenden Spezifikationen:
Die Annotation @MvcBinding wendet MVC-spezifische Regeln auf die JAX-RS und Bean-Validation Annotationen, die zusätzlich für das Feld verwendet werden, an. Wenn Validierungsfehler auftreten, können wir über das injizierte BindingResult Objekt im Controller auf die Fehler zugreifen und sie verarbeiten (in Form von Fehlerrückgaben in die View oder ähnlichem).
Die Annotation @FormParam(<formComponentName>) bekommt den Namen (der über das Attribut name=‘<name>‘ im input-Tag der JSP festgelegt wurde) der zugehörigen Input-Komponente übergeben. Damit schlagen wir eine Brücke zwischen dieser Klasse und unserem Formular. Es können weitere Validierungsannotationen von Java Bean Validation verwendet werden, um die eingegeben Daten zu validieren. Da es sich hierbei nicht um eine konkrete MVC-Technologie handelt, werden wir auf diese Annotationen nicht weiter eingehen, Interessierte können sich darüber in der Java Bean Validation Dokumentation informieren.
Konvertierung
Wie einigen bestimmt schon aufgefallen ist, ist die Variable date vom Typ WrapperLocalDate. Um Strings aus unserem Request in den richtigen Datentyp konvertieren zu können, benötigt unser Objekt eines von drei Fähigkeiten:
- Einen Konstruktor der einen String als Übergabeparameter annimmt
- Die Methode .valueOf(String s) wie bei Integer.valueOf(„5“)
- Die Methode .fromString(String s)
Auf diese Weise werden unsere Strings in die korrekten Datentypen konvertiert. Wenn wir folglich das Geschlecht als Enum angeben und die Value-Attribute der Radio-Buttons unseres Formulars entsprechend setzen, können wir diese Werte problemlos übernehmen.
Das LocalDate hat keine dieser Fähigkeiten. Um darauf zu reagieren, können wir uns eine Wrapperklasse um das LocalDate bauen und einen Konstruktor hinzufügen der einen String als Parameter akzeptiert.
Dieses Problem so zu lösen ist nicht die Regel. Eine andere Lösung wäre einen ParamConverterProvider zu schreiben welcher einen ParamConverter zurückgibt, um unseren Parameter zu konvertieren. Allerdings funktioniert der Wrapper in unserem Beispiel genauso gut.
Die Named Bean User
Als nächstes erzeugen wir noch eine Backing-Bean die alle Felder unserer UserForm enthält, der einzige Unterschied: Wir verwenden das LocalDate jetzt ohne den Wrapper. Die Bean User ist RedirectScoped, das bedeutet sie existiert während der Weiterleitung und bis der Get-Request auf im Ziel-Controller abgeschlossen ist. Hiermit können wir auf die Daten der Named-Bean von der Ziel-JSP aus zugreifen.
Diese Bean wird in unseren Controller injiziert und später von uns in der POST-Methode verwendet.
Des Weiteren besitzt unser AppController noch die Felder BindingResult results und Models models. Models ist eine Map, die dazu gedacht ist, Daten an die View weiterzugeben, ohne dafür eine Bean zu nutzen. Über das BindingResult-Objekt können wir auf Validierungsfehler zugreifen und diese verarbeiten.
Die Post Methode in der AppController-Klasse
Als nächstes konzentrieren wir uns auf die @POST-Methode processForm(…) in unserem Controller. In den Übergabeparametern befinden sich zwei Annotationen.
Die Annotation @BeanParam gehört zu JAX-RS und die Annotation @Valid ist Teil der BeanValidation API. Diese beiden Annotationen leiten die Daten Injektion und Validierung ein. Die Daten werden automatisch über die in der UserForm Klasse verwendeten Annotationen aus dem Post-Request ausgelesen, konvertiert und validiert.
Wenn bei der Validierung Fehler auftreten, gilt die Validierung als gescheitert. Das können wir über das BindingResult-Objekt mit der Getter-Methode isFailed() abfragen. Bei einer gescheiterten Validierung können wir nun die gesammelten ParamError-Objekte als Set über das BindingResults-Objekt mit der Methode getAllErrors() erhalten. Im Beispiel schreiben wir jeden Fehler in unsere Models Map, um aus der View heraus auf die Fehler und Fehlernachrichten zugreifen zu können. Hierfür verwenden wir die Namen der Komponenten wieder als Schlüssel.
Bei unserem Input Komponenten Namens ‚date‘ sähe das dann so aus:
Solange kein Schlüssel ‚date‘ in der Models Map vorhanden ist, bleibt die Ausgabe leer. Im Falle eines Validierungsfehlers, befüllen wir die Map und eine Fehlernachricht wird in der View angezeigt.
Zurück in der Controller @Post-Methode geben wir dann die derzeitige View (die registration.jsp) zurück, da die Validierung und somit die Registrierung gescheitert ist. Wir raten allerdings dazu den größten Teil der Validierung Clientseitig zu erledigen, um unnötigen Traffic und nicht Zielführende Anfragen an den Application-Server zu vermeiden.
Wenn die Validierung fehlerfrei ablaufen konnte, befüllen wir alle Felder unserer injizierten Named Bean mit den konvertierten und validierten Werten, die uns über das Formular übermittelt wurden. Als Rückgabewert leiten wir einen Redirect zur Willkommensseite ein. Für den Pfad /welcome gibt es einen weiteren Controller mit einer @GET-Methode.
Die MessageController-Klasse
Dieser Controller nimmt unseren GET-Request entgegen und gibt unsere personalizedWelcome.jsp als View zurück.
Die @GET-Methode gibt nichts zurück (Controllermethoden die void als Rückgabewert haben, müssen die @View Annotation tragen, um die entsprechende View zu definieren) deshalb wird die Standard View angezeigt, welche der @View-Annotation übergeben wurde. Diese muss noch geschrieben werden. Wir können in dieser JSP über Expression Language auf unsere Named Bean zugreifen und Name, Geburtsdatum sowie Geschlecht auslesen und in unserer personalisierten Willkommensnachricht verwenden. Auch diese JSP landet im View Ordner, wie vorher beschrieben. Jetzt ist es an der Zeit unsere Applikation zu deployen und zu testen.
Cross-Site-Request-Forgery Protection
Ein weiteres Feature des MVC-Frameworks ist die Cross-Site-Request-Forgery Protection. Hierfür gibt es eine Annotation (‘@CsrfProtected’) die auf Methodenebene angebracht wird und für POST-Methoden gedacht ist. Wenn diese Annotation verwendet wird, wird bei der Formübermittlung ein generierter Token mitgeschickt, der dann validiert wird. Ist der Token nicht vorhanden oder nicht korrekt, schlägt die Übermittlung fehl und unsere Anfrage wird abgelehnt. In der View ist folgendes Input-Field vom Typ Hidden zu ergänzen.
Der Token wird zur Laufzeit generiert und die View kann dann über ExpressionLanguage darauf zugreifen. Wenn sich also jemand zwischen unsere Server-Client Verbindung schaltet, sind wir durch die Token-Validierung abgesichert.
Viel Spaß!