Chapter 5. Important Concepts

Table of Contents

5.1. HTTP request handling
5.1.1. HttpRequestHandlers provided by the core framework
5.2. Processing of Requests
5.2.1. The Context
5.2.2. PageFlow
5.2.3. States
5.2.4. Influencing the page request cycle
5.2.5. The basic Pustefix Request Cycle
5.2.6. Pustefix State implementations
5.2.7. Pustefix PageFlow implementation: DataDrivenPageFlow
5.3. The data model: Context resources
5.4. Wrappers and Handlers
5.4.1. IWrappers
5.4.2. The IHandler interface
5.5. StatusCodes
5.6. ContextInterceptors

5.1. HTTP request handling

In a typical Pustefix application the DispatcherServlet provided by Spring is usually the only servlet. This servlet instantiates a Spring ApplicationContext on startup and delegates all requests to the HttpRequestHandler found by using the HandlerMapping. If you want to do request processing for a certain path without using the HttpRequestHandlers provided by Pustefix, you can add your own instance of an UriProvidingHttpRequestHandler to the Spring configuration file.

All request handlers used by Pustefix are based on AbstractPustefixRequestHandler. The main feature of it's session handling is that it's mostly transparent to the user. You can submit data to a URL, and the system will take care to store the supplied data (this is done in the form of a PfixServletRequest, which can be seen as basically the same as a HttpServletRequest, with some additional features, e.g. more or less transparent handling of file uploads), create a session, redirect to the URL with an embedded session id, and continue with the business logic and the data submitted with the original request afterwards.

Even SSL is handled transparently. A descending request handler must implement the method + needsSession(): boolean to decide if the the current request should result into the session to be "secure". You don't have to make sure that the webpages use "https://" in all needed links, the target itself tells the system that it want's to run under SSL.

If the request handler decides that it want's to run under SSL from now on, a complex redirect sequence happens that makes sure that all data is copied from the previous insecure session into a new, secure session (a session is secure if it's session id was never transmitted over an insecure channel). After a session has once transformed into a secure one, there's no way back: Every time you try to use the session with "http://" it will create a new, secure session dropping the old one (because it's tainted now). If the visitor's browser supports cookies, the system manages to map request with the old, original, insecure session id (which can happen whenever the user uses the back button of her browser to go back to a page that has still the old session id embedded in every link) to the new, currently running secure session and silenty and securely redirects to this session.

5.1.1. HttpRequestHandlers provided by the core framework

Figure 5.1, “Pustefix HTTP request handlers” shows some of the HttpRequestHandlers that are already provided by the core framework and should be sufficient for most of your needs when building a web application with Pustefix.

Figure 5.1. Pustefix HTTP request handlers

Pustefix HTTP request handlers

DerefRequestHandler

This is a small request handler that should be used whenever a link to a URL outside the own site is made. All such external links must be of the form /xml/deref?link=http://some.other.domain/foo/bar. This is important because for any link that goes directly to a foreign destination (e.g. <a href="http://some.other.domain/foo/bar">) the session id visible in the logfile of the foreign site's webserver (via the Referer header). This is of course a security problem. Using the DerefServlet avoids this by using a redirect loop so the Referer header transmitted will no longer contain the session id.

AbstractPustefixXMLRequestHandler

This HTTP request handler, while still abstract, implements most of the output handling needed for request handlers that want use XSLT/XML to produce the final html sent to the browser.

The only thing a descendant needs to implement is the abstract method getDom(PfixServletRequest), whose return value can be thought of as a small container around a org.w3c.Document. The additional data stored in a SPDocument is among other things a map of XSLT transformation parameters that should be set and the "page name" which the system uses to choose the correct target stylesheet to transform the DOM with.

PustefixContextDirectOutputRequestHandler

In contrast to AbstractPustefixXMLRequestHandler and descendants, this request handler does not produce it's output by transforming XML with a XSLT stylesheet into HTML. There are situations where you need to stream some other format like pdf or images like PNG or GIF instead. The PustefixContextDirectOutputRequestHandler is used for exactly this purpose, because it delegates request processing to DirectOutputState objects which are allowed to write directly to the HttpServletResponse's OutputStream.

PustefixContextXMLRequestHandler

This is the main request handler that handles almost all pages in a typical Pustefix application - everything that produces HTML to be precise. The servlet doesn't do much on it's own, it delegates the request processing to a de.schlund.pfixcore.workflow.Context object. This Context is created by the PustefixContextXMLRequestHandler once for every session, stored into the HttpSession and reused for all later requests.

A Pustefix application has exactly one PustefixContextXMLRequestHandler. Although more than one additional >PustefixContextDirectOutputRequestHandler could be used, there is usually no need for such a configuration.

5.2. Processing of Requests

Every request that arrives at a pustefix application is processed in a specific way, depending on the HTTP request handler that is being selected by the PustefixHandlerMapping. In this chapter we will show how PustefixContextXMLRequestHandler handles requests.

The special processing of requests by this HTTP request handler is called the Pustefix Request Cycle. It is important to understand how this cycle works, and how to configure it in such a way to achieve the desired result for each request.

The main "director" in this cycle is the Context object, that will handle all the processing logic and call other objects to handle the business logic part of the request cycle (aka: do something useful with all the user supplied parameters of the request). The other main participants are implementations of the State and PageFlow interfaces.

Pustefix supplies default implementations of these interfaces.

5.2.1. The Context

The PustefixContextXMLRequestHandler does most of the request handling in an object called the "context". The interface Context provides the applications view on this object. From the applications view the context mainly is:

  • Providing an interface to de.schlund.pfixcore.workflow.ContextResource objects. These objects contain the data and the methods needed to implement the desired functionality of a project. Each Context objects initializes one de.schlund.pfixcore.workflow.ContextResourceManager, which in turn initializes all the requested ContextResources. All user data must be stored in ContextResources instead of directly into the HttpSession (this is by design, because a HttpSession only allows to store untyped String-to-Object relations, while the ContextResources can expose arbitrary complex access methods to the stored data).

  • Providing a pluggable authentication mechanism that is called before any request processing to check if the current session has the needed privileges.

  • Mapping of requested pagenames (aka "PageRequests") to the objects that implement the functionality that should be supplied by the page. The Context (with the help of a PageMap object initialized on startup of the Context) checks which page is requested and uses the associated de.schlund.pfixcore.workflow.State object to dispatch the request processing to. See below for more details on this process. Note that for each State only one instance is created, so no local data can be stored in States - all session data must be stored in ContextResources.

  • Organizing pages into PageFlows to provide a small scale "workflow management". PageFlows are linear lists of PageRequests which should be stepped through in order. The Context advances a PageFlow after a request has been handled sucessfully, ie. no error has happened as the result of processing the request data. The detailed rules on how page flows work and how the Context steps through them are explained below.

The context provides a de.schlund.pfixxml.SPDocument to the servlet. This class is a small wrapper around a org.w3c.Document and supplies the XML input document for the final transformation which produces the HTML output. Besides the DOM tree it contains the information the system needs to choose the right stylesheet for the desired page that is to be shown plus some other stuff like XSLT parameters that should be set for the transformation process. The Context doesn't produce the SPDocument itself but delegates this to the State's getDocument() method.

5.2.2. PageFlow

For the PageFlow interface there is currently only one implementation, and at the time of this writing it's not yet possible to change this implementation by supplying your own, although this is planned for the near future. The current implementation is called DataDrivenPageFlow, and it will be explained in more detail below. For the discussion presented here, it is sufficient to know the general PageFlow interface.

public interface PageFlow {
    String getName();
    String getRootName();
    boolean containsPage(String pagename);
    String findNextPage(PageFlowContext context, String currentpagename, boolean stopatcurrentpage, boolean stopatnextaftercurrentpage) throws PustefixApplicationException;    
    boolean precedingFlowNeedsData(PageFlowContext context, String currentpagename) throws PustefixApplicationException;
    boolean hasHookAfterRequest(String currentpagename);
    void hookAfterRequest(Context context, ResultDocument resdoc) throws PustefixApplicationException, PustefixCoreException;
    void addPageFlowInfo(String currentpagename, Element root);
}
      

Both getName() and getRootName() return the name of the pageflow, the difference being that getName() contains the full qualified name (the root name together with any variant name, if present) while get RootName() only returns the root name.

containsPage(String pagename) must return true if the given page name is part of the page flow.

findNextPage(...) is more interesting. It implements the main duty of a page flow: To supply some sort of "next" page, given the context (in the form of a PageFlowContext which is just a stripped down version of the Context interface to only support getting information, but no changing the inner state of the context object), the information what the current page name is (currentpagename), and two flags:

  • stopatcurrentpage: If set to true and the current page is part of the pageflow, then the page flow searches no further for another matching page then the current page. I.e. for the linear page flows of the DataDrivenPageFlow implementation this means checking all the (accessible) pages starting at the head of the flow if they need data (that means: The associated State returns true for method call needsData(...)) - if yes, that page is returned. But if the page under consideration is the current page, return it in any case even if it doesn't need data.

  • stopatnextaftercurrent: This is is similar to the first flag, only that we don't stop the page flow search at the current page, but instead at the next accessible page after the current page.

The rest of the methods will be explained in more detail below.

5.2.3. States

The situation is different for States; Pustefix supplies implementations to cover most of the needs one may have in a normal application, however there are always situations where it is needed or at least much easier to write a specialized State instead of trying to re- or misuse one of the two "standard" implementations supplied with the framework.

These two implementations are StaticState and DefaultIWrapperState. The first is used for all static pages, i.e. pages that don't need to process any input parameters, but merely display more or less static content. The only dynamic thing this state can do is to include information from context resources into it's output DOM tree. The second one implements the concept of wrappers and handlers, which is the standard way in pustefix to handle input data.

Both of these states inherit from the abstract class StateImpl, a class that implements a bunch of helper methods useful for basically every conceivable state implementation. So it is strongly suggested to use this (or one of the two described states) as the base class for your own implementations.

While both of these states will be explained in detail below, it is important to note that the context only knows about states, not a special implementation of it. So on this level it makes no difference if a request supplies data to be processed, or if it only request the display of a certain page. So the only thing we need to know for this chapter is the interface all states have to implement:

public interface State {
    boolean        isAccessible(Context context, PfixServletRequest preq) throws Exception;
    boolean        needsData(Context context, PfixServletRequest preq) throws Exception;
    ResultDocument getDocument(Context context, PfixServletRequest preq) throws Exception;
}
      

These three methods are quite easy to explain. isAccessible(...) is used to check if a page is accessible (the exact wording would be "the associated State of the page", but we use page/state interchangeable here, as there is a n:1 association of pages to states anyway, i.e. every page has exactly one associated state, but most of the time many pages share the same state. States are singletons, so they don't store any data themselves. This allows to share them between many pages). getDocument(...) is the method that does all the work. Here we produce the result DOM tree that is used to render the final HTML page with. needsData(...) is (or better: can be) used only during page flow processing to determine what the next page is that needs to be shown. This method will be explained when we describe the PageFlow and it's default implementation in greater detail below.

5.2.4. Influencing the page request cycle

During the request-response cycle, the Context maintains a set of variables that influence the processing of the request. These are listed in the following table. Use these as a reference to see how they can be set and changed, either by specifying values for them in the request (directly, or by referencing an action that sets them) or by calling a method of Context somewhere from Java code during the processing of the request.

Table 5.1. Variables of the context during processing
Variable Type Usage How to set?
currentpagerequest PageRequest This is basically the object representing the current page we use to process the request. The value of currentpagerequest that is valid at the end of processing becomes the page to be displayed.

This variable must never be unset again after initialization during the whole request cycle (it only changes to other PageRequests).

Supplied by the request via either the third request path element (e.g. http://host.dom/xml/config/PAGE?...) or if this is not given via the request parameter __page.

In Pustefix, this is usually transparent to the user: For POST requests, use the send-to-page attribute of pfx:forminput (this defaults to the current page if send-to-page is not given) and for GET requests, use the page attribute of pfx:button.

If no page information can be retrieved from the request, use the default page given in the configuration.

currentpageflow PageFlow Default is null. This object represents the currently valid page flow (if any). If it is null, there is no page flow selected. Can be explicitly set via the parameter __pageflow (in Pustefix used via the attribute pageflow to either pfx:button or pfx:xinp type="submit|image" via the request or by setting the pageflow attribute of a configured action).

Can also be set from Java by using the context method setCurrentPageFlow(String name).

The current page flow is very often not set explicitly, but selected automatically by the context. See below for a detailed explanation of the rules that apply in these cases.

prohibitcontinue boolean Default is false, if set to true during the request processing, the context will not use the pageflow (if any) to determine the next page to show, but instead use the currentpagerequest and display the associated page. Can be set from the outside by using the request parameter __forcestop=true (maps to setting the forcestop attribute of pfx:button or pfx:xinp type="submit|image" to true or by calling a configured action with this attribute set to true).

There is also a method in the context called prohibitContinue() to set this value to true. If by any means this value becomes true, there is no way to reset the value to false again!

jumptopage and jumptopageflow String Default is null for both. If set, jumptopage is interpreted as a page name that should be displayed after the current request is processed and only if prohibitcontinue is not set to true (in which case, as described below in more detail, no further processing takes place and the current page is displayed).

The jumptopageflow variable only has an effect if also jumptopage is set. It is used to set the current page flow to another page flow when jumpting to the target page of jumptopage.

With other words, this mechanism is used to jump to another page after the current request has been successfully handled.

This entry can be set quite similar to the pageflow variable above: We have __jumptopage and __jumptopageflow, normally created via the attributes jumptopage and jumptopageflow in either pfx:button, pfx:xinp type="submit|image" or a configured action.

There are also two methods in the context that can be used to set these values from Java: setJumpToPage(String name) and setJumpToPageFlow(String name).

stopnextforcurrentrequest boolean Default is false. Only has an effect if a page flow is set, and the current page is indeed a member of this page flow.

If set to true the pageflow is expected to return the next accessible page after the current page in the pageflow. The meaning of "after" depends on the implementation of the PageFlow used for the current flow. The default implementation (DataDrivenPageFlow) works with linear flows, so there is always a clear understanding of what is "before" and "after" a page in the flow. Other implementations may have a more complicated interpretation.

Can be set directly in a request by using __forcestop=step (and of course the same for the attributes to the Pustefix tags and configured actions).

This may seem strange, as that parameter is also used to set the prohibitcontinue variable, but as it makes no sense to specify both of them at the same time (prohibitcontinue effectively prohibits the use of a pageflow because the current page is being displayed anyway and no page flow is asked for the next page to display), there is no need to have an independent parameter or attribute.

startwithflow boolean Default is false. This variable is used to instruct the Context to not directly use the page that is submitted with the request (and which is still used to set the currentpagerequest variable), but instead ask the current pageflow for the next page to use. So the caller doesn't actually know which page will be the one to display. Most often, setting this parameter also implies explicitely setting a page flow via the methods listed above. We will cover this special case in more detail below. This variable can be set via the request parameter __startwithflow. With Pustefix tags this is achived by setting the startwithflow attribute of pfx:button to true.

There is no such possibility for pfx:xinp type="submit|image", because it makes no sense for a request which supplies data to not know where to submit to. Also using startwithflow="true" with pfx:button implies that the request will not being marked as one that sends data, even if there are pfx:argument nodes attached.

The currentpageflow variable needs some more explanation, as in many cases, it is not given explicitly neither by submitting an action that specifies the page flow nor directly from the request parameter __pageflow. If this is the case, the Context tries to find a matching page flow by using the following algorithm.

  1. If the current page is a member of exactly one page flow, this flow will become the current page flow.

  2. If the current page is a member of more than one page flow, the Context checks if one of these flows has been the last flow the system has used in any request before (so this even applies if the system didn't use a page flow at all during the last requests). If this is the case, the system uses this flow as the current page flow. This has the effect that a page flow will remain the current flow as long as the pages used for requests are at least a member of this flow.

  3. If the last flow isn't part of the list of flows matching the current page, the system checks if the current page specifies a defaultflow in its configuration (and makes sure that the page is really a member of this flow!). If yes, this flow is preferred and returned as the current page flow. If not, the first of the list will be returned.

  4. If the current page is not a member of any flow, the current page flow remains unset (the currentpageflow variable remains null).

5.2.5. The basic Pustefix Request Cycle

In this section we want to explain the way the request cycle is handled in Pustefix by the Context and its peer objects (State, PageFlow) used during processing.

After the variables have been initialized, we have two different ways to go on. Either startwithflow is set to true, or not. The first case will be explained below in more detail, for now we assume that the value of startwithflow is false. We also do neglect some other aspects, that have to be taken care of during request processing: Role based authentication isn't mentioned here and also the fact that each State will always be asked if it is accessible before calling one of the other two methods won't be mentioned explicitly for the remainder of this explanation.

  1. The first action to take is calling getDocument(...) on the State associated with the current page.

  2. If the current page flow has After-Request-Hooks defined (this is checked by calling the method hasHookAfterRequest(...) on the current PageFlow), these hooks are being run by calling hookAfterRequest(Context context, ResultDocument resdoc). The ResultDocument used here is the return value from the getDocument(...) call above. These hooks can basically do anything that can be achieved with the help of the Context interface (changing jumptopage/jumptopageflow, calling prohibitContinue() and so on). The interesting thing here is that they not only have access to the Context object, but also the resulting DOM-Tree of the processing of the current page. We will learn about an example of such hooks when we look at the current standard implementation DataDrivenPageFlow.

  3. Now we check if prohibitcontinue is set to true. If yes, the ResultDocument will be used to display the current page. The request cycle ends here.

  4. If prohibitcontinue is still false, check if jumptopage is set. If yes, set the currentpagerequest to the jumptopage (and also change currentpageflow to something that matches, preferring jumptopageflow, if it is set); jumptopage/jumptopageflow are unset to avoid recursion, then we re-enter the process at point 1.

  5. If jumptopage is not set, we try to use the current page flow to get the next page by calling findNextPage(..., ..., false, stopnextforcurrentrequest). We set this page to be the current page, call getDocument on its associated State and use the returned ResultDocument to display the page. The request cycle ends here (there is no recursive call of the page flow process!).

  6. If no current page flow is set (currentpageflow == null), we simply use the resulting ResultDocument of the initial call to getDocument(...) and use it to display the current page.

Accessibility of pages

Up to now, we mostly neglected the fact that a page could also be not accessible, which it is if the associated State returns false for a call to its method isAccessible(...). This method is checked each time before the Context tries to call getDocument(...) for a page. The PageFlow is also expected to only return a page that is accessible, and to check the accessibility before each call to needsData(...).

If the initially requested page is not accessible, but a page flow is set, the Context will try to find a matching page by calling findNextPage(..., ..., false, stopnextforcurrentrequest). Because this method is expected to only return a page that is accessible (if it doesn't find one, it must throw an exception) we can safely set this page to be the new current page and start with the request cycle as usual.

If no page flow is set, the system will try to use the default page from the configuration. If this is indeed accessible, it will be set as the new current page, and the whole process continues normally from there. If also the default page is not accessible, an exception will be thrown.

Usually this case won't happen, because you have to force Pustefix to generate a link to a page that is currently not accessible by using the mode="force" attribute on pfx:button. But this procedure is also done when the current page has been set from the jumptopage variable and the request cycle is being restarted (see above). To avoid to see any strange behavior and pages that have never been intended to be displayed, it's important to make sure that a jumptopage request only references a page that will be accessible.

This accessibility check must also be run by the PageFlow before calling needsData(...) or before returning any page name from findNextPage(...). It is expected to simply ignore any page that is not accessible and continue searching for another "next" page. As already explained above, if it is not able to find a page that is accessible, it must throw an exception.

Processing when startwithflow=true

Another change in request processing happens when the startwithflow variable is set to true. In this case, the context doesn't expect to get a valid pagerequest from the request itself (although there is one supplied, see below for the special meaning of this page name), so it has to "search" for the page to handle the request processing. This search is done by directly switching to the page flow handling part of the code to retrieve the "next" page that is suggested by the page flow.

Other than in the case of the usual page flow processing, there is some special trick involved: If during searching for the next page the page flow encounters the supplied page from the request, it will return this page regardless if it needs data or not. In other words: You can limit the search in the page flow by supplying a page that is part of this flow. If the page is not part of the page flow or it is inaccessible, it is ignored (But note that the default page flow implementation DataDrivenPageFlow also sets the supplied page to be the final page (see below for details on this), so essentially every supplied page becomes part of the page flow. But this behavior is not part of the contract between PageFlow and Context and just an implementation detail of DataDrivenPageFlow).

On a quick look, this seems to be almost the same as what is done when a request comes in to a page, that is not accessible (see above). In this case, too, a new page ist searched by asking the page flow for the next page. The main difference is that a "normal" request will always use the supplied page, as long as it is accessible, while in the startwithflow case it will only be used if no other "earlier" candidates have been returned by the page flow.

5.2.6. Pustefix State implementations

Despite all the explanation about States, most of the time one doesn't write States directly, but rather uses one of two predefined States (or trivial descendants of them).

StaticState

The first is de.schlund.pfixcore.workflow.app.StaticStateliteral>. This State is used when the page doesn't need to process any input data. This is true for most of the purely informative pages of a website (e.g. documentation, product information). StaticState returns true for any call to needsData, which makes it of course unusable for any non-trivial pageflow as described here. Also any call to isAccessible returns true. For the creation of the output DOM tree, the State respects the <output> nodes of the page definition in the context configuration file, but of course any <input> nodes are ignored.

DefaultIWrapperState and IHandlers

Of course it would be possible to write specialised States for every page that needs application logic. Experience shows however that the reuseable components of the application logic are not complete pages, but smaller parts that make up the whole functionality of the page. So we need a way to aggregate these smaller parts into a predefined State that delegates and distributes the calls made by the Context.

The State that implements this is called de.schlund.pfixcore.workflow.app.DefaultIWrapperState. The corresponding logical "atomic" components are implementations of de.schlund.pfixcore.generator.IHandler. Associated to these IHandlers are container classes (implementations of de.schlund.pfixcore.generator.IWrapper) that hold the user supplied input data that these handlers should work on. The IWrapper classes are autogenerated from a XML description that is explained in more detail here.

An IHandler has the following simple interface:

  • boolean needsData(Context)

  • boolean prerequisitesMet(Context)

  • boolean isActive(Context)

  • void retrieveCurrentStatus(Context, IWrapper)

  • void handleSubmittedData(Context, IWrapper)

The corresponding methods of the State interface and the mappings to the IHandler methods are:

  • boolean needsData(Context, PfixServletRequest): this method is just delegated to all defined IHandlers. If any of those returns true, the return value of the State methods is true, too and false otherwise.

  • boolean isAccessible(Context, PfixServletRequest): this method maps on the two IHandler methods prerequisitesMet and isActive. The default algorithm works like this: In a first round, on all IHandlers the prerequisitesMet() method is called. If any of the IHandlers returns false here, the return value of the whole State method is false. If all IHandler return true, a second iteration over the IHandler is made with a call to their isActive method. This time, the logic is reversed: if any IHandler returns true, the State method is true, too. It only returns false when no IHandler's isActive method returns true. It's possible to customize the behavior for this second iteration in the config file with the policy attribute of the <input> node.

  • ResultDocument getDocument(Context, PfixServletRequest): this method is mapped on either retrieveCurrentStatus or handleSubmittedData depending if the request is submitting user data by means of a GET request or a form submit (in this case handleSubmittedData would be called) or if the request just wants a page to be displayed (which would result in retrieveCurrentStatus to be called).

Some important things to note:

  • No IHandler method returns a ResultDocument (which is basically a wrapper around the DOM tree that is used for the final transformation to generate the HTML output). In a State it's possible (in fact needed) to create the ResultDocument itself and it's easy to put additional nodes into the DOM tree inside the getDocument method. By design, this no longer works with IHandlers. All output should be generated from the configured ContextRessources listed below the <output> node of the config file.

  • IHandlers don't have access to the PfixServletRequest (which is basically a wrapper around HttpServletRequest). All in- or output of parameters must take place via the typesave getter and setter methods of the associated IWrapper object. In the handleSubmittedData method one typically reads the values as they are supplied from the request, and in the retrieveCurrentStatus method one sets the parameters as they are given by the current status of the application to pre-fill form elements on the user interface (Note: the Pustefix elements to create form elements take care to automatically use the values as supplied via the IWrapper to pre-fill form elements. If you generate your form with other XSLT tags or with the original html elements, this will no longer work ot of the box).

The Context and DefaultIWrapperState

It's an important property of Pustefix that the Context doesn't know anything of IHandlers/IWrappers. From the view of the Context, it always works with States and nothing else. This section describes the way the DefaultIWrapperState and the Context play together.

A request can be processed in two different ways by the getDocument(...) method of DefaultIWrapperState:

  1. A request that submits data (as can be detected with the help of the method isSubmittrigger(...)) will be processed like this (EOR means end of request):

    • All IWrappers will initialize their data from the request.

    • On all IHandlers will be called handleSubmit(...) with the approbiate IWrapper object as a parameter.

    • The system checks if there has been an error during processing the reuqest.

      If yes, put all the error codes into the ResultDocument and tell the context to stop at the current page (by calling context.prohibitContinue()). EOR.

      If no, the system needs to decide to either stay on the current page (and display it again) or hand back the control to the Context to start a pageflow process that determines the next page to display. This decision is made be looking at the state of the context and the set of IHandlers. There are 3 different cases to consider:

      1. First we need to decide if there is anything that actively forces the system to stay on the page even in the case of a successful submit. Two cases must be consideres here: The context method prohibitContinue() has already been called (which can be checked by calling the Context method isProhibitContinueSet()). This will always force the context to stop the process, use the returned ResultDocument and display the current page. For the second case, depending on which IHandlers have been called to handle data (this can be a subset of all the IHandlers on the page, see here on how to restrict the submit to a subset of the full set of defined IHandlers), the DefaultIWrapperState decides to stay on the current page. A simplified explanation of the algorithm is like this: When only a subset of the IHandlers have been used to handle a data submiting request, force the system to stay on the current page. There is an exception to this rule, though: When all the IHandlers of the restricted subset are also marked with the continue attribute on the corresponding <interface> node in the config file set to true, then don't force the system to stay on the current page. All of this does NOT apply when the request has set a JumpToPage (this can be queried from the Context with the method isJumpToPageSet()) - in this case the JumpToPage takes precedence. NOTE: this will maybe be removed in the future and the default will be to try to continue with the pageflow in any case, despite any restricted subset of IHandlers.

      2. Alternatively we try to check if we really want to give control back to the Context to determine the next page. This will happen if one of three conditions is true:

        • A JumpToPage has been set.

        • The current page is part of the current pageflow

        • Or the current pageflow has been set explicitly by the request (in this case, the current page doesn't need to be a member of this flow).

      3. The default for the DefaultIWrapperState if none of the above conditions is true is to stay on the current page.

        If the result of these checks is to stay on the current page, call prohibitContinue() on the Context and call retrieveCurrentStatus() on the suitable IHandlers - suitable in this case means a) if no restriceted subset of IHandlers is selected, use all that are defined or b) use the restricted subset and additionally all IHandlers that have the alwaysretrieve attribute on their corresponding <interface> node in the config file set to true. EOR.

  2. A request for a page that doesn't submit data (direct trigger), or a request that comes in while a pageflow is processed or a request that is the result of a final page instruction.

    • All IWrappers will initialize their data from the request.

    • call retrieveCurrentStatus() on all the defined IHandlers, and force the Context to stop further processing by calling prohibitContinue(). EOR.

5.2.7. Pustefix PageFlow implementation: DataDrivenPageFlow

5.3. The data model: Context resources

5.4. Wrappers and Handlers

Write introduction...

5.4.1. IWrappers

IWrappers are used to store the request data that is used as input for corresponding IHandler classes. IWrappers are not explicitly written by the developer, but are generated from .iwrp files which contain a high level XML description of the parameter types and certain checks that should be applied to them. Each .iwrp file is translated into a .java file with the same name during the build process, and a java class is created. In the corresponding IHandler, you use the IWrapper to access or store data by calling the IWrapper's get and set methods.

The syntax of the .iwrp file is given below.

<interface xsi:schemaLocation="http://pustefix.sourceforge.net/interfacewrapper200401 http://pustefix.sourceforge.net/interfacewrapper200401.xsd" extends="some.other.iwrapper.class"
  xmlns="http://pustefix.sourceforge.net/interfacewrapper200401"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <!--
    The class attribute references the associated IHandler class. Alternatively you can
    reference an existing Spring bean using the bean-ref attribute.
    The ihandler node is optional, but if it is left out, the generated
    IWrapper will be abstract and cannot be used directly, unless an extends attribute
    has been given to the interface node. Normally you will want to specify an ihandler
    class here.
  -->
  <ihandler class="some.de.schlund.pfixcore.generator.IHandler.class"/>
  <!-- or <ihandler bean-ref="aBean"/> -->
  <!--
    This node can (and usually will) occur multiple times, one for every parameter that
    should be part of the interface
  -->
  <param name="AName" type="some.java.Type" occurrence="optional|mandatory|indexed" frequency="single|multiple" missingscode="some.defined.statuscode">
    <!--
      The whole node is optional. It allows to specify a default value (or multiple) for
      a parameter to use, when no value is supplied via the request. Note that this makes
      the destinction between optional and mandatory parameters nonsensical.
    -->
    <default>
      <value>a_default_value</value>
      <value>an_other_default_value</value>
    </default>
      
    <!--
      To test if a param value conforms to the rules, you can specify Check-Classes here
      whose check method will be called to test if the param value makes sense.
      The Check-Classes must implement de.schlund.pfixcore.generator.IWrapperParamPreCheck
      (the interface defines the check(...) method).
    -->
    <precheck class="a.prechecker.class">
      <cparam name="APreCheckerParamName" value="APreCheckerParamValue"></cparam>
    </precheck>
      
    <!--
      Each parameter must be casted from a String to the specific type (unless the type is java.lang.String itself, in this case, no caster need to be supplied). This is done by means of a class implementing de.schlund.pfixcore.generator.IWrapperParamCaster. For the usual simple types you can use a caster from the package de.schlund.pfixcore.generator.casters.
    -->
    <caster class="de.caster.class">
      <cparam name="ACasterParamName" value="ACasterParamValue"></cparam>
    </caster>

    <postcheck class="a.postchecker.class">
      <cparam name="APostCheckerParamName" value="APostCheckerParamValue"></cparam>
    </postcheck>
  </param>
</interface>

Parameters

When defining the parameters for your wrapper, the following attributes are supported in the iwrp format:

Table 5.2. Attributes of an iwrp parameter
Attribute name Mandatory? Description
name mandatory The name of the parameter. This is used in the getter and setter methods that are generated. E.g. for the parameter name Foo there will be - amongst others - a corresponding java method called getFoo.
type mandatory The java type of the parameter. This will determine the return type of the generated getter method.
occurence Optional, default is mandatory This attribute specifies if the parameter must be given (mandatory), or if it's not considered to be an error if it is omitted (optional). The special value indexed tells the system that it should search for occurrences of the parameter name with a suffix appended of the form AParamName.ASuffix. The suffix string must be unique for every occurrence of the parameter named AParamName. Indexed parameters are never mandatory.
occurence Optional, default is single This attribute specifies if only one parameter of the same name should be accepted or multiple. This determines if the generated getter method's return value is a single object or an array.
occurence Optional, default is de.schlund.pfixcore.generator.MISSING_PARAM This attribute applies only to mandatory parameters. It allows to specify a different than the default StatusCode to use when the parameter is not supplied.

IWrappers support different parameter types that result in different method signatures in the generated wrapper code.

The parameter type is influenced by the attributes occurance and frequency

Single mandatory/optional parameters

In most cases, your wrappers will be used to accept and validate simple input parameters, like data entered in input fields. These parameters are created setting frequency to single and occurrence to mandatory or optional.

This will lead to the following methods:

public class MyWrapper {
    Bar  getFoo();
    void setFoo(Bar value);
    void setStringValFoo(String str_value);
    void addSCodeFoo(de.schlund.util.statuscode.StatusCode scode);
    void addSCodeWithArgsFoo(de.schlund.util.statuscode.StatusCode scode, String[] args);
}
Multiple mandatory/optional parameters

In some cases, a parameter might be present in a page more than once. This might be the case for a list of checkboxes, where all input tags have the same name, but different values. These parameters are created setting frequency to multiple and occurrence to mandatory or optional.

This will lead to the following methods:

public class MyWrapper {
    Bar[] getFoo();
    void  setFoo(Bar[] value);
    void  setStringValFoo(String[] str_value);
    void  addSCodeFoo(de.schlund.util.statuscode.StatusCode scode);
    void  addSCodeWithArgsFoo(de.schlund.util.statuscode.StatusCode scode, String[] args);
}
Single indexed parameters

Pustefix also allows you to repeat a parameter inside a page/request and access all occurrences of the parameter using an index. In the request, the parameter and the index are separated using a . (dot).

These parameters are created setting frequency to single and occurrence to indexed.

This will lead to the following methods:

public class MyWrapper {
    Bar  getFoo(String index);
    void setFoo(Bar value, String index);
    void setStringValFoo(String str_value, String index);
    void addSCodeFoo(de.schlund.util.statuscode.StatusCode scode, String index);
    void addSCodeWithArgsFoo(de.schlund.util.statuscode.StatusCode scode, String[] args, String index);
}

This is very helpful, if you have a list of entities (like users, books, etc.) and it is possible to edit the data of several of these entities in one form.

Multiple indexed parameters

Of course, it is also possible to combine multiple and indexed parameters in a wrapper.

These parameters are created setting frequency to multiple and occurrence to indexed.

This will lead to the following methods:

public class MyWrapper {
    Bar[] getFoo(String index);
    void  setFoo(Bar[] value, String index);
    void  setStringValFoo(String[] str_value, String index);
    void  addSCodeFoo(de.schlund.util.statuscode.StatusCode scode, String index);
    void  addSCodeWithArgsFoo(de.schlund.util.statuscode.StatusCode scode, String[] args, String index);
}

5.4.2. The IHandler interface

public interface IHandler {
    void    handleSubmittedData(Context context, IWrapper wrapper) throws Exception;
    void    retrieveCurrentStatus(Context context, IWrapper wrapper) throws Exception;
    boolean prerequisitesMet(Context context) throws Exception;
    boolean isActive(Context context) throws Exception;
    boolean needsData(Context context) throws Exception;
}

5.5. StatusCodes

StatusCodes are assigned to IWrapper parameters and form fields to indicate invalid data and display according error messages. They are automatically assigned by the framework when prechecks, casters or postchecks fail, or they can be assigned programmatically by the application logic.

The framework predefines some common StatusCodes, e.g. MISSING_PARAM - indicating the missing of a mandatory form value, or StatusCodes for builtin prechecks, casters and postchecks, like precheck.REGEXP_NO_MATCH - indicating that a form field's value doesn't match a given regular expression. Application developers can arbitrarily define additional StatusCodes.

StatusCodes are defined as normal include parts. They're placed in their own files located under the dyntxt directory. Within such a statuscode file the part names uniquely identify the StatusCode, the content nested inside the part element represents the message.

<?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="ILLEGAL_LOGIN">
    <theme name="default">Illegal login data.</theme>
  </part>
  ...
</include_parts>

Those StatusCode files are used by the build process to automatically create Java classes containing the StatusCodes as constants, using the part names as field names (in uppercase form). Thus the StatusCodes can be safely referenced from within Java code.

Before the StatusCode files are recognized by the build process, they have to be configured inside a statuscode metainformation file. The file has to be called statuscodeinfo.xml and has to be located in the project's conf directory or in the dyntxt directory. Within this file you define which StatusCode constant class is built from which StatusCode files.

<?xml version="1.0" encoding="UTF-8"?>
<statuscodeinfo xmlns="http://pustefix-framework.org/statuscodeinfo" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                xsi:schemaLocation="http://pustefix-framework.org/statuscodeinfo ../../core/schema/statuscodeinfo.xsd">
  <statuscodes class="example.bank.BankStatusCodes">
    <file>statusmessages.xml</file>
    <file>...</file>
  </statuscodes>
  ...
</statuscodeinfo>

Using the above examples Pustefix will build the following Java class (excerpt):

package example.bank;
...
public class BankStatusCodes {
    public static StatusCode getStatusCodeByName(String name) { ... }
    ...
    public static final StatusCode ILLEGAL_LOGIN = new StatusCode("ILLEGAL_LOGIN", __RES[0]);
}

The following login handling example uses the generated StatusCode class to indicate a failed login:

public class LoginHandler implements IHandler {
    public void handleSubmittedData(Context context, IWrapper wrapper) throws Exception {
        Login login = (Login) wrapper;
        //check login data
        //if invalid
        login.addSCodeUser(BankStatusCodes.ILLEGAL_LOGIN);
    }
}

If you take a look at the resulting XML tree, you see like the according include part is referenced inside the error element (and thus can be displayed on the page using the pfx:checkfield, pfx:error and pfx:scode tags).

<formresult ...>
   ...
   <formvalues>
      <param name="login.User">xyz</param>
      ...
   </formvalues>
   <formerrors>
     <error name="login.User">
        <pfx:include href="samplebank/dyntxt/statusmessages.xml" part="ILLEGAL_LOGIN"/>
     </error>
   </formerrors>
   ...
</formresult>

If your error message should contain dynamic data, you can use placeholders, which will be replaced by the arguments passed along with the addScode call at the IWrapper:

<part name="ILLEGAL_LOGIN">
  <theme name="default">User <pfx:argref pos="1"/> is illegal.</theme>
</part>

StatusCodes can be also referenced from within IWrapper definitions. If you're using a built-in StatusCode (from org.pustefixframework.generated.CoreStatusCodes), you can just use its name, if you're using a StatusCode from another module, you have to prefix the name with the class name of the generated class separated by a # sign:

<interface xmlns="http://pustefix.sourceforge.net/interfacewrapper200401">

  <ihandler class="..."/>
  
  <param name="Login" type="..." missingscode="example.bank.BankStatusCodes#MISSING_LOGIN_DATA">
    ...
  </param>

</interface> 

The statusmessage files for statuscodes coming from the Pustefix core or arbitrary modules can be edited to customize the messages for your project's requirements. Therefor the original statusmessage files are initially copied or if a copy already exists, the files are merged (statusmessages for new statuscodes are added, statusmessages for omitted statuscodes are removed) by the build process. You should only customize these merged files, changes in the original files will be overwritten with the next module update.

The built-in core and editor statuscodes are merged to projects/core-override/dyntxt/statusmessages-core-merged.xml and projects/core-override/dyntxt/statusmessages-editor-merged.xml. Pustefix module statuscodes are merged to projects/modules-override/MODULENAME/dyntxt/statusmessages-merged.xml.

5.6. ContextInterceptors

Pustefix provides a mechanism to hook custom logic into the request processing lifecycle. Therefor it provides a set of interception points, where you can register classes or Spring beans implementing the ContextInterceptor interface, which then are automatically called during the processing of a request.

Common use cases for ContextInterceptors are:

  • Implementation of cross-cutting concerns like logging, cleanup.

  • Implementation of standard design patterns like Hibernate's "Open Session in view".

  • Triggering logic/setting data independent of the requested page, e.g. setting a variant.

  • Handling of common, page-independent/shared parameters.

Figure 5.2. ContextInterceptors

ContextInterceptors


Currently Pustefix supports three interception points: start, which is right before the actual request handling gets started, end, which is right after the DOM result tree has been created, and postrender, which is right after the view has been rendered.

public interface ContextInterceptor {
    void process(Context context, PfixServletRequest preq);
}

The framework calls the process method of the registered ContextInterceptor implementations, passing the Context and PfixServletRequest as parameters.

[Note]Note
Comparison with ServletFilters: ServletFilters apply right before/right after a request/response enters/leaves the framework's processing layer and give you means to manipulate the incoming ServletRequest and outgoing ServletResponse, whereas ContextInterceptors are part of the framework's lifecycle and give you direct access to the request (but not the response), Context and other Spring beans, and are meant to trigger logic or set data.

ContextInterceptors are configured in the Context's configuration file. Below you can see an a sample configuration fragment showing all available configuration options:

<context-xml-service-config>
 
   <context> ... </context>

   <!-- ContextInterceptors are defined within the <interceptors> element directly following the <context> element. -->
   <interceptors>

     <!-- The interceptors are grouped by their interception points (start, end, postrender), which are represented 
            by accordingly named elements. -->
     <start>

        <!-- Interceptors are registered either by setting the ContextInterceptor implementation class or by setting 
               an according reference to a Spring bean configured elsewhere. -->
        <interceptor class="mypackage.MyStartInterceptor"/>

        <!-- If you're setting an implementation class, the framework will create an according Spring bean for you. 
               By default it will get Singleton scope, but you can provide an alternative scope using the scope attribute. --> 
        <interceptor class="mypackage.MySessionScopedInterceptor" scope="session"/>

        <interceptor bean-ref="myStartInterceptor"/>

     </start>

     <end> ... </end>

     <postrender> ... </postrender>

   </interceptors>

   ...

</context-xml-service-config>

The following example interceptor tries to guess from which country the client request is coming and sets a special variant if it comes from Germany. Therefor it checks the client's IP retrieved from the servlet request using a GeoLocationAPI. It sets a flag after the first check to avoid multiple checks. Remembering the flag this way only works when the interceptor has session scope and synchronization can be omitted because the operation is repeatable and boolean access is atomic.

public class MySessionScopedInterceptor implements ContextInterceptor {
	
	private boolean checked;

	@Override
	public void process(Context context, PfixServletRequest preq) {
		if(!checked) {
			String country = GeoLocationAPI.getCountry(preq.getRemoteAddr());
			if(country.equals("DE")) context.setVariant("foo");
			checked = true;
		}
	}
	
}
[Note]Note

ContextInterceptors are singleton-scoped beans by default. So if you want to store data or call non-threadsafe code, you possibly will have to do some synchronization or use an appropriate scope.

ContextInterceptors are called on every request. So you shouldn't execute long-running tasks and avoid that time-consuming code is unnecessarily called with each invocation.