Table of Contents
List of Figures
List of Tables
These tutorials are meant as a starting point for developers that have never worked with Pustefix. They introduce you to the different types of applications that can be implemented with Pustefix. Of course, they can only scratch at the surface of most of the features, if you are interested in an in-depth look at Pustefix, refer to the reference documentation.
The source code used in the tutorials is available for public checkout. To test the tutorials on you local development machine, execute the following commands:
$ svn co https://pustefix.svn.sourceforge.net/svnroot/pustefix/trunk/pfixtutorial $ cd pfixtutorial $ ant $ ./startTomcat.sh
Of course, you can also start with a skeleton application and implement the tutorials from scratch.
Table of Contents
Before we can get started, you have to make sure that some requirements are met by your development environment. You will need:
JDK 5.0 or newer
POSIX-like operating system (Pustefix has been tested with Linux and Mac OS X, but might also work with other systems like *BSD)
Apache Tomcat 5.5.x
Apache Ant 1.6.5 or newer
The installation of these tools (except Tomcat) is not covered by this tutorial. Please refer to the documentation provided with these tools for installation instructions.
If you are not using Eclipse, you can just create an empty directory that will contain the project files and proceed with Section 1.3, “Unpack the skeleton”.
Start the Eclipse workbench and create a new project of type Java Project".
Make sure that you choose separate source and build folders: Use
src for the source and build for
the build folder. This is important because the Pustefix build script
expects these folders.
Download the newest pfixcore-skel-X.X.X.tar.gz from
Pustefix's downloads page.
Unpack the archive to a temporary directory. A new directory with the name
skel will be created. Copy the content of this directory
to your new project directory.
Now you need to download Apache Tomcat. Choose the
.tar.gz archive from the download page and place it in
the lib/tomcat directory of your project directory.
After you are done with that, refresh the resources view in Eclipse to make the new files appear.
To make features like auto-completion and auto-build work, you have to
import the libraries into Eclipse. Right-click on your project in Eclipse
and choose "Build Path" ⇒ "Configure Build Path...". Now use the
"Add JARs..." button to add all libraries from the project's
lib directory.
As Pustefix generates some classes, you have to add the folder with the
generated sources to Eclipse's source path. To make this work choose the
"Source" tab in the same dialog you used to configure the build path and
add the gensrc folder to the list of source folders.
Finally, you have to configure the path to the JAR file containing Ant.
In Eclipse choose ⇒ .
In the dialog window choose ⇒ ⇒
. Choose
and create a variable with the name PFX_ANT_LIB
that contains the path to the lib/ant.jar within your
Ant installation directory.
Within the project directory, create a file called
build.properties containing two properties:
standalone.tomcat=true makemode=test
The first property tells the build process, that we do want to run Tomcat without Apache Httpd integration. Apache Httpd integration can be useful, because static files can be served faster. However this is an advanced topic and for our purposes Tomcat alone will be okay.
The second parameter set the so-called "make mode". This flag can be set to either "test" or "prod" and will cause the editor console to appear in web pages when in "test" mode. In fact you can even make your own settings depend on the make mode, but we will take care of this later. For the moment "test" mode is just what we want. By the way, whenever you switch the make mode, you should do a complete rebuild using ant realclean && ant to make sure, all resources have been built using the same make mode.
Now run ant to perform a first build of the environment. This will create needed symlinks and initialize the environment.
Table of Contents
In this tutorial, you will learn how to work with the basic features provided by the Pustefix framework. You will accept user input, store it in the session and display it back to the user. Furthermore, you will create a very simple workflow containing three pages.
The requirements for your application are:
Provide an HTML form that can be used to register new users. The data for a new user must contain: gender, name, email-address, homepage, date of birth and a flag to mark the user as an administrator.
Validate the user data after the page has been submitted and display error information.
If the entered data is correct, move to a new page, which displays the user data and allows the user to choose whether he wants to go back and modify the data or accept the data.
After the data has been stored, display a confirmation page to the user.
In this tutorial application, you will focus on how the requirements will be implemented in the Pustefix framework. There will be no real business logic like actually storing the user data in any data base. These tasks are left to your favorite ORM framework.
Before you can start developing the application, make sure that your system fulfills all requirements that are mentioned in Section 1.1, “Requirements” and that you have build your Pustefix installation at least once, so that all necessary files have been extracted from the Pustefix distribution. More information on building Pustefix can be found in Section 1.5, “Build parameters & first build”
If your environment is set up correctly, you may create a new Pustefix project. For this
tutorial, please name the project first-app. A new Pustefix project
can be created using the newProject.sh script, which is part of the
Pustefix distribution. This script will guide you to the project creation and copy and
create all necessary files for you new project.
Please move to your installation directory and start the script:
$ ./newproject.sh ************************************************** * * * Pustefix ProjectGenerator 1.0 * * * ************************************************** Please follow the instructions to create a new project. You can abort the process by pressing Ctrl + C. Please type in the projects name e.g. "myproject"
After the newProject.sh script has been started, you will be asked for a project name.
Please enter first-app at the prompt and press return. The name of the project will be used
for several things:
It will be used as the name for the folder that contains all project-relevant files inside the projects directory
It will also be used as the default sub-domain name for your project.
After you entered the project name and pressed return, the setup script will continue:
Please type in the projects default language (it's english if you leave the field blank).
As your application will be in English, just leave the default language empty and press return. Again, the setup script will continue after you pressed return.
Please type in a comment for the Project It will be "projectname + comment" if you leave it blank.
Please enter My first Pustefix application as a comment for the project. This will not be visible
anywhere for the users of your application.
Please type in a name for the servlet 1
Every Pustefix application at least needs one servlet to process the request. Please enter app and
press return to assign a name to the servlet. The setup script will then continue.
Servlet 1 has been added! Would you like to create another servlet? [yes] [no]
In your first example application, all requests will be processed by the same servlet. Enter no to inform
the setup script, that you do not want to add another servlet. New servlets can easily be added during the development of
your application.>
Now you provided all necessary information and the setup script will create a new project based on your input.
Creating project in "/path/to/your/installation/projects/" starts now Creating project folder: first-app Folder has been created successfully Creating subfolder starts now Creating folder conf... Folder has been created successfully Creating folder img... Folder has been created successfully Creating folder xsl... Folder has been created successfully Creating folder xml... Folder has been created successfully Creating folder txt & subfolders Folder has been created successfully Creating folder htdocs... Folder has been created successfully Xml files editing starts now... Try to read the dom of the given Document: /path/to/your/installation/projects/core/prjtemplates/config.tmpl Transforming into dom has been successfull Changing the attribute depend Changing attribute has been successfull Changing the attribute name Changing attribute has been successfull Changing the attribute name Changing attribute has been successfull Changing the attribute name Changing attribute has been successfull Writing the file: app.conf.xml Writing file has been successfull Writing the file: app.conf.xml Writing file has been successfull Try to read the dom of the given Document: /path/to/your/installation/projects/core/prjtemplates/depend.tmpl Transforming into dom has been successfull Changing the attribute project Changing attribute has been successfull Changing the attribute lang Changing attribute has been successfull Changing the attribute handler Changing attribute has been successfull Changing the attribute stylesheet Changing attribute has been successfull Changing the attribute stylesheet Changing attribute has been successfull Changing the attribute xml Changing attribute has been successfull Writing the file: depend.xml Writing file has been successfull Try to read the dom of the given Document: /path/to/your/installation/projects/core/prjtemplates/frame.tmpl Transforming into dom has been successfull Changing the attribute href Changing attribute has been successfull Writing the file: frame.xml Writing file has been successfull Try to read the dom of the given Document: /path/to/your/installation/projects/core/prjtemplates/project.tmpl Transforming into dom has been successfull Changing the value of the tag depend Changing Textnode has been successfull Changing the attribute name Changing attribute has been successfull Changing the value of the tag comment Changing Textnode has been successfull Changing the value of the tag servername Changing Textnode has been successfull Changing the value of the tag serveralias Changing Textnode has been successfull Changing the value of the tag defpath Changing Textnode has been successfull Changing the value of the tag passthrough Changing Textnode has been successfull Changing the value of the tag documentroot Changing Textnode has been successfull Writing the file: project.xml.in Writing file has been successfull Try to read the dom of the given Document: /path/to/your/installation/projects/core/prjtemplates/skin.tmpl Transforming into dom has been successfull Writing the file: skin.xsl Writing file has been successfull Try to read the dom of the given Document: /path/to/your/installation/projects/core/prjtemplates/metatags.tmpl Transforming into dom has been successfull Writing the file: metatags.xsl Writing file has been successfull Try to read the dom of the given Document: /path/to/your/installation/projects/core/prjtemplates/page.tmpl Transforming into dom has been successfull Changing the value of the tag theme Changing Textnode has been successfull Writing the file: main_home1.xml Writing file has been successfull Writing the file: main_home1.xml Writing file has been successfull Your project has been successfully created. To see how it works type in "ant". Afterwards restart Apache httpd and Tomcat. Then type in "http://first-app.HOSTNAME.DOMAIN"
To complete the setup process, please re-run ant. Now Pustefix will automatically create the Apache and Tomcat configuration for your new project. Please make sure, that you restart Apache and Tomcat, after ant has finished its work.
After you restarted your web server, please open a web browser and open the URL http://first-app.HOSTNAME.DOMAIN.
Figure Figure 2.1, “The new Pustefix project” shows the output of the new Pustefix project.
The newProject.sh generated a working application for you. All relevant files have been put
into projects/first-app. Please take a look at the most important folders in this new project
directory:
The conf folder contains all configuration files for your applications. These are:
project.xml.in contains the information the project, like the domain name where
the project can be accessed, the servlets available in the project and some Apache configuration options.
app.conf.xml contains the configuration of the app servlet that
you created using the setup script.
depend.xml contains the configuration of the pages and XSL stylesheets used in
your project.
The htdocs is empty and can be used to store any resource that should be available
via the web server. Possible resources are JavaScript or CSS files.
The same applies to the img folder, which apparently is used to store image files.
The txt folder contains the text and HTML content of your application. After the
setup script has finished, it contains a pages folder which stores the different
pages of the application.
The xml folder contains the different frames of your application. A frame is an
XML document which will be used for every page that is generated. This way, you can easily share
header, footer and navigation between all pages.
The xsl folder contains XSL stylesheets that are only used in your application.
XSL stylesheets provided by Pustefix are not located inside this folder but in projects/core/xsl.
Start implementing your application by adding the three needed pages to the application:
EnterData, ReviewData and Confirm.
New pages are added by editing the conf/depend.xml file. For each page, you
have to add two XML tags:
The <page/> tag defines the servlet, that handles the request to this page.
All three pages will be handled by the app servlet, which is available at
xml/app.
The <standardpage/> tag defines the layout of the page by specifying the
XML frame that should be used. Again, all three pages will be rendered using the standard
frame that has been generated by the setup script.
<?xml version="1.0" encoding="utf-8"?> <make lang="en_GB" project="first-app"> <navigation> <page handler="/xml/app" name="home1"/> <page handler="/xml/app" name="EnterData"/> <page handler="/xml/app" name="ReviewData"/> <page handler="/xml/app" name="Confirm"/> </navigation> <!-- Metatags have been left out. --> <standardpage name="home1" xml="first-app/xml/frame.xml"/> <standardpage name="EnterData" xml="first-app/xml/frame.xml"/> <standardpage name="ReviewData" xml="first-app/xml/frame.xml"/> <standardpage name="Confirm" xml="first-app/xml/frame.xml"/> </make>
You can now open the EnterData page by browsing to http://first-app.HOSTNAME.DOMAIN/xml/app/EnterData.
As you did not provide any content for this page, Pustefix will display an error icon. When hovering
over this icon, you can see, that the content of the page in the file first-app/txt/pages/main_EnterData.xml
is missing.
This problem can easily be solved by adding this file:
<?xml version="1.0" encoding="utf-8"?> <include_parts xmlns:ixsl="http://www.w3.org/1999/XSL/Transform" xmlns:pfx="http://www.schlund.de/pustefix/core"> <part name="content"> <theme name="default"> <h1>Register new user</h1> </theme> </part> </include_parts>
If you reload the page, you will see the Register new user headline. Now repeat this
step for all three pages.
If you open the application in your browser without specifying the page directly, Pustefix will redirect
you to xml/app/home1, which is the default page generated by the setup script. Desired behaviour would
be, that the EnterData page is displayed, when your application is started.
This can be changed in the servlet configuration in conf/app.conf.xml:
<?xml version="1.0" encoding="utf-8"?> <contextxmlserver xmlns="http://pustefix.sourceforge.net/properties200401" xmlns:cus="http://www.schlund.de/pustefix/customize" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0" xsi:schemaLocation="http://pustefix.sourceforge.net/properties200401 http://pustefix.sourceforge.net/properties200401.xsd"> <servletinfo depend="first-app/conf/depend.xml" name="pfixcore_project:first-app::servlet:app"> <editmode allow="true"/> </servletinfo> <context defaultpage="EnterData"/> <pagerequest name="home1"/> </contextxmlserver>
The entry page is specified using the defaultpage attribute of the
<context/> tag. After you set this attribute to EnterData
open the URL http://first-app.HOSTNAME.DOMAIN in your browser and you will be automatically
redirected to the page to register new users.
After you changed the entry page, you can get rid of the generated entry page by executing these steps:
Delete the file main_home1.xml from the first-app/txt/pages folder.
Remove the <pagerequest/>> tag from the app.conf.xml file.
Remove the <page/>> and <standardpage/>> tags from the depend.xml file.
Next, you have to create the HTML form to accept the data of a new user. To create the form, you should not use the standard HTML tags, but the replacements by Pustefix, which automatically write back the data from the business logic to the HTML page.
The form has to be added to txt/pages/main_EnterData.xml:
<?xml version="1.0" encoding="utf-8"?> <include_parts xmlns:ixsl="http://www.w3.org/1999/XSL/Transform" xmlns:pfx="http://www.schlund.de/pustefix/core"> <part name="content"> <theme name="default"> <h1>Register new user</h1> <pfx:forminput> <table> <tr> <td>Gender:</td> <td> <pfx:xinp type="select" name="user.sex"> <pfx:option value="m">male</pfx:option> <pfx:option value="f">female</pfx:option> </pfx:xinp> </td> </tr> <tr> <td>Name:</td> <td><pfx:xinp type="text" name="user.name"/></td> </tr> <tr> <td>Email:</td> <td><pfx:xinp type="text" name="user.email"/></td> </tr> <tr> <td>Homepage:</td> <td><pfx:xinp type="text" name="user.homepage"/></td> </tr> <tr> <td>Birthdate:</td> <td><pfx:xinp type="text" name="user.birthdate"/></td> </tr> <tr> <td>Administrator:</td> <td><pfx:xinp type="check" name="user.admin" value="true"/></td> </tr> </table> <pfx:xinp type="submit" value="register"/> </pfx:forminput> </theme> </part> </include_parts>
See the Pustefix reference documentation for more information about the XML tags that have been used in this page.
Now that you have finished most of the HTML frontend, you should start implementing the business logic. The business logic in Pustefix applications mostly consists of three parts:
A wrapper is used to extract the user input from the HTTP-request, executes
some checks and casts the data to the desired Java types. A wrapper is also used
to write the values and/or error information back to the response. It connects your HTML frontend
with your application logic.
A handler processes the HTTP request. It extracts the user input from the wrapper,
executes additional information and does whatever is necessary in the specific application. You have all the power
provided by Java at your command when implementing a handler.
A handler does not have direct access to the HTTP request, HTTP session or HTTP response.
A ContextResource allows you to store any data in the Pustefix Context and thus
in the HTTP-session.
A ContextResource furthermore allows you to add XML data to the ResultDocument
which is the Pustefix way to pass information to the HTML frontend.
When implementing the business logic you will always start by implementing a wrapper. Wrappers in Pustefix are implemented using XML, which will then be used to generate a Java class for the wrapper.
Before you can implement a new wrapper, you will have to create a new Java package org.pustefixframework.tutorial.firstapp.wrapper
which will then contain the new wrapper. After the package has been created, create a new EnterUserDataWrapper.iwrp file for the wrapper
and paste the following content into this new file:
<interface xsi:schemaLocation="http://pustefix.sourceforge.net/interfacewrapper200401 http://pustefix.sourceforge.net/interfacewrapper200401.xsd" xmlns="http://pustefix.sourceforge.net/interfacewrapper200401" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <!-- This handler will process the data --> <ihandler class="org.pustefixframework.tutorial.firstapp.handler.EnterUserDataHandler"/> <!-- Parameters that have to be extracted from the request --> <param name="sex" type="java.lang.String" occurrence="mandatory"/> <param name="name" type="java.lang.String" occurrence="mandatory"/> <param name="email" type="java.lang.String" occurrence="mandatory"/> <param name="homepage" type="java.lang.String" occurrence="optional"/> <param name="birthdate" type="java.lang.String" occurrence="optional"/> <param name="admin" type="java.lang.Boolean" occurrence="optional"> <default> <value>false</value> </default> <caster class="de.schlund.pfixcore.generator.casters.ToBoolean"/> </param> </interface>
The wrapper defines various options:
Using the <ihandler/> tag, you define the name of the class that will do
the request processing for this handler. This class will be implemented at a later point (see
Section 2.6.3, “Implementing a handler” if you are too curious).
The different <param/> tags are used to define the different parameters that
should be extracted from the HTTP request. For each parameter you define the name and the type of the data.
All parameters except the admin-flag are String parameters, the admin-flag should be casted to a boolean
value. This can be achieved by setting the type attribute to java.lang.Boolean
and supplying a <caster/> tag that specifies a class to to the conversion for
you. The class de.schlund.pfixcore.generator.casters.ToBoolean is provided by the
Pustefix framework.
For each parameter you may also specify a default value and define whether the parameter is mandatory or
not. For more information on the differen wrapper features, please refer to the
reference documentation.
After you created the iwrp definition, please run ant generate-src to
generate the Java class for this wrapper:
$ ant generate-src
Buildfile: build.xml
init-dirs:
[mkdir] Created dir: /home/schst/workspace/pfixtutorial/build
[mkdir] Created dir: /home/schst/workspace/pfixtutorial/gensrc
tomcat.dir.opt:
tomcat.dir.missing:
ant-tasks-stage1:
ant-tasks-stage2:
ant-tasks:
skel.pre-compile:
pre-compile:
[echo] Extend the pre-compile target with your own definitions here...
generate-src:
[pfx-iwrp] Transformed 1 of 1 file, 0 have been up to date
BUILD SUCCESSFUL
Total time: 3 seconds
After ant has finished the build, you will find a new class
org.pustefixframework.tutorial.firstapp.wrapper.EnterUserDataWrapper in the
gensrc folder of your installation.
This file contains all information needed to extract the parameters from the request.
Now that you have implemented the HTML page and the wrapper you have to connect the HTML form with
the wrapper. This is done using the app.conf.xml servlet configuration. If a page
contains business logic that must be executed, you have to add a <pagerequest/>
tag to the servlet configuration.
Place an <input/> tag inside the <pagerequest/> tag which
will act as a container for all wrappers on this page.
Each wrapper is registered using an <interface/> tag which requires two parameters
to be set:
class specifies the clasname of the wrapper.
prefix specifies the prefix of all request parameters that this wrapper should
pay attention to. If you take a look the the HTML page (Section 2.5, “Create the input form”)
you will see, that all input fields are prefixed with user and a dot. This way,
you can have two wrappers that share parameter names, but will not conflict, as the parameters reside
in different namespaces.
To add your new wrapper to the EnterData page, add these lines to the configuration:
<?xml version="1.0" encoding="utf-8"?> <contextxmlserver xmlns="http://pustefix.sourceforge.net/properties200401" xmlns:cus="http://www.schlund.de/pustefix/customize" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0" xsi:schemaLocation="http://pustefix.sourceforge.net/properties200401 http://pustefix.sourceforge.net/properties200401.xsd"> <!-- ... --> <pagerequest name="EnterData"> <input> <interface prefix="user" class="org.pustefixframework.tutorial.firstapp.wrapper.EnterUserDataWrapper" /> </input> </pagerequest> </contextxmlserver>
If you now try to open the page again, Pustefix will respond with an IllegalStateException
and the following message:
unable to find class [org.pustefixframework.tutorial.firstapp.handler.EnterUserDataHandler] :org.pustefixframework.tutorial.firstapp.handler.EnterUserDataHandler
This already tells you, that implementing the handler must be your next step. But before you can go on and implement the handler, there is a small task left. Up to now, you do not have a Java type that is able to store the data for a user. This will be done in your next step.
![]() | Note |
|---|---|
This step has absolutely nothing to do with the Pustefix framework. However, as it is needed to understand the example, it still is part of the tutorial. |
As you need to store the data submitted by the user, you will need a bean, that is able to store all the information. The following class is a very simple implementation, in your applications you might already have these beans or use a framework, that is generating them for you.
package org.pustefixframework.tutorial.firstapp; public class User { private String name; private String email; private String birthday; private boolean admin; private String homepage; private String sex; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getBirthday() { return birthday; } public void setBirthday(String birthday) { this.birthday = birthday; } public boolean getAdmin() { return admin; } public void setAdmin(boolean admin) { this.admin = admin; } public String getHomepage() { return homepage; } public void setHomepage(String homepage) { this.homepage = homepage; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } }
This bean contains exactly the same properties as the wrapper you defined earlier. You will later use this class to store the user information in the session.
A handler is responsible to execute the actual business logic of your application. A handler can be any class but is has some requirements that have to be met:
It has to implement the de.schlund.pfixcore.generator.IHandler interface.
As handlers are used as flyweights, they must not have any static non-final properties.
If you use Eclipse to generate a new EnterUserDataHandler class that implements the
IHandler interface, you get the following code:
package org.pustefixframework.tutorial.firstapp.handler; import de.schlund.pfixcore.generator.IHandler; import de.schlund.pfixcore.generator.IWrapper; import de.schlund.pfixcore.workflow.Context; public class EnterUserDataHandler implements IHandler { public void handleSubmittedData(Context context, IWrapper wrapper) throws Exception { } public boolean isActive(Context context) throws Exception { return false; } public boolean needsData(Context context) throws Exception { return false; } public boolean prerequisitesMet(Context context) throws Exception { return false; } public void retrieveCurrentStatus(Context context, IWrapper wrapper) throws Exception { } }
If you now open the page again, you still get an error, that the page is still not accessible. This is because of the return values of the generated methods and how Pustefix processes a request.
When a page is requested, Pustefix calls the prerequisitesMet method of all
handlers that are configured for this page. If any of these methods return false,
the page will not be displayed.
If all of these methods return true, Pustefix will call the isActive method
on all handlers of the page. If none of the methods return true, the page will not be displayed.
When Eclipse generated the method bodies, both methods return false und thus the page
cannot be displayed.
Modify the return values of prerequisitesMet and isActive to
make the page accessible
package org.pustefixframework.tutorial.firstapp.handler; import de.schlund.pfixcore.generator.IHandler; import de.schlund.pfixcore.generator.IWrapper; import de.schlund.pfixcore.workflow.Context; public class EnterUserDataHandler implements IHandler { public boolean isActive(Context context) throws Exception { return true; } public boolean prerequisitesMet(Context context) throws Exception { return true; } }
If you now open the page again, all form fields will be displayed. To test your form, fill out at least the mandatory fields:
gender
name
If you submit the data, the wrapper will validate your data and then display the page again. At this point, the form elements will still contain the values that you entered. Pustefix saved their state automatically.
If you click on the XML button in the upper right corner of the page, you will see the
XML document that contains the data of the rendered page:
<formresult serial="1214247165246"> <formvalues> <param name="user.email">schst@bar.de</param> <param name="user.name">Stephan</param> <param name="user.sex">m</param> </formvalues> <formerrors/> <formhiddenvals/> <wrapperstatus> <wrapper active="true" name="org.pustefixframework.tutorial.firstapp.wrapper.EnterUserDataWrapper" prefix="user"/> </wrapperstatus> </formresult>
The <formvalues/> node contains a <param/> element for each
of the form fields that you submitted. Inside the <wrapperstatus/> node, you can see a list
of all wrappers that are registered for this page.
As the handler mostly consists of auto-generated code, it does not execute any business logic. If you want
to execute Java code after the page is submitted, you only need to place it in the handleSubmittedData
method
package org.pustefixframework.tutorial.firstapp.handler; import de.schlund.pfixcore.generator.IHandler; import de.schlund.pfixcore.generator.IWrapper; import de.schlund.pfixcore.workflow.Context; public class EnterUserDataHandler implements IHandler { public void handleSubmittedData(Context context, IWrapper wrapper) throws Exception { System.out.println("Place business logic here"); } }
You will re-visit this class at a later point in the tutorial and add some real logic instead of just debugging code.
To be able to transport the user data from one page to the other, you have to store it in the session.
Pustefix does not allow you direct access to the applications HTTP session. Instead it provides you with an easy way to use objects that live inside the session scope. Those objects are called context resources. A context resource can be any class, it just has to meet the following requirements:
There has to be an interface and an implementation for the context resource.
The interface has to extend the ContextResource interface provided by Pustefix.
A context resource, that is able to store a user inside the HTTP session only requires two methods: one to set the user and one to retrieve it from the context resource.
package org.pustefixframework.tutorial.firstapp.contextresources; import org.pustefixframework.tutorial.firstapp.User; import de.schlund.pfixcore.workflow.ContextResource; public interface ContextUser extends ContextResource { public void setUser(User user); public User getUser(); }
The implementation for this interface also is very easy:
package org.pustefixframework.tutorial.firstapp.contextresources; import org.pustefixframework.tutorial.firstapp.User; import org.w3c.dom.Element; import de.schlund.pfixcore.workflow.Context; import de.schlund.pfixxml.ResultDocument; public class ContextUserImpl implements ContextUser { private User user; public User getUser() { return user; } public void setUser(User user) { this.user = user; } public void init(Context context) throws Exception { // nothing to do here } public void insertStatus(ResultDocument document, Element element) throws Exception { // will be implemented later } }
The ContextResource interface forces you to implement the following methods:
init will be called when the context resource is created.
insertStatus should insert its current state into the DOM tree.
If a context resource is only used as a container to store information in the session, it is not needed to implement any of these methods.
To make this context resource available in your application, you have to register it in the servlet configuration
file app.conf.xml:
<?xml version="1.0" encoding="utf-8"?> <contextxmlserver xmlns="http://pustefix.sourceforge.net/properties200401" xmlns:cus="http://www.schlund.de/pustefix/customize" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0" xsi:schemaLocation="http://pustefix.sourceforge.net/properties200401 http://pustefix.sourceforge.net/properties200401.xsd"> <!-- Rest of configuration --> <context defaultpage="EnterData"> <resource class="org.pustefixframework.tutorial.firstapp.contextresources.ContextUserImpl"> <implements class="org.pustefixframework.tutorial.firstapp.contextresources.ContextUser"/> </resource> </context> <!-- Rest of configuration --> </contextxmlserver>
Now, that you have created a new context resource, you will learn, how you can access it from your applciation.
All context resources can be accessed via the ContextResourceManager, which provides a
getResource method.
ContextResourceManager manager = context.getContextResourceManager();
ContextUser cUser = manager.getResource(ContextUser.class);
This method accepts the Class object of the interface. If the resource is accessed for the
first time, a new instance of the implementation specified in the servlet configuration, is created and returned.
On subsequent calls, the same instance is returned, so the resource is a singleton in the session scope.
You can now implement the business logic in the handleSubmittedData method, that creates
the User object and stores it in your context resource.
But first, you need to know, how you can extract the parameters from the request. Pustefix will pass two arguments
to the handleSubmittedData method: The Context and an instance of
IWrapper. This IWrapper actually is an instance of the generated
EnterUserDataWrapper class, that has been generated from your wrapper configuration. This
class provides getter-methods for all parameters that you specified in the wrapper. Use these methods to extract
the data from the request and create a new User instance based on the input data:
package org.pustefixframework.tutorial.firstapp.handler; import org.pustefixframework.tutorial.firstapp.User; import org.pustefixframework.tutorial.firstapp.contextresources.ContextUser; import org.pustefixframework.tutorial.firstapp.wrapper.EnterUserDataWrapper; import de.schlund.pfixcore.generator.IHandler; import de.schlund.pfixcore.generator.IWrapper; import de.schlund.pfixcore.workflow.Context; import de.schlund.pfixcore.workflow.ContextResourceManager; public class EnterUserDataHandler implements IHandler { public void handleSubmittedData(Context context, IWrapper wrapper) throws Exception { ContextResourceManager manager = context.getContextResourceManager(); ContextUser cUser = manager.getResource(ContextUser.class); EnterUserDataWrapper euWrapper = (EnterUserDataWrapper)wrapper; User user = new User(); user.setSex(euWrapper.getSex()); user.setName(euWrapper.getName()); if (euWrapper.getEmail() != null) { user.setEmail(euWrapper.getEmail()); } if (euWrapper.getHomepage() != null) { user.setHomepage(euWrapper.getHomepage()); } if (euWrapper.getBirthdate() != null) { user.setBirthday(euWrapper.getBirthdate()); } user.setAdmin(euWrapper.getAdmin()); cUser.setUser(user); } }
Now you have implemented the business logic, that creates your User object and stores it in the session,
but still, the EnterData page is displayed, after you submitted the page. The desired behaviour is to display
the ReviewData after the business logic has successfully been executed.
To achieve this, you have to create a new workflow and specify the steps in this workflow. This is done via the <pageflow/> tag
in the servlet configuration file:
<?xml version="1.0" encoding="utf-8"?> <contextxmlserver xmlns="http://pustefix.sourceforge.net/properties200401" xmlns:cus="http://www.schlund.de/pustefix/customize" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0" xsi:schemaLocation="http://pustefix.sourceforge.net/properties200401 http://pustefix.sourceforge.net/properties200401.xsd"> <!-- Rest of configuration --> <pageflow name="RegisterUser" final="Confirm"> <flowstep name="EnterData"/> <flowstep name="ReviewData" stophere="true"/> </pageflow> <!-- Rest of configuration --> </contextxmlserver>
The workflow starts with the EnterData page, which is followed by the ReviewData page. After the page flow
is finished, the final page Confirm will be displayed. Open the EnterData page again, after you made the changes,
fill out the mandatory fields and submit them: the page flow will lead you to the ReviewData page, as you specified in the configuration.
Up to now, you expected, that the users of your application do not make any mistakes. We all know, that this is not the case for
real-life users. To test, how the application reacts, if you do not fill out all required fields, open the EnterData
page again, leave the name field empty and submit the page.
As you can see, Pustefix is clever enough not to continue the page flow. If you would debug the application, you would see, that Pustefix
even does not execute the handleSubmittedData method of your handler. This is because the generated wrapper class does
some basic validation on the input data. As you specified the name parameter as mandatory, the data
is not valid and the wrapper is not passed to the handler.
But how is the user supposed to know, why the page flow is not processed, there is no error message. But this is not completely true. Pustefix sends you an error message, but your page does not display it. If you open the DOM-tree view again, you will see a difference in the XML document that is used for the page rendering:
<formresult serial="1214248690859"> <formvalues> <param name="user.email">schst@bar.de</param> <param name="user.sex">m</param> </formvalues> <formerrors> <error name="user.name"> <pfx:include href="core-override/dyntxt/statusmessages-core-merged.xml" part="MISSING_PARAM"/> </error> </formerrors> <formhiddenvals/> <wrapperstatus> <wrapper active="true" name="org.pustefixframework.tutorial.firstapp.wrapper.EnterUserDataWrapper" prefix="user"/> </wrapperstatus> <pageflow name="RegisterUser"> <step current="true" name="EnterData"/> <step name="ReviewData"/> </pageflow> </formresult>
Inside the <formerrors/> node, there is an <error/> element which signals an
error for the field user.name. The content of the error is an include to the part MISSING_PARAM
in the file core-override/dyntxt/statusmessages-core-merged.xml. This file contains all error messages
provided by the core. If you open the file and search for this part, you will find something ike this XML:
<part name="MISSING_PARAM"> <theme name="default">This parameter is mandatory.</theme> </part>
As you can see, Pustefix informs you, that an error happened, and even provides you with a human readable message for the error. All
that is left for you to do, is display it to the user. This can be done using the <pfx:checkfield/>, <pfx:error/>
and <pfx:scode/> tags provided by Pustefix.
To add the error message, open the page content file txt/pages/main_EnterData.xml. The <pfx:checkfield/> tag
is used to check, whether an error happened for a specific field or not. Inside this tag, you can place a <pfx:error/> tag. The
content of this tag will only be displayed, if an error occurred for this field. The <pfx:scode/> tag is an easy way
to fetch the error message from the core-override/dyntxt/statusmessages-core-merged.xml file.
<?xml version="1.0" encoding="utf-8"?> <include_parts xmlns:ixsl="http://www.w3.org/1999/XSL/Transform" xmlns:pfx="http://www.schlund.de/pustefix/core"> <part name="content"> <theme name="default"> <h1>Register new user</h1> <pfx:forminput> <table> <!-- Other form fields --> <tr> <td>Name:</td> <td> <pfx:xinp type="text" name="user.name"/> <pfx:checkfield name="user.name"> <pfx:error><div style="color:#ff0000;"><pfx:scode/></div></pfx:error> </pfx:checkfield> </td> </tr> <!-- Other form fields --> </table> <pfx:xinp type="submit" value="register"/> </pfx:forminput> </theme> </part> </include_parts>
If you now try to submit the page again without entering your name, Pustefix will display the correct error message. Of course, you have to repeat this step for all other input fields on your page.
Now that your application is displaying errors correctly, you should go back to implement the main requirements. The next feature
that you will be implementing is the review page, where the user will be able to review has data. To achieve this, you will have
to write the user data to the DOM tree, that is used to render the page. This can be done by implementing the insertStatus
method of the ContextUserImpl class.
This method receives the document and the element where it should append the user data. The ResultDocument class
provides some wrapper methods, which make it easier to add data to the XML:
package org.pustefixframework.tutorial.firstapp.contextresources; import org.pustefixframework.tutorial.firstapp.User; import org.w3c.dom.Element; import de.schlund.pfixcore.workflow.Context; import de.schlund.pfixxml.ResultDocument; public class ContextUserImpl implements ContextUser { public void insertStatus(ResultDocument document, Element element) throws Exception { if (this.user == null) { return; } ResultDocument.addTextChild(element, "sex", this.user.getSex()); ResultDocument.addTextChild(element, "name", this.user.getName()); ResultDocument.addTextChild(element, "email", this.user.getEmail()); ResultDocument.addTextChild(element, "homepage", this.user.getHomepage()); ResultDocument.addTextChild(element, "birthday", this.user.getBirthday()); ResultDocument.addTextChild(element, "admin", String.valueOf(this.user.getAdmin())); } }
Now that the context resource is able to render its state to the XML document, you have to tell Pustefix, on which pages, the context
resource should be rendered. Again, this is done in the servlet configuration file app.conf.xml. This time, you will
have to add an <output/> tag to the page and nest <resource/> tags for each
context resource, that should be rendered on this page.
<?xml version="1.0" encoding="utf-8"?> <contextxmlserver xmlns="http://pustefix.sourceforge.net/properties200401" xmlns:cus="http://www.schlund.de/pustefix/customize" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0" xsi:schemaLocation="http://pustefix.sourceforge.net/properties200401 http://pustefix.sourceforge.net/properties200401.xsd"> <!-- other configuration options --> <pagerequest name="ReviewData"> <output> <resource node="user" class="org.pustefixframework.tutorial.firstapp.contextresources.ContextUser"/> </output> </pagerequest> </contextxmlserver>
The class attribute references the interface implemented by the context resource and the node
attribute defines the name of the element in which it will be rendered.
Open the XML view for the page, after you made the changes and reloaded the page and you will see, that the user data has been inserted into the XML:
<formresult serial="1214249956789"> <formvalues/> <formerrors/> <formhiddenvals/> <user> <sex>m</sex> <name>Stephan</name> <email>schst@bar.de</email> <homepage>http://pustefix-framework.org</homepage> <birthday>null</birthday> <admin>false</admin> </user> <pageflow name="RegisterUser"> <step name="EnterData"/> <step current="true" name="ReviewData"/> </pageflow> </formresult>
Pustefix allows you to use XSL and XPath in your page definitions to insert this information in the rendered page. Use the ixsl
namespace to access nodes from the DOM tree:
<?xml version="1.0" encoding="utf-8"?> <include_parts xmlns:ixsl="http://www.w3.org/1999/XSL/Transform" xmlns:pfx="http://www.schlund.de/pustefix/core"> <part name="content"> <theme name="default"> <h1>Review your data</h1> <table> <tr> <td>Gender:</td> <td> <ixsl:choose> <ixsl:when test="/formresult/user/gender/text() = 'f'">female</ixsl:when> <ixsl:otherwise>male</ixsl:otherwise> </ixsl:choose> </td> </tr> <tr> <td>Name:</td> <td><ixsl:value-of select="/formresult/user/name/text()"/></td> </tr> <tr> <td>Email:</td> <td><ixsl:value-of select="/formresult/user/email/text()"/></td> </tr> <tr> <td>Homepage:</td> <td><ixsl:value-of select="/formresult/user/homepage/text()"/></td> </tr> <tr> <td>Birthdate:</td> <td><ixsl:value-of select="/formresult/user/birthdate/text()"/></td> </tr> <tr> <td>Administrator:</td> <td><ixsl:value-of select="/formresult/user/admin/text()"/></td> </tr> </table> </theme> </part> </include_parts>
If you now open the page again, enter your data and submit the form, Pustefix will display a new page, which contains the information you entered.
Your application now handles the input of user data and displays it again to the user. To complete the last requirement, your application
will have to provide another button on the ReviewData page, which lets the user confirm the data and then executes
the business logic to store the data in your persistence layer.
To achieve this, you will have to implement a new pair of wrapper and handler. As the ReviewData page does not
contain any input fields, the new wrapper does not need any parameters:
<interface xsi:schemaLocation="http://pustefix.sourceforge.net/interfacewrapper200401 http://pustefix.sourceforge.net/interfacewrapper200401.xsd" xmlns="http://pustefix.sourceforge.net/interfacewrapper200401" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <!-- This handler will process the data --> <ihandler class="org.pustefixframework.tutorial.firstapp.handler.SaveUserDataHandler"/> </interface>
After you implemented the wrapper, continue by creating a new SaveUserData class, which implements the
IHandler interface. Make sure that the handler is active by returning true from the
prerequisitesMet and isActive methods. Place the business logic that saves the new user
in the handleSubmittedData method. In this example, the business logic has been replaced by a simple
System.out.println call.
package org.pustefixframework.tutorial.firstapp.handler; import org.pustefixframework.tutorial.firstapp.contextresources.ContextUser; import de.schlund.pfixcore.generator.IHandler; import de.schlund.pfixcore.generator.IWrapper; import de.schlund.pfixcore.workflow.Context; import de.schlund.pfixcore.workflow.ContextResourceManager; public class SaveUserDataHandler implements IHandler { public void handleSubmittedData(Context context, IWrapper wrapper) throws Exception { System.out.println("Business logic to save data"); } public boolean isActive(Context context) throws Exception { return true; } public boolean needsData(Context context) throws Exception { return false; } public boolean prerequisitesMet(Context context) throws Exception { return true; } public void retrieveCurrentStatus(Context context, IWrapper arg1) throws Exception { // Nothing to be done here } }
As the handler should be triggered from the ReviewData page, you have to add the handler to the
<pagerequest/> tag in the servlet configuration:
<?xml version="1.0" encoding="utf-8"?> <contextxmlserver xmlns="http://pustefix.sourceforge.net/properties200401" xmlns:cus="http://www.schlund.de/pustefix/customize" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0" xsi:schemaLocation="http://pustefix.sourceforge.net/properties200401 http://pustefix.sourceforge.net/properties200401.xsd"> <pagerequest name="ReviewData"> <input> <interface prefix="user" class="org.pustefixframework.tutorial.firstapp.wrapper.SaveUserDataWrapper" /> </input> <output> <resource node="user" class="org.pustefixframework.tutorial.firstapp.contextresources.ContextUser"/> </output> </pagerequest> </contextxmlserver>
All that is left your you now, is to add a possibility to submit the ReviewData page. But as there is no
input form needed, you will learn a new way, how data can be submitted using Pustefix. The <pfx:button/>
tag is used, to create links between different Pustefix pages. But it can also be used to link to the same page again and
pass any arguments using the <pfx:argument/>. If you use the <pfx:argument/>
tag, Pustefix will treat the request as if the page had been submitted and call the handleSubmittedData method
on all handlers on the page.
As your handler does not accept any parameters, you can pass any argument as you like, for example setting the argument
user.save to true.
<?xml version="1.0" encoding="utf-8"?> <include_parts xmlns:ixsl="http://www.w3.org/1999/XSL/Transform" xmlns:pfx="http://www.schlund.de/pustefix/core"> <part name="content"> <theme name="default"> <h1>Review your data</h1> <!-- Display user data --> <pfx:button> <pfx:argument name="user.save">true</pfx:argument> Go ahead and save the data </pfx:button> </theme> </part> </include_parts>
If you click on this link, the page will be submitted and if you placed the business logic inside the handler, it will be executed.
Open the txt/pages/main_Confirm.xml file and add some HTML, that will be displayed after the Pustefix
workflow will continue to the last page.
<?xml version="1.0" encoding="utf-8"?> <include_parts xmlns:ixsl="http://www.w3.org/1999/XSL/Transform" xmlns:pfx="http://www.schlund.de/pustefix/core"> <part name="content"> <theme name="default"> <h1>Congratulations</h1> <p>Your data has been saved</p> </theme> </part> </include_parts>
Now you have implemented all specified requirements and finished your first Pustefix application.
In the last part of the tutorial you will polish some of the rough edges of your applications, which you did not deal with while implementing the application.
Although your application is already working, there are some minor problems that you should deal with.
In a typical application, the review page does not only display the entered data, but offer you a link to go back and modify the data you entered before. Your application currently lacks this feature, but you will now learn how this is implemented in Pustefix.
At first, you have to add a new link to the main_ReviewData.xml page, which sends the user back to the
EnterData page. This can be done using the <pfx:button/> tag:
<pfx:button page="EnterData">Go back and edit data</pfx:button>
If you click on this link, the EnterData page will be loaded and the form will be displayed again.
But the application does not behave exactly as you would like it to, as the form fields are empty and not filled with
the data that the user entered before. This can be easily changed.
Every time a page is rendered, Pustefix will call the retrieveCurrentStatus method of all handlers that
are registered for the page and pass in the context and the matching wrapper for the handler. In this method
you may use the generated setter methods of the wrapper class to assign values to the form fields.
To prefill the form fields with the values the user entered before, you need to fetch the User instance
from the context resource, extract the properties and assign them to the form elements. This is almost the opposite of what you
implemented in the handleSubmittedData method:
Fetch the ContextUser context resource using the ContextResourceManager.
Fetch the User instance from the context resource.
If the user equals null the page is accessed for the first time, no user data has been entered, and there is nothing left to do. You can leave the method using the return statement.
Cast the generic wrapper parameter to the concrete EnterUserDataWrapper implementation.
Get the properties from the User instance and set them to the corresponding properties in the wrapper.
package org.pustefixframework.tutorial.firstapp.handler; import org.pustefixframework.tutorial.firstapp.User; import org.pustefixframework.tutorial.firstapp.contextresources.ContextUser; import org.pustefixframework.tutorial.firstapp.wrapper.EnterUserDataWrapper; import de.schlund.pfixcore.generator.IHandler; import de.schlund.pfixcore.generator.IWrapper; import de.schlund.pfixcore.workflow.Context; import de.schlund.pfixcore.workflow.ContextResourceManager; public class EnterUserDataHandler implements IHandler { public void retrieveCurrentStatus(Context context, IWrapper wrapper) throws Exception { ContextResourceManager manager = context.getContextResourceManager(); ContextUser cUser = manager.getResource(ContextUser.class); User user = cUser.getUser(); if (user == null) { return; } EnterUserDataWrapper euWrapper = (EnterUserDataWrapper)wrapper; euWrapper.setSex(user.getSex()); euWrapper.setName(user.getName()); euWrapper.setEmail(user.getEmail()); euWrapper.setHomepage(user.getHomepage()); euWrapper.setBirthdate(user.getBirthday()); euWrapper.setAdmin(user.getAdmin()); } }
If you restart the application, enter some data and then go back to the form, you can see, that the form fields contain the values that you wanted them to contain and that you entered before.
Another problem ist, that your pages are not protected against unwanted access. If you open the URL
http://first-app.HOSTNAME.DOMAIN/xml/app/ReviewData, Pustefix will display this page,
although there is not data to review.
It is the responsibility of the application developer to make sure, that the page can only be
displayed, if it makes sense in the application context. However, Pustefix provides an easy
way to disable the availability of a page. The method prerequisitesMet will be called on
every handler, every time a page should be displayed. If this method returns false, the page
will not be displayed.
To protect the ReviewData page, you have to modify the prerequisitesMet method
of the SaveUserDataHandler handler. As the page should only be accessible, if a user has
been entered, you only need to fetch the ContextUser context resource and check, whether it contains
user data. If no user has been set, the method must return false, otherwise it must return true.
package org.pustefixframework.tutorial.firstapp.handler; import org.pustefixframework.tutorial.firstapp.contextresources.ContextUser; import de.schlund.pfixcore.generator.IHandler; import de.schlund.pfixcore.generator.IWrapper; import de.schlund.pfixcore.workflow.Context; import de.schlund.pfixcore.workflow.ContextResourceManager; public class SaveUserDataHandler implements IHandler { public boolean prerequisitesMet(Context context) throws Exception { ContextResourceManager manager = context.getContextResourceManager(); ContextUser cUser = manager.getResource(ContextUser.class); if (cUser.getUser() != null) { return true; } return false; } }
If you now open the http://first-app.HOSTNAME.DOMAIN/xml/app/ReviewData, the page cannot be displayed.
Now the workflow mechanism kicks in and selects the next page, that should be displayed. This is the Confirm
page, which has been defined as the final page of the workflow.
It would have been better, if the EnterData page would have been displayed instead of the Confirm,
page, as the user must enter data before any other page can be displayed. This can easily be achieved using Pustefix.
Each handler has a needsData method, which will be called, when the workflow mechanism selects the next
page to display. The workflow will start with the first page in the current workflow and if the needsData
method returns true, it will stop at this page until the handler is satisfied.
To make sure, that the user will stay on this page, you only need to implement the needsData method of the
EnterUserDataHandler handler: the method must return true, unless the context
resource has a User instance.
package org.pustefixframework.tutorial.firstapp.handler; import org.pustefixframework.tutorial.firstapp.User; import org.pustefixframework.tutorial.firstapp.contextresources.ContextUser; import org.pustefixframework.tutorial.firstapp.wrapper.EnterUserDataWrapper; import de.schlund.pfixcore.generator.IHandler; import de.schlund.pfixcore.generator.IWrapper; import de.schlund.pfixcore.workflow.Context; import de.schlund.pfixcore.workflow.ContextResourceManager; public class EnterUserDataHandler implements IHandler { public boolean needsData(Context context) throws Exception { ContextResourceManager manager = context.getContextResourceManager(); ContextUser cUser = manager.getResource(ContextUser.class); if (cUser.getUser() == null) { return true; } return false; } }
if you now try to access the ReviewData page directly, Pustefix will redirect you to the
EnterData page instead until you entered the data of a new user.
In this tutorial you learned the basics about the core features of Pustefix and developed a very simple, but typical, web application. For details about the features that you got to know in this tutorial, take a look at the comprehensive reference documentation, which describes all the configuration options, XML tags and interfaces in detail.
Of course, Pustefix provides features to automate these tasks, there is no need to re-implement everything from scratch for your applications. See Chapter 3, Usermanager tutorial to learn how Pustefix will automatically generate wrapper definitions from your existing beans, how to add custom validations and how you can avoid working with DOM and adding any object structure to the generated XML tree automatically.
Table of Contents