Table of Contents
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
| 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
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 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 |
| 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 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 |
| 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 |
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 |
| 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 |
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.
If the current page is a member of exactly one page flow, this flow will become the current page flow.
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.
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.
If the current page is not a member of any flow, the current page flow remains unset (the currentpageflow variable remains null).
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.
The first action to take is calling getDocument(...) on the State associated with the current page.
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.
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.
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.
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!).
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.
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.
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.
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).
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.
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).
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:
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:
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.
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).
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.
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.
Write introduction...
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>
When defining the parameters for your wrapper, the following attributes are
supported in the iwrp format:
| 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
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); }
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); }
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.
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); }
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; }
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.
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.
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 |
|---|---|
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 |
|---|---|
|