Pustefix Tutorial

Version 0.16.x

Tobias Fehrenbach

Martin Leidig

Stephan Schmidt

Felix Firgau

This tutorial relates to the latest Pustefix release from the 0.16 branch.


Table of Contents

Introduction
1. Getting started
1.1. Requirements
1.2. Maven setup
1.3. Generating a new project from a Maven archetype
1.4. Configure your IDE
1.5. Getting the source code
2. Basic tutorial
2.1. Setting up a new project
2.2. Generated files
2.3. Creating the pages
2.4. Setting the entry page
2.5. Create the input form
2.6. Implementing the business logic
2.6.1. Implementing a wrapper
2.6.2. Interlude: Implementing a bean
2.6.3. Implementing a handler
2.6.4. Implementing a ContextResource
2.6.5. Accessing a ContextResource
2.7. Implementing the workflow
2.8. Displaying errors
2.9. Displaying the entered data
2.10. Saving the data
2.11. The finishing touch
2.11.1. Changing data
2.11.2. Avoiding unwanted access
2.12. Conclusion
3. Usermanager tutorial
3.1. Setup
3.2. Create a new bean
3.3. Create the handler
3.4. Create a caster
3.4.1. Create a new statuscode for the caster
3.4.2. Create the ToURL-caster
3.5. Annotate the User bean
3.5.1. Annotate the class declaration
3.5.2. Annotate the getter
3.6. Generate wrapper
3.7. Create a userlist class
3.8. Adjust the servlet configuration
3.9. Add a new user
3.10. Create the pages
3.10.1. Create a page for user data input
3.10.2. Create page for listing users
3.10.3. Add pages to xml configuration files
3.11. Add a pageflow
3.12. Try to add your own user
3.13. Add sample users
3.13.1. Add new constructors to your UserList
3.13.2. Add init method to UserList
3.14. New feature: Delete a user
3.14.1. Create a new bean
3.14.2. Create a new handler
3.14.3. Annotate the DeleteUser bean
3.14.4. Generate the wrapper class
3.14.5. Add a delete user method
3.14.6. Delete a user
3.14.7. Add a delete button
3.14.8. Adjust the servlet configuration
3.15. New feature: Edit a user
3.15.1. Create a new bean
3.15.2. Create a new handler
3.15.3. Annotate the EditUser bean
3.15.4. Generate the wrapper class
3.15.5. Add a method to replace a user of our UserList class
3.15.6. Edit a user
3.15.7. Add an edit button
3.15.8. Adjust the servlet configuration
3.15.9. Add user to form
3.15.10. Replace method handleSubmittedData of UserHandler
3.16. Conclusion
4. AJAX Calculator tutorial
4.1. Setup
4.2. Using the webservice servlet
4.3. Implementing the business logic
4.4. Exposing the service
4.5. Consuming the service
4.6. Conclusion

List of Figures

2.1. The new Pustefix project

List of Tables

4.1. Global webservice configuration
4.2. Local webservice configuration

Introduction

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.

You can either work through the tutorial by following the instructions and create the applications from scratch (the preferred way for first-time Pustefix users) or you can just check out the complete source code (see Section 1.5, “Getting the source code”).

Getting started

1.1. Requirements

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 Maven 2.0.10 or newer

The installation of these tools is not covered by this tutorial. Please refer to the documentation provided with these tools for installation instructions.

1.2. Maven setup

Pustefix provides some Maven archetypes for quickly creating new applications. Therefore you have to configure the repository where the according Pustefix artifacts reside (because they currently aren't hosted in the Maven central repository).

You can either configure the repository globally for Maven or always specify the URL of the repository's archetype-catalog when generating a new project (and add the repository configuration to the project's POM, if not already there). If you don't want to configure it globally, you can skip the next paragraphs and continue with the archetype generation in the next section.

Adding the Pustefix repository can be easily done by adding the following profile to the Maven settings. The profile can be either added to the settings.xml file located in the .m2 directory within your home directory (if it doesn't already exist you can just create it) or to the settings.xml file located in the conf directory of your Maven installation.

<settings>
  <profiles>

    <profile>
      <id>pustefix</id>
      <activation>
        <activeByDefault>true</activeByDefault>
      </activation>
      <repositories>
        <repository>
          <id>pustefix</id>
          <name>Pustefix Maven Repository</name>
          <url>http://pustefix-framework.org/repository/maven</url>
        </repository>
      </repositories>
      <pluginRepositories>
        <pluginRepository>
          <id>pustefix</id>
          <url>http://pustefix-framework.org/repository/maven</url>
        </pluginRepository>
      </pluginRepositories> 

    </profile>
  </profiles>
</settings>

1.3. Generating a new project from a Maven archetype

Pustefix provides an archetype for creating a basic application. Just call mvn archetype:generate and select the archetype pustefix-archetype-basic:

      $ mvn archetype:generate

After choosing the pustefix-archetype-basic Maven will ask you for your project's groupId, artifactId, version and package. Having finished these settings Maven will generate the new project within a new directory called like the artifactId and located within the current working directory.

      $ cd myproject
      $ mvn tomcat:run

The generated project is a standard Maven project with war packaging type, i.e. you can immediately build and run your application, e.g. using Tomcat by calling mvn tomcat:run or mvn tomcat:run-war and opening http://localhost:8080/myproject in your browser.

1.4. Configure your IDE

You can use the IDE of your choice. As a Pustefix project is a standard Maven project, you can use an arbitrary IDE supporting projects with Maven layout.

Eclipse will be a good choice, e.g. you can just generate an according Eclipse configuration for your project using the Eclipse Maven plugin:

      $ mvn eclipse:eclipse

Then you can import your project into Eclipse. Doing this the first time you will have to add the M2_REPO classpath variable pointing to your local Maven repository (usually .m2/repository within your home directory). This can be done by opening the WindowPreferences dialog and navigating to JavaBuild PathClasspath Variables (also see the Maven Eclipse documentation).

1.5. Getting the source code

The source code used in the tutorials is available for public checkout. To test the tutorials on your local development machine, execute the following commands:

$ svn co https://pustefix.svn.sourceforge.net/svnroot/pustefix/tags/pustefixframework-0.18.x/pustefix-tutorial
$ cd pustefix-tutorial/ajax-calculator (or first-app, usermanagement)
$ mvn tomcat:run

Basic tutorial

Martin Leidig

Stephan Schmidt

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:

  1. 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.

  2. Validate the user data after the page has been submitted and display error information.

  3. 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.

  4. 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.

2.1. Setting up a new project

Before you can start developing the application, make sure that your system fulfills all requirements that are mentioned in Section 1.1, “Requirements”.

If your environment is set up correctly, you may create a new Pustefix project. For this tutorial, please name the project firstapp. A new Pustefix project can be created using a Maven archetype as described in Section 1.2, “Maven setup” and Section 1.3, “Generating a new project from a Maven archetype”. The project name corresponds to the artifactId, so you should enter firstapp when you're asked by Maven. You should further choose org.pustefixframework.tutorial as groupId and org.pustefixframework.tutorial.firstapp as package name (you can choose other names, but you will have to replace the recommended names wherever used in this tutorial).

After Maven successfully created the new project, you can start it using mvn tomcat:run and open it in your browser under http://localhost:8080/firstapp. Figure Figure 2.1, “The new Pustefix project” shows the output of the new Pustefix project.

Figure 2.1. The new Pustefix project

The new Pustefix project

2.2. Generated files

Maven generated a working application for you. All relevant files have been put into the firstapp folder within your working directory. Please take a look at the most important folders:

  • The src/main/webapp/WEB-INF folder contains all configuration files for your applications. These are:

    • project.xml contains general project information, framework configuration, e.g. for the rendering system, exception processing, path mappings for static resources, etc.

    • app.xml contains the configuration concerning the Java part of the application, e.g. pagerequests and assigned Java framework classes

    • depend.xml contains the configuration concerning the XML/XSLT part of the application, e.g. pages and XSL stylesheets used in your project.

    • web.xml contains servlet configuration and mappings.

    • spring.xml is empty by default and can be used to configure your Spring beans.

    • pfixlog.xml contains the log4j configuration.

  • The src/main/webapp folder also contains resource folders. These are:

    • The css and img folders contain CSS files and images which should be delivered as static resources.

    • The txt folder contains the web pages, i.e. the according XML content and fragments.

    • 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 (not including XSL stylesheets provided by Pustefix itself).

2.3. Creating the pages

Start implementing your application by adding the three needed pages to the application: EnterData, ReviewData and Confirm.

New pages are added by editing the src/main/webapp/WEB-INF/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.

You can just remove or alter the according tags for the predefined pages. The new configuration should look like this:

<?xml version="1.0" encoding="utf-8"?>
<make lang="en_GB" project="firstapp">

  <!--
    Metatags have been left out.
  -->
  
  <standardpage name="EnterData" xml="xml/frame.xml"/>
  <standardpage name="ReviewData" xml="xml/frame.xml"/>
  <standardpage name="Confirm" xml="xml/frame.xml"/>
  
</make>

You can now open the EnterData page by browsing to http://localhost:8080/firstapp/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 txt/pages/EnterData.xml is missing.

This problem can easily be solved by adding the file src/main/webapp/txt/pages/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>
    </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 (you can remove/alter the predefined pages which are no longer used).

2.4. Setting the entry page

If you open the application in your browser without specifying the page directly, Pustefix will redirect you to xml/app/Home, which is the default page generated by the Maven archetype (you will get an error message, if you already removed the page in the previous step). Desired behaviour would be, that the EnterData page is displayed, when your application is started. This can be changed in the configuration in src/main/webapp/WEB-INF/app.xml:

<?xml version="1.0" encoding="utf-8"?>
<context-xml-service-config>
  
  <global-config />

  <context defaultpage="EnterData">
    ...
  </context>

  ...

</context-xml-service-config>

The entry page is specified using the defaultpage attribute of the <context/> tag. After you set this attribute to EnterData open the URL http://localhost:8080/firstapp 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 completely rid of the generated/predefined pages by following these steps (if not already done before):

  1. Delete the XML files for the predefined pages from the src/main/webapp/txt/pages folder.

  2. Remove the <pagerequest/> and <pageflow> tags for the predefined pages from the src/main/webapp/WEB-INF/app.xml file.

  3. Remove the <page/> and <standardpage/> tags for the predefined pages from the src/main/webapp/WEB-INF/depend.xml file.

2.5. Create the input form

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.

2.6. Implementing the business logic

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.

2.6.1. Implementing a wrapper

When implementing the business logic you will always start by implementing a wrapper. Wrappers in Pustefix usually are implemented using XML, which will then be used to generate a Java class for the wrapper. Alternatively Wrappers can be defined using annotated Java beans (see the reference documentation).

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 (if you started with the Maven archetype, the package should already exist). 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 xmlns="http://www.pustefix-framework.org/2008/namespace/iwrapper"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.pustefix-framework.org/2008/namespace/iwrapper 
                               http://www.pustefix-framework.org/2008/namespace/iwrapper.xsd">
  
  <!-- 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:

  1. 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).

  2. 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 mvn generate-sources to generate the Java class for this wrapper:

$ mvn generate-sources
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Building Pustefix Basic Application
[INFO]    task-segment: [generate-sources]
[INFO] ------------------------------------------------------------------------
...
[INFO] [pustefix-iwrapper:generate {execution: default}]
[INFO] Generated 1 IWrapper class
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
...

After Maven has finished the build, you will find a new class org.pustefixframework.tutorial.firstapp.wrapper.EnterUserDataWrapper in the target/generated-sources/iwrapper folder of your installation. This file contains all information needed to extract the parameters from the request.

If you refresh your IDE or compile you will get an error because the generated class references an IHandler class which doesn't exist and will be created later in this tutorial.

Assigning the wrapper to the page

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.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 <wrapper/> tag which requires two parameters to be set:

  1. class specifies the classname of the wrapper.

  2. 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"?>
  <context-xml-service-config>
  
  <!-- ... -->

  <pagerequest name="EnterData">
    <input>
      <wrapper prefix="user" class="org.pustefixframework.tutorial.firstapp.wrapper.EnterUserDataWrapper" />
    </input>
  </pagerequest>

</context-xml-service-config>

The next step will be implementing a handler processing the wrapped data. 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.

2.6.2. Interlude: Implementing a bean

[Note]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.

2.6.3. Implementing a handler

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:

  1. It has to implement the de.schlund.pfixcore.generator.IHandler interface.

  2. 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 (re-)start your application and 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 restart the application and open the page again, all form fields will be displayed. To test your form, fill out at least the mandatory fields:

  • gender

    name

    email

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.

2.6.4. Implementing a ContextResource

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:

  1. There has to be an interface and an implementation for the context resource.

  2. 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.xml:

<?xml version="1.0" encoding="utf-8"?>
<context-xml-service-config xmlns="http://www.pustefix-framework.org/2008/namespace/context-xml-service-config" 
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
      version="1.0" xsi:schemaLocation="http://www.pustefix-framework.org/2008/namespace/context-xml-service-config 
      ../../core/schema/context-xml-service-config.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 -->  

</context-xml-service-config>

2.6.5. Accessing a ContextResource

Now, that you have created a new context resource, you will learn, how you can access it from your application. 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);
    }

}

2.7. Implementing the workflow

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"?>
<context-xml-service-config xmlns="http://www.pustefix-framework.org/2008/namespace/context-xml-service-config" 
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
      version="1.0" xsi:schemaLocation="http://www.pustefix-framework.org/2008/namespace/context-xml-service-config 
      ../../core/schema/context-xml-service-config.xsd">

  <!-- Rest of configuration -->  

  <pageflow name="RegisterUser" final="Confirm">
    <flowstep name="EnterData"/>
    <flowstep name="ReviewData" stophere="true"/>
  </pageflow>

  <!-- Rest of configuration -->  

</context-xml-service-config>

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.

2.8. Displaying errors

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.

2.9. Displaying the entered data

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.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"?>
<context-xml-service-config xmlns="http://www.pustefix-framework.org/2008/namespace/context-xml-service-config" 
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0" 
       xsi:schemaLocation="http://www.pustefix-framework.org/2008/namespace/context-xml-service-config 
       ../../core/schema/context-xml-service-config.xsd">

  <!-- other configuration options -->
  
  <pagerequest name="ReviewData">
    <output>
      <resource node="user" class="org.pustefixframework.tutorial.firstapp.contextresources.ContextUser"/>
    </output>
  </pagerequest>

</context-xml-service-config>

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.

2.10. Saving the data

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 xmlns="http://www.pustefix-framework.org/2008/namespace/iwrapper"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.pustefix-framework.org/2008/namespace/iwrapper 
                          http://www.pustefix-framework.org/2008/namespace/iwrapper.xsd">
  
  <!-- 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"?>
<context-xml-service-config xmlns="http://www.pustefix-framework.org/2008/namespace/context-xml-service-config" 
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0" 
      xsi:schemaLocation="http://www.pustefix-framework.org/2008/namespace/context-xml-service-config 
      ../../core/schema/context-xml-service-config.xsd">
  
  <pagerequest name="ReviewData">
    <input>
      <wrapper prefix="user" class="org.pustefixframework.tutorial.firstapp.wrapper.SaveUserDataWrapper" />
    </input>
    <output>
      <resource node="user" class="org.pustefixframework.tutorial.firstapp.contextresources.ContextUser"/>
    </output>
  </pagerequest>

</context-xml-service-config>

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.

2.11. The finishing touch

Although your application is already working, there are some minor problems that you should deal with.

2.11.1. Changing data

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.

2.11.2. Avoiding unwanted access

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.

2.12. Conclusion

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.

Usermanager tutorial

Tobias Fehrenbach

In this tutorial you will learn how to work with the advanced feature provided by the Pustefix Framework. Like in the basic tutorial you will accept user input, store it in the session and display the data. Furthermore you will be able to delete and edit the data stored in the session.

The requirements for your application are:

  1. 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.

  2. Validate the user data after the page has been submitted and display error information.

  3. If the entered data is correct, move to an Overview page, which displays a list of user data and allows the user to delete, edit or add new users.

  4. If you edit the user data, the page with the HTML form shows up with prefilled values.

  5. If you choose to delete the user data the data will be removed from session context.

3.1. Setup

To build this tutorial, create a new project usermanagement as described in Section 2.1, “Setting up a new project” with the following data:

Property Value
groupId org.pustefixframework.tutorial
artifactId usermanagement
version default
package org.pustefixframework.tutorial.usermanagement
pustefixVersion the latest 0.16.x version

3.2. Create a new bean

Create a simple Java-bean User in the package org.pustefixframework.tutorial.usermanagement where you can store your user user data.

package org.pustefixframework.tutorial.usermanagement;

import java.net.URL;
import java.util.Date;

public class User {
    private int id;
    private String name;
    private String email;
    private Date birthday;
    private boolean admin;
    private URL homepage;
    private String gender;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    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 Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public boolean getAdmin() {
        return admin;
    }

    public void setAdmin(boolean admin) {
        this.admin = admin;
    }

    public URL getHomepage() {
        return homepage;
    }

    public void setHomepage(URL homepage) {
        this.homepage = homepage;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }
}

3.3. Create the handler

Create a new class UserHandler which implements de.schlund.pfixcore.generator.IHandler

package org.pustefixframework.tutorial.usermanagement;

import de.schlund.pfixcore.generator.IHandler;
import de.schlund.pfixcore.generator.IWrapper;
import de.schlund.pfixcore.workflow.Context;

public class UserHandler implements IHandler {

    public void handleSubmittedData(Context context, IWrapper wrapper) throws Exception {
    }

    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 wrapper) throws Exception {
        
    }
}

3.4. Create a caster

In order to cast the String parameter from the HTML form to the java.net.URL object we defined in the bean above, you need to create a caster.

3.4.1. Create a new statuscode for the caster

First of all you need a new statuscode to show an appropriate errormessage. Since this is your first statuscode you have to setup this first.

Create the file src/main/webapp/dyntxt/statuscodeinfo.xml with the following content.

<?xml version="1.0" encoding="UTF-8"?>
<statuscodeinfo xmlns="http://www.pustefix-framework.org/2008/namespace/statuscodeinfo" 
                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                xsi:schemaLocation="http://www.pustefix-framework.org/2008/namespace/statuscodeinfo 
                                    http://www.pustefix-framework.org/2008/namespace/statuscodeinfo.xsd">

  <statuscodes class="org.pustefixframework.tutorial.StatusCodeLib">
    <file>statusmessages.xml</file>
  </statuscodes>
  
</statuscodeinfo>

This configuration file tells you application that the statuscodes can be found in the file statusmessages.xml in the same directory

You need to create the file src/main/webapp/dyntxt/statusmessages.xml and add an statuscode for an invalid url.

<?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="caster.url.INVALID">
    <theme name="default">
      The URL you entered is not valid
    </theme>
  </part>
</include_parts>

In order to be able to use the Status Code in the following code, you have to create the associated sources using the mvn generate-sources command.

3.4.2. Create the ToURL-caster

Create a new class ToURL in the package org.pustefixframework.tutorial.caster which extends SimpleCheck. The ToURL class casts the request param to an java.net.URL object and adds the new created statuscode if the url is malformed.

package org.pustefixframework.tutorial.caster;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import org.pustefixframework.tutorial.usermanagement.StatusCodes;

import de.schlund.pfixcore.generator.IWrapperParamCaster;
import de.schlund.pfixcore.generator.SimpleCheck;
import de.schlund.pfixxml.RequestParam;
import de.schlund.util.statuscodes.StatusCode;

public class ToURL extends SimpleCheck implements IWrapperParamCaster {

    private URL[] value = null;
    private StatusCode scode;
    
    public ToURL() {
        scode = StatusCodeLib.CASTER_URL_INVALID;
    }
    
    public void castValue(RequestParam[] requestParams) {
        List<URL> out = new ArrayList<URL>();
        URL url;
        for (RequestParam param : requestParams) {
            try {
                url = new URL(param.getValue());
                out.add(url);
            } catch(MalformedURLException ex) {
                addSCode(scode);
            }
        }
        if (!errorHappened()) {
            value = out.toArray(new URL[] {});
        }
    }

    public Object[] getValue() {
        return value;
    }
}

3.5. Annotate the User bean

Instead of creating an iwrp file described in Section 2.6.1, “Implementing a wrapper” you can now use annotations to create the wrapper class. Therefor you need to annotate the class and the getters of the user bean from above.

3.5.1. Annotate the class declaration

To tell the pustefix build system to create an wrapper class from the bean you need to add the @IWrapper annotation to the class declaration. You can specify the name of the generated wrapper class and the corresponding handler class.

import de.schlund.pfixcore.generator.annotation.IWrapper;
@IWrapper(name="UserWrapper", ihandler=UserHandler.class)
public class User {
    ...

3.5.2. Annotate the getter

The @Param annotation is used to mark a bean property as parameter. You can select the name of the parameter and choose if it is mandatory or optional.

import de.schlund.pfixcore.generator.annotation.Caster;
import de.schlund.pfixcore.generator.annotation.Param;
import de.schlund.pfixcore.generator.annotation.Transient;
import de.schlund.pfixcore.oxm.impl.annotation.DateSerializer;
import org.pustefixframework.tutorial.caster.ToURL;

The @Transient annotation is used to avoid the generation of a wrapper parameter.

@Transient
public int getId() {
    ...
@Param(name="name")
public String getName() {
    ...
@Param(name="email")
public String getEmail() {
    ...

The @DateSerializer("format") annotation tells the wrapper that it should use the given serializer class to serialize this property.

@Param(name="birthday")
@DateSerializer("yyyy/MM/dd")
public Date getBirthday() {
    ...
@Param(name="admin", mandatory=false)
public boolean getAdmin() {
    ...

The @Caster annotation notes the caster implementation class. Here you add your ToURL caster implementation from Section 3.4, “Create a caster”.

@Param(name="homepage", mandatory=false)
@Caster(type=ToURL.class)
public URL getHomepage() {
    ...
@Param(name="gender")
public String getGender() {
    ...

3.6. Generate wrapper

Type mvn generate-sources to generate the wrapper.

You have to add all subfolders of target/generated-sources to your Java Buildpath of your project (in Eclipse, go to Project Properties - Java BuildPath - Tab "Source" - "Add Folder..." and check all subfolders of target/generated-sources). You can find the new class org.pustefixframework.tutorial.usermanagement.UserWrapper in the target/generated-sources/apt directory of your project.

3.7. Create a userlist class

You need a class to hold your users. Therefor create a new class org.pustefixframework.tutorial.usermanagement.UserList with the property users and methods to get one specific users, get all users and add a new user. Further methods will be added later.

package org.pustefixframework.tutorial.usermanagement;

import java.util.ArrayList;
import java.util.List;

public class UserList {
    
    private List<User> users = new ArrayList<User>();
    private int id = 0;
    
    public void addUser(User user) {
        user.setId(id);
        users.add(user);
        id++;
    }
    
    public List<User> getUsers() {
        return users;
    }
    
    public User getUser(int id) {
        for (User user : users) {
            if (user.getId() == id) {
                return user;
            }
        }
        return null;
    }
}

3.8. Adjust the servlet configuration

Get rid of the old sample stuff by removing the sample pageflow, the pagerequest elements and the defaultflow attribute of the context element

Add this to the servlet configuration file user.conf.xml. The context element should look like the following snippet:

<context>
  <resource class="org.pustefixframework.tutorial.usermanagement.UserList" />
</context>

3.9. Add a new user

Adding a new user is quite simple. Implement the handleSubmittedData method of the UserHandler you created in Section 3.3, “Create the handler”. You create a new User object by calling IWrapperToBean.createBean(IWrapper iwrapper, Class<T> clazz), get the UserList from session context and add the user to the userList instance.

import de.schlund.pfixcore.generator.iwrpgen.IWrapperToBean;
User user = IWrapperToBean.createBean(wrapper, User.class);
UserList userList = context.getContextResourceManager().getResource(UserList.class);
userList.addUser(user);

3.10. Create the pages

Here you create two pages: One page for the user data input and one page to list the user data. To keep this application clean get rid of the sample pages by removing the sample pages in the directory src/main/webapp/txt/pages.

3.10.1. Create a page for user data input

Create a new file Userform.xml in the directory src/main/webapp/txt/pages. This page will contain the HTML form to submit the user data.

<?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">
      <pfx:forminput>
        Name: <pfx:xinp type="text" name="user.name" />
        <pfx:checkfield name="user.name">
          <pfx:error><pfx:scode/></pfx:error>
        </pfx:checkfield>
        <br/>

        E-Mail: <pfx:xinp type="text" name="user.email" />
        <pfx:checkfield name="user.email">
          <pfx:error><pfx:scode/></pfx:error>
        </pfx:checkfield>
        <br/>

        Birthday: <pfx:xinp type="text" name="user.birthday" />
        <pfx:checkfield name="user.birthday">
          <pfx:error><pfx:scode/></pfx:error>
        </pfx:checkfield>
        <br/>

        Gender: 
        <pfx:xinp type="select" name="user.gender">
          <pfx:option value="m" >male</pfx:option>
          <pfx:option value="f" >female</pfx:option>
        </pfx:xinp>
        <pfx:checkfield name="user.gender">
          <pfx:error><pfx:scode/></pfx:error>
        </pfx:checkfield>
        <br/>

        Homepage: <pfx:xinp type="text" name="user.homepage" />
        <pfx:checkfield name="user.homepage">
          <pfx:error><pfx:scode/></pfx:error>
        </pfx:checkfield>
        <br/>
        
        Admin: <pfx:xinp type="check" name="user.admin" value="true" default="false" />
        <br/>      
        <pfx:xinp type="submit" value="save"/>
      </pfx:forminput>  
    </theme>
  </part>
</include_parts>

3.10.2. Create page for listing users

Create a new file Overview.xml in the directory src/main/webapp/txt/pages. This page shows all users which were stored in the session context. Later this page will provide links to delete and edit users.

<?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">
      <h3>Users</h3>
      <ixsl:for-each select="/formresult/users/users/user">
        Name: <ixsl:value-of select="./@name"/><br/>
        Email: <ixsl:value-of select="./@email"/><br/>
        Birthday: <ixsl:value-of select="./@birthday"/><br/>
        Gender: <ixsl:value-of select="./@gender"/><br/>
        <ixsl:if test="./@homepage">
          Homepage: <ixsl:value-of select="./@homepage"/><br/>
        </ixsl:if>
        Admin: <ixsl:value-of select="./@admin"/><br/>
        <br/><br/>        
      </ixsl:for-each>

      <pfx:button page="Userform">add</pfx:button>
    </theme>
  </part>
</include_parts>

3.10.3. Add pages to xml configuration files

user.conf.xml

Add two new pagerequest elements to the app.xml configuration file below the context element.

<pagerequest name="Userform">
  <input>
    <wrapper prefix="user" class="org.pustefixframework.tutorial.usermanagement.UserWrapper" />
  </input>
</pagerequest>
<pagerequest name="Overview">
  <output>
    <resource node="users" class="org.pustefixframework.tutorial.usermanagement.UserList" />
  </output>
</pagerequest>

Set the Overview page as defaultpage by adding the attribute to the context element as described in Section 2.4, “Setting the entry page”.

<context defaultpage="Overview">

depend.xml

Remove the sample page element and the sample standardpage alement.

Add the standardmetatags element:

<standardpage name="Userform" xml="xml/frame.xml"/>
<standardpage name="Overview" xml="xml/frame.xml"/>

3.11. Add a pageflow

Place a new pageflow element between context and pagerequest elements in the servlet configuration file user.conf.xml.

<pageflow name="userFlow" final="Overview">
  <flowstep name="Userform"/>
  <flowstep name="Overview"/>
</pageflow>

3.12. Try to add your own user

Type mvn tomcat:run to start the web-application.

Now you can point your browser to http://localhost:8080/usermanagement/

If everything works fine you should see this:

3.13. Add sample users

If you tried out your application you've seen that the Overview page is empty unless you add new users. You can add some sample users by calling the addUser method of UserList directly.

3.13.1. Add new constructors to your UserList

To add sample users more easily add an constructor to set the mandatory data at once.

public User(String name, String email, Date birthday, boolean admin, URL homepage, String gender) {
  this.name = name;
  this.email = email;
  this.birthday = birthday;
  this.admin = admin;
  this.homepage = homepage;
  this.gender = gender;
}

Further you need a default constructor here.

public User() {
}

3.13.2. Add init method to UserList

To add the users when the context resource is initialized you need to add a method annotated with @InitResource.

import java.util.GregorianCalendar;
import java.net.URL;
import de.schlund.pfixcore.beans.InitResource;
@InitResource
public void createSampleUsers() throws Exception {
  addUser(new User("Neo", "neo@pustefix-framework.org", new GregorianCalendar(1964, 8, 2).getTime(), true, 
      new URL("http://pustefix-framework.org"), "m"));
  addUser(new User("Trinity", "trinity@pustefix-framework.org", new GregorianCalendar(1967, 7, 21).getTime(), true, 
      new URL("http://pustefix-framework.org"), "f"));
  addUser(new User("Morpheus", "morpheus@pustefix-framework.org", new GregorianCalendar(1961, 6, 30).getTime(), true, 
      new URL("http://pustefix-framework.org"), "m"));
}

When you try this out again (see Section 3.12, “Try to add your own user”) you should see the following screen:

3.14. New feature: Delete a user

To remove an existing user from the UserList you need to create a new bean which stores the selected user id, a new handler and a delete method in the UserList class.

3.14.1. Create a new bean

Create another bean org.pustefixframework.tutorial.usermanagement.DeleteUser. This bean stores the id of the selected user.

package org.pustefixframework.tutorial.usermanagement;

public class DeleteUser {
    private int id;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
}

3.14.2. Create a new handler

Create a new handler org.pustefixframework.tutorial.usermanagement.DeleteUserHandler. This handler gets the selected user id from the wrapper and calls the remove method from UserList.

package org.pustefixframework.tutorial.usermanagement;

import de.schlund.pfixcore.generator.IHandler;
import de.schlund.pfixcore.generator.IWrapper;
import de.schlund.pfixcore.workflow.Context;

public class DeleteUserHandler implements IHandler {

    public void handleSubmittedData(Context context, IWrapper wrapper) throws Exception {
    }

    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 wrapper) throws Exception {
    }

}

3.14.3. Annotate the DeleteUser bean

Annotate the class declaration

import de.schlund.pfixcore.generator.annotation.IWrapper;
@IWrapper(name="DeleteUserWrapper", ihandler=DeleteUserHandler.class)
public class DeleteUser {

Annotate the getter

import de.schlund.pfixcore.generator.annotation.Param;
@Param(name="id")
public int getId() {

3.14.4. Generate the wrapper class

Type mvn generate-sources to generate the wrapper class.

3.14.5. Add a delete user method

Add a new method deleteUser(int id) to remove an existing user from UserList.

public void deleteUser(int id) {
    User userToDelete = getUser(id);
    if (userToDelete != null) {
        users.remove(userToDelete);
    }
}

3.14.6. Delete a user

To delete a user implement the method handleSubmittedData of DeleteUserHandler. First create a new DeleteUser bean with IWrapperToBean.createBean(Iwrapper wrapper, Class<T> and get your UserList instance from session context. Then call the deleteUser(Integer id) method.

import de.schlund.pfixcore.generator.iwrpgen.IWrapperToBean;
public void handleSubmittedData(Context context, IWrapper wrapper) throws Exception {
    DeleteUser deleteUser = IWrapperToBean.createBean(wrapper, DeleteUser.class);
    UserList userList = context.getContextResourceManager().getResource(UserList.class);
    userList.deleteUser(deleteUser.getId());
}

3.14.7. Add a delete button

You need a button to trigger to deletion of a user. Simply add the following code inside the for-each loop after the admin text in the Overview.xml file

<pfx:button>delete
  <pfx:command name="SELWRP">delete</pfx:command>
  <pfx:argument name="delete.id"><ixsl:value-of select="./@id"/></pfx:argument>
</pfx:button>

3.14.8. Adjust the servlet configuration

Add the input element to the existing pagerequest of the Overview page.

<input>
  <wrapper prefix="delete" class="org.pustefixframework.tutorial.usermanagement.DeleteUserWrapper" />
</input>

3.15. New feature: Edit a user

The last feature in this tutorial is to edit an existing user

3.15.1. Create a new bean

Create another bean org.pustefixframework.tutorial.usermanagement.EditUser. This bean stores the id of the selected user and if the user is currently being edited.

package org.pustefixframework.tutorial.usermanagement;


public class EditUser {
    private int id;
    private boolean editing = false;

    public boolean isEditing() {
        return editing;
    }

    public void setEditing(boolean editing) {
        this.editing = editing;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
}

3.15.2. Create a new handler

Create a new handler org.pustefixframework.tutorial.usermanagement.EditUserHandler. This handler gets the selected user id from the wrapper and loads the selected user.

package org.pustefixframework.tutorial.usermanagement;

import de.schlund.pfixcore.generator.IHandler;
import de.schlund.pfixcore.generator.IWrapper;
import de.schlund.pfixcore.workflow.Context;

public class EditUserHandler implements IHandler {

    public void handleSubmittedData(Context context, IWrapper wrapper) throws Exception {
    }

    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 wrapper) throws Exception {
    }

}

3.15.3. Annotate the EditUser bean

To generate a wrapper from this bean you have to annotate it.

Annotate the class

import de.schlund.pfixcore.generator.annotation.IWrapper;
@IWrapper(name="EditUserWrapper", ihandler=EditUserHandler.class)
public class EditUser {

Annotate the getter for id

import de.schlund.pfixcore.generator.annotation.Param;
@Param(name="id", mandatory=true)
public int getId() {

Make the getter for editing transient

import de.schlund.pfixcore.generator.annotation.Transient;
@Transient
public boolean isEditing() {

3.15.4. Generate the wrapper class

Type mvn generate-sources to generate the wrapper.

3.15.5. Add a method to replace a user of our UserList class

To edit a user you need to implement the method to replace an existing user. We add the following code to the UserList class.

public void replaceUser(User user) {
    User userToReplace = getUser(user.getId());
    if (userToReplace != null) {
        users.remove(userToReplace);
        users.add(user);
    }
}

3.15.6. Edit a user

To edit a user implement the method handleSubmittedData of EditUserHandler. First create a new EditUser bean with IWrapperToBean.createBean(Iwrapper wrapper, Class<T> and get your EditUser instance from session context. Then call the setId(Integer id) method. This stores the id of the user in the session context.

import de.schlund.pfixcore.generator.iwrpgen.IWrapperToBean;
public void handleSubmittedData(Context context, IWrapper wrapper) throws Exception {
    EditUser editUser = IWrapperToBean.createBean(wrapper, EditUser.class);
    EditUser editUserContext = context.getContextResourceManager().getResource(EditUser.class);
    editUserContext.setId(editUser.getId());
    editUserContext.setEditing(true);
}

3.15.7. Add an edit button

We need a button to edit a user. We add the following code below the delete button in the Overview.xml file.

<pfx:button jumptopage="Userform">edit
  <pfx:command name="SELWRP">edit</pfx:command>
  <pfx:argument name="edit.id"><ixsl:value-of select="./@id"/></pfx:argument>
</pfx:button>

3.15.8. Adjust the servlet configuration

Add the EditUser class as an resource to the context element

<resource class="org.pustefixframework.tutorial.usermanagement.EditUser" />

Add the following interface element into the input element of the pagerequest of the Overview page.

<wrapper prefix="edit" class="org.pustefixframework.tutorial.usermanagement.EditUserWrapper" />

3.15.9. Add user to form

To add the user data to the existing form of the Userform page you need to implement the retrieveCurrentStatus method of your UserHandler class.

import de.schlund.pfixcore.generator.iwrpgen.BeanToIWrapper;
public void retrieveCurrentStatus(Context context, IWrapper wrapper) throws Exception {
    EditUser editUser = context.getContextResourceManager().getResource(EditUser.class);
    if (editUser.isEditing()) {
        UserList userList = context.getContextResourceManager().getResource(UserList.class);
        User user = userList.getUser(editUser.getId());
        BeanToIWrapper.populateIWrapper(user, wrapper);
    }
}

3.15.10. Replace method handleSubmittedData of UserHandler

To add and replace an existing user you have to edit the method handleSubmittedData:

public void handleSubmittedData(Context context, IWrapper wrapper) throws Exception {
    UserList userList = context.getContextResourceManager().getResource(UserList.class);
    EditUser editUser = context.getContextResourceManager().getResource(EditUser.class);
    User user = IWrapperToBean.createBean(wrapper, User.class);
    if (editUser.isEditing()) {
        // replace existing user
        user.setId(editUser.getId());
        userList.replaceUser(user);
        editUser.setEditing(false);          
    } else {
        // add new user
        userList.addUser(user);    
    }
}

3.16. Conclusion

In this tutorial you learned how to automatically create wrapper from annotated java 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. 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 annotations, configuration options, XML tags and interfaces in detail.

AJAX Calculator tutorial

Stephan Schmidt

In this tutorial you will learn how to create an AJAX application with Pustefix. As an example, you will build a very simple calculator, which does the real calculation in Java on the server. As in the other tutorials, the business logic has been kept extremely simple to focus on the tasks that are required to implement an AJAX application with the Pustefix framework.

No matter how complex your business logic is, the tasks required to set up an AJAX application will be almost the same as in this tutorial.

4.1. Setup

To build this tutorial, create a new project calculator with one servlet as described in Section 2.1, “Setting up a new project” with the following data:

Property Value
groupId org.pustefixframework.tutorial
artifactId calculator
version default
package org.pustefixframework.tutorial.calculator
pustefixVersion the latest 0.16.x version

You will also need additional dependencies in your pom.xml as described below. The version might differ from the version of pustefix-core, go to http://pustefix-framework.org/repository/maven/org/pustefixframework/webservices/ to see which versions are actually available.

<dependency>
    <groupId>org.pustefixframework.webservices</groupId>
    <artifactId>pustefix-webservices-core</artifactId>
    <version>0.16.2</version>
</dependency>
<dependency>
    <groupId>org.pustefixframework.webservices</groupId>
    <artifactId>pustefix-webservices-jsonws</artifactId>
    <version>0.16.2</version>
</dependency>
<dependency>
    <groupId>org.pustefixframework.webservices</groupId>
    <artifactId>pustefix-webservices-jaxws</artifactId>
    <version>0.16.2</version>
</dependency>

Although you are building an AJAX application and will not need any wrappers and handlers which process your submit requests, you still need at least one HTML page as a starting point for your application. Please create a page called Calculator by adding the necessary tags to the depend.xml as described in Section 2.3, “Creating the pages”. As the pages itself do not trigger any business logic, there is no need to make the page known in the servlet configuration file.

Set the new Calculator page as the default page as described in Section 2.4, “Setting the entry page”

4.2. Using the webservice servlet

In AJAX applications, you will need a servlet, that is able to process the AJAX requests sent by the client. Pustefix provides a ready-to-use servlet, that is able to expose any Java class as a webservice and process requests made by the client. To use this servlet, open the project.xml configuration file and add the the webservice element at the end of the file.

<?xml version="1.0" encoding="utf-8"?>
<project-config xmlns="http://www.pustefix-framework.org/2008/namespace/project-config">

<project>
  <application>

    <!-- auto-generated content -->

    <!-- Add these lines manually above <static> and <choose> -->
    <webservice-service>
      <path>/webservice</path>
      <config-file>docroot:/WEB-INF/webservice.conf.xml</config-file>
    </webservice-service>
    
    <static>
      <!-- ... -->
    </static>
    
    <choose>
      <!-- ... -->
    </choose>

  </application>
</project-config>

Instead of using the ContextXMLServlet for the servlet, you need to use the org.pustefixframework.webservices.WebServiceServlet instead. To configure the webservice servlet you add the docroot:/WEB-INF/webservice.conf.xml file in the <propfile/> tag.

This webservice.conf.xml file makes use of the XML syntax like all the previous configuration files you encountered in Pustefix. Add the following content to the file:

<webservice-config
  xmlns="http://www.pustefix-framework.org/2008/namespace/webservice-config"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.pustefix-framework.org/2008/namespace/webservice-config ../../core/schema/webservice-config.xsd">
  
  <webservice-global>
    <requestpath>/webservice</requestpath>
    <wsdlsupport enabled="false"/>
    <stubgeneration enabled="false"/>
    <protocol type="JSONWS"/>
    <session type="servlet"/>
    <scope type="application"/>
    <admin enabled="true"/>
    <monitoring enabled="true" scope="session" historysize="10"/>
    <logging enabled="true"/>
    <faulthandler class="org.pustefixframework.webservices.fault.LoggingHandler"/>
  </webservice-global>
</webservice-config>

Table 4.1, “Global webservice configuration ” lists all used configuration options and describes their usage in this example.

Table 4.1. Global webservice configuration
Option Description
requestpath The path where the webservice requests will be sent to.
wsdlsupport Whether WSDL for SOAP webservices should be generated. As you will be using a JSON protocol instead of SOAP, this is not needed.
stubgeneration Whether JavaScript stub classes should be generated in the build process. Again, this is not needed when using a JSON-based protocol.
protocol The protocol to use. In this example, you will be using the JSONWS protocol, which is very similar to JSON-RPC.
session Which session type to use. Using servlet means that there must be an HTTP-session that can be used in your services.
scope Specifies the scope in which your web services will be executed. When using application, the service object is created only once and reused for all requests.
admin Whether to enable the web service admin console.
monitoring Whether to enable the web service monitoring, which is useful for debgging.
logging Whether to enable the pustefix-webservice.log, which contains all requests and responses.
faulthandler Specifies a class that will receive all exceptions that are thrown in your web services.

You have now configured the WebServiceServlet and it is now available at http://localhost:8080/calc/webservice/. If you open this URL in your browser, you will get an HTTP Code 400 Bad Request as you made a simple HTTP request instead of a webservice request.

To make web service requests, you will at first have to implement a service that you can send requests to.

4.3. Implementing the business logic

In Pustefix, a web service always has to consist of a service interface and an implementation. In this tutorial, you will implement a very simple webservice which provides three methods to execute mathematical operations.

  • Add two integer numbers

  • Subtract two integer numbers

  • Multiply two integer numbers

The interface for this business logic is very easy to implement:

package org.pustefixframework.tutorial.calculator;

public interface CalculatorService {
    public int add(int a, int b);
    public int subtract(int a, int b);
    public int multiply(int a, int b);
}

The implementation for this interface is not any harder to implement than the interface iteself:

package org.pustefixframework.tutorial.calculator;

import org.pustefixframework.webservices.AbstractService;

public class CalculatorServiceImpl extends AbstractService implements CalculatorService {
    public int add(int a, int b) {
        return a+b;
    }

    public int subtract(int a, int b) {
        return a-b;
    }

    public int multiply(int a, int b) {
        return a*b;
    }
}

As you can see, the CalculatorServiceImpl implements the CalculatorService service interface and extends the abstract class AbstractService provided by Pustefix. All of your web service implementations should extend this class to inherit some methods that are useful when working with web services.

[Tip]Accessing the context

The AbstractService class provides a getContext method which gives you access to the Context of the current user. This allows you to access any data that your application stored in the context.

4.4. Exposing the service

Now that you have defined the interface for your service and also provided an implementation for it, the next step is to expose this service using the WebServiceServlet provided by Pustefix

This is done by adding some additional configuration options to the webservice.conf.xml file you created earler (in Section 4.2, “Using the webservice servlet”). For each webservice you want to expose, you have to add one <webservice/> tag in this configuration file:

<webservice-config
  xmlns="http://www.pustefix-framework.org/2008/namespace/webservice-config"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.pustefix-framework.org/2008/namespace/webservice-config ../../core/schema/webservice-config.xsd">
  
  <!-- global config -->
  
  <webservice name="CalculatorService">
    <interface name="org.pustefixframework.tutorial.calculator.CalculatorService"/>
    <implementation name="org.pustefixframework.tutorial.calculator.CalculatorServiceImpl"/>
  </webservice>

</webservice-config>

Each of these webservice configurations requires at least the following options to be set:

Table 4.2. Local webservice configuration
Option Description
name The name attribute specifies the name of the webservice. This name does not have to match the name of the Java class. It will be used to identify the service, when the web service servlet is requested by a client.
interface The <interface/> tag specifies the name of interface for the web service. Only methods defined in this interface will be exposed as web service methods.
implementation The <implementation/> tag references the actual implementation of the service.

Also, most of the global options (see Table 4.1, “Global webservice configuration ”) can be used in the local web service configuration to override them for the specified service.

[Tip]Accessing the context

If your web service extends the AbstractService class and you want to access the Context of your application, you can use the <context/> tag to reference the context you want to access in your service:

<context name="pfixtutorial:ajax-calculator::servlet:calc"/>

4.5. Consuming the service

With these steps, you have finished all Java and configuration work packages and can continue implementing the client side of your application. Open the Calculator.xml file and add some HTML as a frontend for your calculator application:

<?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">
      <!-- Include external JavaScript -->
      <pfx:include part="javascript-includes"/>

      <h1>AJAX Calculator Tutorial</h1>
      <fieldset>
        <legend>Calculator</legend>
        A: <input type="text" id="a"/><br/>
        B: <input type="text" id="b"/><br/>
        <input type="button" value="add" onClick="add();"/>
        <input type="button" value="subtract" onClick="subtract();"/>
        <input type="button" value="multiply" onClick="multiply();"/>
      </fieldset>
    </theme>
  </part>
</include_parts>

The HTML page contains several controls:

  • Two text input fields (with ids a and b), in which the user enters the two values, that are used for the calculation.

  • A button labeled add which will call the JavaScript function add() when clicked.

  • A button labeled subtract which will call the JavaScript function subtract() when clicked.

  • A button labeled multiply which will call the JavaScript function multiply() when clicked.

To implement an AJAX application, you will have to use JavaScript. This has been left out in the last example of your page, there is only a <pfx:include/> tag, which includes a second part javascript-includes defined in the same page. This part is used to separate the HTML from all your JavaScript includes and make the page more maintainable.

Pustefix does not only provide server side implementation helpers for your AJAX applications, but also some client side JavaScript classes which hide all transportation details from you. Pustefix will also generate JavaScript stub classes for the Java services you exposed via the WebServiceServlet. To use the JSONWS implementation, you have to include the following files:

  • httpRequest.js provides an abstraction over the different XmlHttpRequest implementations in the different browsers.

  • webservice_json.js provides an abstraction of the JSONWS protocol used by Pustefix.

  • The JavaScript stub for your calculator service is not available as a file, but is generated on demand by the servlet. To include this generated JavaScript code, Pustefix provides the <pfxwsscript/> tag, which takes the name of the service (as specified in the <webservice/> tag in your web service configuration) in the name attribute. This tag will generate the <script/> tag that requests the generated JavaScript code from the web service servlet.

The following listing shows the javascript-includes part, which is referenced by the frontend of your application and has to be included in your Calculator.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">
    <!-- content, including part "javascript-includes", see above -->
  </part>
  
  <part name="javascript-includes">
    <theme name="default">
      <script type="text/javascript" src="{$__contextpath}/modules/pustefix-core/script/httpRequest.js"></script>
      <script type="text/javascript" src="{$__contextpath}/modules/pustefix-webservices-jsonws/script/webservice_json.js"></script>
      <pfx:wsscript name="CalculatorService"/>
    </theme>
  </part>
</include_parts>

If you are taking a look at the generated JavaScript code, you will see, that this code defines a new class called WS_CalculatorService, which provides three methods:

  • add()

  • subtract()

  • divide()

These are exactly the same methods that the CalculatorService interface declared in your Java code.

//Autogenerated webservice stub (don't modify this code manually!)
function WS_CalculatorService(context) {
  pfx.ws.json.BaseStub.call(this,"CalculatorService",context);
}
WS_CalculatorService.prototype=new pfx.ws.json.BaseStub;
WS_CalculatorService.prototype.subtract=function() {
  return this.callMethod("subtract",arguments,2);
}
WS_CalculatorService.prototype.multiply=function() {
  return this.callMethod("multiply",arguments,2);
}
WS_CalculatorService.prototype.add=function() {
  return this.callMethod("add",arguments,2);
}

This class acts as a remote proxy for the Java business logic you implemented earlier. If you call any of these methods, the calculation will not be done on the client, but the request will be sent to the server, where it will be processed by the Java implementation. The response will then be sent back to the client and is available in your JavaScript application.

So all that is left to do, is implement the client side logic, that will create the proxy object and delegate the calculation to it. This is done in a new part javascript-logic, which you need to include in the content part of your application:

<?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">
      <!-- Include external JavaScript -->
      <pfx:include part="javascript-includes"/>
      <pfx:include part="javascript-logic"/>

      <!-- HTML has been left out --> 
    </theme>
  </part>
</include_parts>

In this part, you are using the <pfx:script/> tag, which is used to insert inline JavaScript into your page. In this script, you create a new instance of WS_Calculator and assign it to a global JavaScript variable.

Furthermore, you implement the three missing functions add(), subtract() and multiply(), which are called, if the corresponding buttons are clicked. All three methods are implemented the same way: The values of the two input fields are read and the corresponding method of the proxy object is called with these values. The return value of the proxy method is displayed to the user, using the alert() function.

<?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">
    <!-- content, see above -->
  </part>
  
  <part name="javascript-includes">
    <!-- see above -->
  </part>
  
  <part name="javascript-logic">
    <theme name="default">
      <pfx:script>
var calcProxy = new WS_CalculatorService();
function add() {
  var a = parseInt(document.getElementById('a').value);
  var b = parseInt(document.getElementById('b').value);
  alert(calcProxy.add(parseInt(a),parseInt(b)));
}
function subtract() {
  var a = parseInt(document.getElementById('a').value);
  var b = parseInt(document.getElementById('b').value);
  alert(calcProxy.subtract(parseInt(a),parseInt(b)));
}
function multiply() {
  var a = parseInt(document.getElementById('a').value);
  var b = parseInt(document.getElementById('b').value);
  alert(calcProxy.multiply(parseInt(a),parseInt(b)));
}
      </pfx:script>
    </theme>
  </part>
</include_parts>

Now you can open the page, enter any two integer numbers and click on any button. Your application will send the request to your business logic on the server and display the result on the client without reloading the page.

4.6. Conclusion

This tutorial showed you, how to implement an AJAX application based on Pustefix in several minutes. While this example only used a very simple business logic and transferred only primitive type, AJAX applications in Pustefix are not restricted to built-in Java types. Pustefix is able to send any Java-bean style objects from the client to the server and vice-versa. You only need to make sure, that there is an empty constructor in your beans.

Furthermore, the following features are also supported by Pustefix:

  • Access any property of the context (including context resources) in your services.

  • Switch from JSONWS to SOAP without changing any of your services.

  • Implement custom protocols or custom serializers to fit your special needs.

Of course, you can combine the AJAX features of Pustefix with any other feature in Pustefix with ease. For example, it is possible to force AJAX requests to be made secure or to restrict a service to users with sepcial roles attached.