Table of Contents
Variants are a way to have the same page look and behave different depending on a variant id that can be set freely at runtime. This allows to select different look and behaviour depending on data collected during the user sesion.
Variants can be selected by calling the method setVariant(Variant var)
in the Context. This will store and reuse the id for the current and all following
requests until it is set to a different id (or erased by setting it to null).
The Context takes care to set the variant in the
SPDocument it returnes to the AbstractXMLServer which
in turn tries to get the matching target for the requested page and
requested variant.
If no target matching the variant is found, the "root" variant
(in other words, the page without a variant at all) will be tried.
When defining variants of pages, the system automatically makes sure that you can only define variants for a page that has also a no-variant definition. This works for pages defined using the <standardpage> tag and for pages defined by hand using <target> tags directly.
The valid characters for variants are a-zA-Z0-9_+- (but see below for compound variants and the use of the : character).
Variants can be variants of variants themself which in turn can be considered variants of another variant. The name of a variant that is a subvariant of another variant must be expressed in the following way: foo:bar:baz
Here we have a variant that is a subvariant of foo:bar which in turn is a subvariant of foo. This information is used when determining the right Target for generating the output. Say the variant foo:bar:baz has been set via the setVariant(Variant var) method of the Context and the request wants to display the page Home. When trying to get the matching Target for transforming the output the system first tries to get the matching Target for Home, variant foo:bar:baz. If it gets no result, it goes on trying Home, variant foo:bar; then Home, variant foo before finally requesting the "root" variant of page Home.
Variants can influence the way an application works:
Different variants of pages are usually defined via the standardpage tag:
<standardpage xml="MyProject/xml/frame.xml" name="Home" variant="foo:bar:baz">
What's interesting here is how the system produces the themes attribute used for the two targets that result from the expansion of the standardpage tag.
If we have not given a special local themes attribute to the standardpage tag,
and no global themes attribute has been given (see here), then the value of the
themes attribute for both targets is in this example baz bar foo MyProject default.
As can be seen, the use of the variant attribute will expand the themes as they would be valid for the targets produced from the standardpage tag (either by using a locally defined themes attribute of the standardpage tag, or automatically by using the global fallback) by putting more specific themes in front of the themes list, which are generated from the given variant id. If the variant id is a compount variant, then each segment of the compound variant will be used, from right to left, to generate a theme name to be put before the "normal" themes valid for the page.
If the standardpage had its own themes attribute given (say for example
"theme_a theme_b default"), the resulting themes after taking the
variant id into account would of course be baz bar foo theme_a theme_b default.
Variants can also be used to create different implementations for the same pagerequest. This is done by a simple extension to the way a pagerequest is defined in the servlet configuration file.
<pagerequest name="foo"> <default> [Any other tag that is allowed inside a pagerequest] </default> <variant name="var_id"> [Any other tag that is allowed inside a pagerequest] </variant> <variant name="another_var_id"> [Any other tag that is allowed inside a pagerequest] </variant> </pagerequest>
![]() | Note |
|---|---|
You must give a Each of the |
Variants can also be used to create different implementations for the same pageflow. This is done by a simple extension to the way a pageflow is defined in the servlet configuration file.
<pageflow name="fooFlow"> <default> [Any other tag that is allowed inside a pageflow] </default> <variant name="var_id"> [Any other tag that is allowed inside a pageflow] </variant> <variant name="another_var_id"> [Any other tag that is allowed inside a pageflow] </variant> </pageflow>
![]() | Note |
|---|---|
You must give a Each of the |
Pustefix allows you to implement multi-language applications. As there is a clean separation between business logic and presentation, you can easily change the presentation layer to a new language.
See Section 4.5.2, “Displaying content based on the language” on more information on how the access the currently selected language in the presentation layer.
If your business logic needs to react to the currently selected language, you can retrieve
the current language of the application via the getLanguage() : String of the
Context.
The Context also provides a method setLangauge(String language),
which allows you to change the selected language at run-time.
Pustefix applications can be modularized by packaging/sharing application parts or components as Pustefix modules, which are regular Jar files with a defined directory structure and a special deployment descriptor (see Chapter 7, Module Support). Pustefix modules not only contain Java classes, but can contain any kind of resource, like XML, XSL, Javascript, CSS or image files.
Prior Pustefix releases required that these resources were extracted to the file system by the build process to be usable in an application. Meanwhile Pustefix supports that XML and XSL files used by the rendering engine (TargetGenerator) can be directly loaded from the module jar files.
Developing strongly modularized applications often requires, that you want to be able to adapt parts of a shared module to your application's context without breaking the modularization. Therefor Pustefix supports the dynamic inclusion of resources, which allows you to provide a local version of a resource, which will be automatically taken instead of the according version from a module jar. Additionally resources from modules can be overridden by other modules.
Resources from modules are directly used by additionally specifying the name of the module. Therefor the according tags from the Pustefix tag library and
the configuration elements in depend.xml provide a module attribute.
samplemodule.jar
META-INF/pustefix-module.xml PUSTEFIX-INF/img/image.png PUSTEFIX-INF/img/theme/image.png PUSTEFIX-INF/txt/common.xml PUSTEFIX-INF/txt/pages/main_page.xml PUSTEFIX-INF/xml/frame.xml PUSTEFIX-INF/xsl/metatags.xsl PUSTEFIX-INF/xsl/skin.xsl
Let's take a look at how the various resources from the sample module can be directly included using the Pustefix tag library:
<pfx:xinp type="image" src="img/image.png" module="samplemodule"/>
<pfx:image themed-path="img" themed-img="image.png" module="samplemodule"/>
<pfx:include part="foo" href="txt/common.xml" module="samplemodule"/>
<pfx:maincontent part="content" path="txt/pages" module="samplemodule"/>
You just have to set the module attribute and be aware that the file paths are relative to the PUSTEFIX-INF root folder from the module jar file.
![]() | Note |
|---|---|
If you're using includes from within a module and you're omitting the href or module attribute, you should be aware of the following behaviour:
|
If you want to directly reference XML/XSL files in the depend.xml configuration file, this works the same way by adding a module attribute to the according elements:
<standardmaster> <include stylesheet="xsl/skin.xsl" module="samplemodule"/> </standardmaster>
<standardmetatags> <include stylesheet="xsl/metatags.xsl" module="samplemodule"/> </standardmetatags>
<standardpage name="page" xml="xml/frame.xml" module="samplemodule"/>
Including or importing a stylesheet from a module into another stylesheet can be done using a module URI:
<xsl:import href="module://samplemodule/xsl/test.xsl"/>
A module URI complies with the hierarchical URI syntax [scheme:][//authority][path][?query][#fragment]. The scheme has to be set to module and the authority part is set to the module name. The path has to be relative to the PUSTEFIX-INF folder.
Such URIs also can be programmatically used to get a module resource with the ResourceUtil helper class:
Resource res = ResourceUtil.getResource("module://samplemodule/txt/test.txt");Resources included from modules can be overridden by providing an alternative version in the project itself or in the common folder. Resources included from the common folder can be overridden by a project specific version as well.
Therefor you have to place the overriding file under the same relative path in your project/application or the common directory and set the search attribute of the according Pustefix tag to dynamic.
<pfx:include part="foo" href="txt/common.xml" module="samplemodule" search="dynamic"/>
<pfx:image src="img/image.png" module="samplemodule" search="dynamic"/>
Setting search to dynamic the resource will be searched at different pre-defined locations in the following order:
projects/myproject/path/to/resourceprojects/common/path/to/resourcePUSTEFIX-INF/path/to/resourcePUSTEFIX-INF/path/to/resource
If you omit the module attribute, the resource will be only searched in the project's and the common folder.
![]() | Note |
|---|---|
| If you're dynamically searching for an include part and a file is found in the fallback chain, but doesn't contain the requested part, the search will move along to the next level. In contrast, themes aren't taken into account, i.e. the first file containing the searched part will be returned, regardless of how specific the contained themes are. |
<pfx:image themed-path="img" themed-img="image.png" module="samplemodule" search="dynamic"/>
If you're dynamically including a themed image, the search iterates over the theme list on each fallback level, e.g. the theme fallback list "foo bar default" will result in the following search:
projects/myproject/img/foo/image.png
projects/myproject/img/bar/image.png
projects/myproject/img/default/image.png
projects/common/img/foo/image.png
projects/common/img/bar/image.png
projects/common/img/default/image.png
...
![]() | Note |
|---|---|
| If you're dynamically searching for a themed image and a matching file is found, the returned image will be the one with the most specific theme on the current fallback level, images matching more specific themes down in the fallback chain aren't taken into account. |
Resources from modules can be overridden by other modules. Therefor a module can declare, which resources from which module it wants to override. This declaration is done within its module descriptor (see Chapter 7, Module Support).
<module-descriptor xsi:schemaLocation="http://pustefix.sourceforge.net/moduledescriptor200702 http://pustefix.sourceforge.net/moduledescriptor200702.xsd" xmlns="http://pustefix.sourceforge.net/moduledescriptor200702" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <module-name>module-B</module-name> <override-modules> <module name="module-A"> <resource path="txt/common.xml"/> </module> </override-modules> </module-descriptor>
In this example module-B overrides a resource of module-A. This means that module-B is inserted into the fallback chain for the resource right after the common folder and right before module-A.
<pfx:include part="foo" href="txt/common.xml" module="module-A" search="dynamic"/>
Trying to dynamically include part foo from txt/common.xml and module-A will result in the following fallback chain:
projects/myproject/txt/common.xml projects/common/txt/common.xml module://module-B/txt/common.xml module://module-A/txt/common.xml
An overriding module itself can be overridden by another module. Consider the following descriptor for sample-module-C:
<module-descriptor xsi:schemaLocation="http://pustefix.sourceforge.net/moduledescriptor200702 http://pustefix.sourceforge.net/moduledescriptor200702.xsd" xmlns="http://pustefix.sourceforge.net/moduledescriptor200702" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <module-name>module-C</module-name> <override-modules> <module name="module-B"> <resource path="txt/common.xml"/> </module> </override-modules> </module-descriptor>
This will result in the following new fallback chain:
projects/myproject/txt/common.xml projects/common/txt/common.xml module://module-C/txt/common.xml module://module-B/txt/common.xml module://module-A/txt/common.xml
Now we add another module, called module-A-ext, which is also overriding module-A:
<module-descriptor xsi:schemaLocation="http://pustefix.sourceforge.net/moduledescriptor200702 http://pustefix.sourceforge.net/moduledescriptor200702.xsd" xmlns="http://pustefix.sourceforge.net/moduledescriptor200702" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <module-name>module-A-ext</module-name> <override-modules> <module name="module-A"> <resource path="txt/common.xml"/> </module> </override-modules> </module-descriptor>
The new fallback chain looks as follows:
projects/myproject/txt/common.xml projects/common/txt/common.xml module://module-C/txt/common.xml module://module-B/txt/common.xml module://module-A-ext/txt/common.xml module://module-A/txt/common.xml
Module module-A-ext is inserted right before module-A. This is done because the override mechanism processes the modules in alphabetic order to ensure predictable behaviour, i.e. it iterates over all modules overriding the current module resource, adding the overriding module on top of the module fallback list, and then recursively processes this module the same way. So changing the module name to module-D would have been resulted in placing the module right before module-C.
Pustefix provides a role-based authorization mechanism. You can define arbitrary roles,
declare logical operations/combinations on this roles using authconstraints,
and assign these authconstraints to pagerequests.
A role is defined using an according XML element with a unique
name attribute value. Setting the initial
attribute to true the role will be automatically
set on context initialization.
<role name="MYROLE" initial="true"/>
Authconstraints can combine various authorization conditions,
supported conditions are: hasrole, and,
or and not, represented by according XML elements.
Using the authpage attribute you can define the page, which should be
called on authorization failure. Using the default attribute you can
set one toplevel authconstraint to be the default one for
all pagerequests having no authconstraint asssigned.
<authconstraint id="MYCONSTRAINT" authpage="login" default="true"> <or> <hasrole name="MYROLE"/> <hasrole name="OTHERROLE"/> </or> </authconstraint>
Pagerequests can either define new authconstraints as
child elements or can reference existing toplevel authconstraints by
their id.
<pagerequest name="mypage"> <authconstraint ref="MYCONSTRAINT"/> ... </pagerequest> <pagerequest name="mypage"> <authconstraint authpage="login"> <hasrole name="MYROLE"/> </authconstraint> ... </pagerequest>
You can programmatically set/query roles using the de.schlund.pfixcore.auth.Authentication
object, which can be retrieved from the Context calling its getAuthentication()
method.
public interface Authentication { public boolean hasRole(String roleName); public boolean addRole(String roleName); public boolean revokeRole(String roleName); public Role[] getRoles(); }
You can add a new role using addRole(), revoke exisiting
roles using revokeRole() or check for a role using
hasRole(). Using getRoles() you get an array
of all currently set roles. If a default role is defined, this role will be initially set.
You can also query the current roles from within your XML/XSLT code using the
XPath extension function pfx:hasRole(rolename)
<ixsl:if test="pfx:hasRole('MYROLE')"> ... </ixsl:if>
If you try to access a page for which the authconstraint isn't fulfilled, you're forwarded
to the according login page. The login page has to be a regular page, e.g. containing a login form.
Login forms require the type attribute set to auth.
<pfx:forminput type="auth"> ... </pfx:forminput>
The framework automatically inserts an authentication element into
the login page's DOM tree. This element contains the state of the authenticated
flag, the targetpage which should be accessed, the current roles
and the authorizationfailure containing the violated
authconstraint.
<formresult> <authentication authenticated="true" targetpage="mypage"> <roles> <role name="SOMEROLE"/> </roles> <authorizationfailure authorization="pageaccess" target="mypage"> <authconstraint> <hasrole name="MYROLE"/> </authconstraint> </authorizationfailure> </authentication> ... </formresult>
You can use this information to decide which information to display on the login page.
Example 6.1. Configuring roles
The following example defines three roles. The role
ANONYMOUS is configured as initial role,
i.e. every session/context has this role automatically set from the beginning.
There are two top-level authconstraints. The authconstraint
AC_DEFAULT is declared as default, i.e. pages, having no explicitly set
authconstraint, will get this one. The authconstraint's authpage
is set to login and it has a simple condition saying that it requires the role
ANONYMOUS.
The authconstraint AC_KNOWN declares that
it requires the USER or the ADMIN role. This
authconstraint is referenced by the pagerequest userpage, using
an empty authconstraint element having a ref attribute containing the
authconstraint's id.
The pagerequest adminpage contains an anonymous
authconstraint element, which defines the role
ADMIN as requirement.
<contextxmlserver> <role name="ANONYMOUS" initial="true"/> <role name="USER"/> <role name="ADMIN"/> <authconstraint id="AC_DEFAULT" authpage="login" default="true"> <hasrole name="ANONYMOUS"/> </authconstraint> <authconstraint id="AC_KNOWN" authpage="login"> <or> <hasrole name="USER"/> <hasrole name="ADMIN"/> </or> </authconstraint> <pagerequest name="home"> ... </pagerequest> <pagerequest name="login"> <input> ... </input> </pagerequest> <pagerequest name="adminpage"> <authconstraint authpage="login"> <hasrole name="ADMIN"/> </authconstraint> ... </pagerequest> <pagerequest name="userpage"> <authconstraint ref="AC_KNOWN"/> ... </pagerequest> ... </contextxmlserver>
You can extend the authentication mechanism by providing custom conditions (additionally to the predefined conditions: hasrole, and, or, not). Therefore you just have to implement the Condition interface and register your implementation class in the context configuration file. Then your custom condition can be used within authconstraints, just as the builtin conditions and arbitrarily mixed with them.
public interface Condition { public boolean evaluate(Context context); }
You have to implement the evaluate method, which returns if the condition is fulfilled. Therefore the evaluation logic can access the Context. Implementations shouldn't change the data model, perform fast and hold only immutable state (or be stateless).
The following example shows a condition which retrieves a ContextResource for a customer and checks if its debit exceeds a configured limit. The limit is automatically set to a value configured as property in the condition's configuration.
package example; import de.schlund.pfixcore.auth.Condition; import de.schlund.pfixcore.workflow.Context; import example.ContextCustomer; public class PremiumCustomerCondition implements Condition { private float limit; public boolean evaluate(Context context) { ContextCustomer contextCustomer=context.getContextResourceManager().getResource(ContextCustomer.class); return contextCustomer.getTotalDebit() >= limit; } public void setLimit(float limit) { this.limit = limit; } }
Let's look how this condition is registered and used in the context configuration:
<condition id="isPremiumCustomer" class="example.PremiumCustomerCondition"> <property name="limit" value="100000"/> </condition> <authconstraint id="..." authpage="..."> <and> <hasrole name="..."/> <condition ref="isPremiumCustomer"/> </and> </authconstraint>
Conditions are registered using top-level condition elements. They require an id and a class attribute. Authconstraints can reference conditions using condition elements with an according ref attribute.
Conditions can be checked from within XML/XSL using the pfx:condition function, e.g.:
<ixsl:if test="pfx:condition('isPremiumCustomer')"> ... </ixsl:if>
Roles by default are configured within the context configuration. As an alternative approach you're able to plug-in your own RoleProvider implementation. Thus you can provide roles programmatically or from another source.
You just have to implement the RoleProvider interface and register the implementation class in the context configuration.
public interface RoleProvider { public Role getRole(String roleName) throws RoleNotFoundException; public List<Role> getRoles(); }
The getRole method returns a Role object by name.
The getRoles method returns a list of all available roles.
public interface Role { public String getName(); public boolean isInitial(); }
Role objects can either be created by implementing the
Role interface or by using the default implementation
de.schlund.pfixcore.auth.RoleImpl. Role implementations
have to return a unique name and if they should be initially set.
The following example shows a simple RoleProvider implementation,
which holds the roles in a programmatically filled map.
public class MyRoleProvider implements RoleProvider { private Map<String, Role> roles; public MyRoleProvider() { roles = new HashMap<String, Role>(); Role role = new RoleImpl("ADMIN", false); roles.put(role.getName(), role); ... } public Role getRole(String roleName) throws RoleNotFoundException { Role role = roles.get(roleName); if(role == null) throw new RoleNotFoundException(roleName); return role; } public List<Role> getRoles() { return new ArrayList<Role>(roles.values()); } }
The RoleProvider implementation is registered using the
context configuration top-level roleprovider element with
a class attribute. You can optionally configure properties,
which will be automatically injected into the RoleProvider instance using
according setter methods (Spring bean-style setter injection).
<roleprovider class="example.MyRoleProvider"> <property name="..." value="..."/> </roleprovider>
You should be aware that the current mechanism doesn't support dynamic RoleProviders. The provided roles have to be constant, i.e. they're read at application startup time and aren't updated at a later time.
Pustefix provides AJAX-support via SOAP webservices and JSON RPC-style services. Autogenerated stubs and dynamic proxies make it easy to implement new services without any knowledge of the protocol details, among other things the protocol can be switched without changing the Javascript or Java code, additionally a service can work with both protocols at the same time.
Implementing a service can be done in two ways: Either you can export an arbitrary Spring bean as a webservice or you have to create a special service class which derives from an abstract framework class. The first way is recommended, because your service can be a POJO without any framework dependencies.
org.pustefixframework.webservices.AbstractService. Thus the implementation class
inherits the method getContextResourceManager(), which can be used to access
ContextResources. The service has to be declared explicitly in the project's
central webservice configuration file projectdir/conf/webservice.conf.xml (see Configuration section below).
![]() | Note |
|---|---|
If you want to use SOAP as service protocol, you have to note that the SOAP runtime used by Pustefix (Sun's JAXWS implementation)
requires that service classes are annotated with the |
The Pustefix build process automatically generates Javascript SOAP stubs for all declared services (if the SOAP protocol is enabled). It also generates the deployment descriptors needed by Axis, the SOAP/webservices library used in Pustefix. Optionally it can create WSDL descriptions for all webservices. The JSON protocol support needs no build time processing, cause the stubs can either be dynamic proxies or generated at runtime by the server.
Using the services on the client-side is quite easy too. You just have to include the necessary Javascript libraries provided by Pustefix and instantiate a service proxy object via Javascript. The service proxy object delegates your local method calls to the server, which invokes the according methods on your service implementation and returns the result. The remote invocation works completely transparent for the client, just as a local method call.
The SOAP proxy is an autogenerated Javascript stub, which uses Pustefix's home-brewed Javascript SOAP implementation (as long as the native browser support for SOAP isn't sufficient). JSON provides two different proxy models: a dynamic proxy which is set up dynamically at runtime (during its instantiation the service's method list is requested from the server and according Javascript methods are created) and a stub which is generated at runtime by the server.
The client-server communication is done using the XmlHttpRequest object (supported by most modern browsers). Thus service requests can be done either synchronous or asynchronous (passing a callback function as parameter instead of getting the result directly from the service method). There's also a fallback mechanism (via hidden iframes) for older browsers that do not know a XmlHttpRequest object.
Spring-managed services are configured as part of the Spring configuration. You can export an arbitrary bean as webservice
using the webservice element (from the http://pustefixframework.org/schema/webservices
namespace). Therefor you have to reference it using the ref attribute and set a unique
servicename.
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ws="http://pustefixframework.org/schema/webservices" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd http://pustefixframework.org/schema/webservices http://pustefixframework.org/schema/webservices/pustefix-webservices.xsd"> <bean id="MyBeanId" class="mypackage.MyBean" scope="session"> <aop:scoped-proxy/> </bean> <ws:webservie id="Webservice_MyBean" servicename="MyBean" interface="MyBeanServiceInterface" ref="MyBeanId" protocol="ANY" /> </beans>
You can optionally set an interface which defines the methods which should be exported (by default all
public methods are exported, if you're using SOAP/JAXWS you can also exclude methods using the @WebMethod(exclude=true)
annotation). Using the protocol attribute you can set the webservice protocol (default is JSONWS,
other options are SOAP or ANY).
The webservice configuration file PROJECTNAME/conf/webservices.conf.xml contains global settings
for the runtime system, default settings for webservices, and optionally webservice-specific settings.
If you're using Spring-managed services only, you can ignore the webservice-specific settings in this file as they're
done as part of the Spring configuration. The global and default settings are applied to the managed services too.
<webservice-config schemaLocation="http://pustefix.sourceforge.net/wsconfig200401 ../../core/schema/wsconfig200401.xsd" xmlns="http://pustefix.sourceforge.net/wsconfig200401" xmlns:xsi="xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <!-- The webservice-global section contains general configuration options, which are applied to all webservices. Some of the options can be overridden individually for single webservices. --> <webservice-global> <!-- Set the path under which the webservice servlet is reachable (as defined in you project configuration) [Optional - Default: /xml/webservice] --> <requestpath>/xml/webservice</requestpath> <!-- Set if WSDL should be generated for SOAP-enabled webservices and where to store it (absolute path within the project's webapp directory) [Optional - Default: enabled="true" repository="/wsdl"] --> <wsdlsupport enabled="true" repository="/wsdl"/> <!-- Set if Javascript stubs should be generated and where to store it (absolute path within the project's webapp directory - only for SOAP). The jsnamespace attribute controls what namespace prefix the generated Javascript should use (COMPAT: WS_, COMPAT_UNIQUE: WS_ for SOAP JWS_ for JSON, JAVA_NAME: full Java class name, other values are used as namespace themselves, dot-separated namespaces are supported too, code to setup the according JS context objects gets auto-generated) [Optional - Default: enabled="true" repository="/wsscript" jsnamespace="COMPAT"] --> <stubgeneration enabled="true" repository="/wsscript" jsnamespace="COMPAT"/> <!-- Set which service protocol should be used. You can set a certain protocol (SOAP|JSONWS) or allow all protocols (ANY). If you choose to use JSONWS only, you should set the type to JSONWS to accelerate the build process, otherwise the SOAP stubs will be generated unnecessarily. [Optional - Default: type="JSONWS"] --> <protocol type="ANY"/> <!-- Set which SOAP encoding style should be used. The best supported style is rpc/encoded. The server-side also supports other styles like rpc/literal and document/literal, but the client-side support for these styles is rudimentary. [Optional - Default: style="rpc" use="encoded"] --> <encoding style="rpc" use="encoded"/> <!-- Set if the JSON representation of serialized Java beans should be augmented with type meta information (classhinting). [Optional - Default: classhinting="false"] --> <json classhinting="true"/> <!-- Set if webservice requests need to have a valid HTTP/Pustefix-Session (servlet) or they should work without one too (none). [Optional - Default: type="servlet"] --> <session type="servlet"/> <!-- Set the service object's scope. Scope application means that the service object is created only once and shared between all sessions, session, that it's created once per session, request, that it's created newly for each request. [Optional - Default: type="application"] --> <scope type="application"/> <!-- Set if only https requests should be allowed. [Optional - Default: force="false"] --> <ssl force="true"/> <!-- Set the identifier of the Context the services need to access (from /contextxmlserver/servletinfo/@name in your project's ContextXMLServer configuration file). Via synchronize you can control whether the service requests should be synchronized on the Pustefix Context (default is true). [Optional - Default: synchronize="true"] --> <context name="pfixcore_project:webservice::servlet:config" synchronize="true"/> <!-- There are some options which should be configured differently in development and production mode. Therefor you can use the following choose/when elements (which are optional, you can just leave them out). --> <choose> <when test="$mode = 'prod'"> <!-- Set if admin tool should be available (see Development Tools). [Optional - Default: enabled="false"] --> <admin enabled="false"/> <!-- Set if monitor tool should be available (see Development Tools). [Optional - Default: enabled="false"] --> <monitoring enabled="false"/> <!-- Set if extensive logging should be enabled (i.e. logging of all request/response messages in pustefix-webservice.log). [Optional - Default: enabled="false"] --> <logging enabled="false"/> <!-- Set a FaultHandler, which will process exceptions before they are sent to the client (see Exception Handling). [Optional - Default: none] --> <faulthandler class="de.schlund.pfixcore.webservice.fault.EmailNotifyingHandler"> <param name="recipients" value="errors@domain.de"/> <param name="sender" value="pfxerror@domain.de"/> <param name="smtphost" value="localhost"/> </faulthandler> </when> <otherwise> <admin enabled="true"/> <monitoring enabled="true" scope="session" historysize="10"/> <logging enabled="true"/> <faulthandler class="de.schlund.pfixcore.webservice.fault.LoggingHandler"/> </otherwise> </choose> </webservice-global> <!-- Each service has to define name, interface and implementation. All other options are optional and inherited from the global section respectively. Some global options can be overridden here (stubgeneration, protocol, encoding, json, session, scope, ssl, context, faulthandler). --> <webservice name="Counter"> <!-- Set the service interface. [Mandatory] --> <interface name="de.schlund.pfixcore.example.webservices.Counter"/> <!-- Set the service implementation class. [Mandatory] --> <implementation name="de.schlund.pfixcore.example.webservices.CounterImpl"/> </webservice> <webservice name="...">...</webservice>... </webservice-config>
Pustefix provides a special exception handling mechanism for AJAX services. You can register predefined or custom FaultHandlers, which are automatically called before an exception is sent to the client. Thus you can filter exceptions, change them or do some logging or notification stuff.
There are three predefined FaultHandlers:
de.schlund.pfixcore.webservice.fault.LoggingHandler: as its name denotes, it just logs all Exceptions
via log4j (location can be configured within the general log4j configuration)
de.schlund.pfixcore.webservice.fault.EmailNotifyingHandler: this handler sends Email notifications (recipients, sender and smtphost can be configured as parameters, see the Configuration section)
de.schlund.pfixcore.webservice.fault.ExceptionProcessorAdapter: this handler implements a direct connection to the general Pustefix exception processing mechanism via ExceptionProcessors (and only makes sense if the configured ExceptionProcessor only consumes exceptions, but doesn't produce any output, like a HTML error page)
You are free to implement your own FaultHandler and register it with your services in the webservice configuration file.
You just have to extend the abstract base class de.schlund.pfixcore.webservice.fault.FaultHandler and
implement the abstract methods init() and handleFault(Fault). The Fault object
methods getThrowable() and setThrowable can be used to get the thrown exception and
to change or replace it.
You also can derive from one of the predefined handlers. E.g. you can extend the EmailNotifyingHandler by overriding the two methods public boolean isInternalServerError(Fault fault) and public boolean isNotificationError(Fault fault) to customize if error details should be hidden from the client and if a notification mail should be sent for certain exceptions.
Pustefix provides some tools, which can help you during the development process. It provides an admin and monitoring webinterface. The admin tool lists all registered services with their service methods. The monitoring tool shows a history of the last requests including the request and response messages.
You can access these tools by including the special tag <pfx:webserviceconsole/> (see Section 4.5.4, “Using the Pustefix console”)
into your Pustefix page and enabling them in the global webservice configuration (as shown in the Configuration section:
the admin and monitoring elements within the choose/when elements).
By the way, if any unexplainable problems occur, you're recommended to take a look into the logfile pustefix-webservice.log, where (if log4j log level is set to DEBUG) among other things the request and response messages are logged too.
Doing asynchronous webservice calls, you can choose between two different webservice callback mechanisms.
You can pass a function reference as last argument of the service method call (or last but one, if you want to set a request ID too). After receiving the server's response your function will be automatically called, passing the result (if no exception occurred), the request ID (if set) and an Error object (if exception occurred).
var service=new WS_Service();
var callback=function(result,requestid,exception) {
if(exception) {
...
} else {
...
}
}
var requestid="...";
service.serviceMethod(arg0, arg1, ... , callback); //asynchronous with callback
service.serviceMethod(arg0, arg1, ... , callback, requestid); //asynchronous with callback and requestidYou can instantiate the webservice stub with an object reference as argument of the constructor function. Your object has to provide methods of the same name as the service methods which are automatically called back by the stub (you don't have to pass a callback object or function to the serviceMethod).
var object={
serviceMethod: function(result,requestid,exception) {
if(exception) {
...
} else {
...
}
}
};
var service=new WS_Service(object); //instantiate service with callback object
//optionally you can pass the scope object within
var requestid="...";
service.serviceMethod(arg0, arg1, ...); //asynchronous
service.serviceMethod(arg0, arg1, ... , requestid); //asynchronous with requestid
If you're using JSON stubs, you can optionally pass a scope object as second constructor argument. Thus this scope is used as the "this" context argument instead of the object itself when the object's callback method is called. [Since: 0.13.1]
var service = new WS_Service(object, scope);
Supported Java types are:
Primitive, wrapper, String and Date types are mapped to their according Javascript counterpart (Number, Boolean, String, Date). Java beans are mapped to general Javascript objects with the according properties (there's no representation/simulation of the Java bean's type hierarchy, using JSON you optionally can enable classhinting, which provides every object instance with a special property - javaClass - containing the full Java class name).
The Java bean mapping mechanism supports both, so-called simple properties (having according getter and setter methods as defined in the Java Bean Specification) and public members (without access methods).
Additional Java types supported by JSON services:
Because of the restrictions of the JSON format, especially the absence of type information and the usage of builtin Javascript types (arrays for Lists, objects for Maps), the support for Lists and Maps itself is restricted: deserializing Lists to Java requires a parameterized type parameter and this type has to be instantiable (or be a parameterized List or Map itself), the same with Map values, Map keys have to be java.lang.String instances.
The JSON bean (de-)serialization mechanism supports customizable bean property mappings via Java annotations.
You can exclude individual properties from (de-)serialization by marking the according getter with an @Exclude
annotation or you can exclude all properties by marking the bean class with an @ExcludeByDefault
annotation and include individual properties with @Include annotations at their getters (marking public members is
supported too).
The following examples show the different annotations in action. Both class definitions give access to the foo and baz property and restrict access to the bar property. The first class definition includes all properties by default (which is the default behaviour without a type annotation) and excludes the bar property, while the second excludes all properties by default and includes the foo and baz properties:
public class A {
int foo;
int bar;
int baz;
public int getFoo() {return foo;}
public void setFoo(int foo) {this.foo=foo;}
@Exclude
public int getBar() {return bar;}
public void setBar(int bar) {this.bar=bar;}
public int getBaz() {return baz;}
public void setBaz(int baz) {this.baz=baz;}
}@ExcludeByDefault
public class A {
int foo;
int bar;
int baz;
@Include
public int getFoo() {return foo;}
public void setFoo(int foo) {this.foo=foo;}
public int getBar() {return bar;}
public void setBar(int bar) {this.bar=bar;}
@Include
public int getBaz() {return baz;}
public void setBaz(int baz) {this.baz=baz;} }
}All annotations only take effect in the declaring class and aren't inherited. Thus, if you derive your bean class, the type annotation of your base class has no effect, so declaring new properties in your class will include them by default. Properties included or excluded in your base class will be included/excluded in your inherited class too, if you don't redeclare or override this properties. The following example demonstrates this behaviour:
public class A {
int foo;
int bar;
int baz;
public int getFoo() {return foo;}
public void setFoo(int foo) {this.foo=foo;}
@Exclude
public int getBar() {return bar;}
public void setBar(int bar) {this.bar=bar;}
public int getBaz() {return baz;}
public void setBaz(int baz) {this.baz=baz;}
}
@ExcludeByDefault
public class B extends A {
int hey;
int ho;
@Include
public int getHey() {return hey;}
public void setHey(int hey) {this.hey=hey;}
public int getHo() {return ho;}
public void setHo(int ho) {this.ho=ho;}
@Override
public int getBaz() {return super.getBaz();}
}Using the @Alias annotation you can control the name used for (de-)serialization (i.e. the name used as JSON property name). The following example shows how to add aliases to a public member and a property getter.
public class A {
@Alias("mybaz")
public int baz;
int foo;
@Alias("myfoo")
public int getFoo() {return foo;}
public void setFoo(int foo) {this.foo=foo;}
}If you don't like Java annotations or you want to overwrite existing annotations, you can also customize your beans using a XML configuration file. The file has to be named beanmetadata.xml and has to be placed in the project configuration directory or in a META-INF directory that's part of the classpath.
<bean-metadata xsi:schemaLocation="http://pustefix.sourceforge.net/bean-metadata http://pustefix.sourceforge.net/beanmetadata.xsd"> <bean class="de.schlund.pfixcore.webservice.beans.A"> <property name="foo" alias="myfoo"/> <property name="bar" exclude="true"/> </bean> <bean class="de.schlund.pfixcore.webservice.beans.B" exclude-by-default="true"> <property name="hey"/> </bean> </bean-metadata>
The format is very simple: Create a bean element referencing the class you want to annotate. Add property elements referencing the properties you want to annotate. All Java annotations have an according XML attribute counterpart you can add to these elements.
Pustefix provides a lightweight object serialization mechanism, which can be used to serialize arbitrary objects into the result DOM without having to do any DOM operations by yourself. The XML binding is customizable via Java annotations within the bean classes.
The framework supports arbitrary Beans, Arrays,
Collections, Maps, Numbers (including
the primitive types and their object wrapper types), Strings,
and Date/Calendar. To support other types or to serialize
to a custom format, it's possible to write your own serializers and annotations (to attach
them to the according bean properties).
The serialization of beans can be customized using the generic Pustefix bean annotations,
which are known from the JSON serialization framework. You can exclude individual properties
from serialization by marking the according getter with an @Exclude
annotation or you can exclude all properties by marking the bean class with an
@ExcludeByDefault annotation and include individual properties with
@Include annotations at their getters (marking public members is supported
too). Using the @Alias annotation you can control
the name used as the resulting attribute or element name.
The serialization to the result tree is done by calling one of the static addObject
methods of the ResultDocument class. The element
argument is the parent DOM element at which the serialized XML will be appended, the optional name
argument can be used to create an additional child element for the serialized XML. The
object argument is the object, which should be serialized.
public class ResultDocument { ... public static Element addObject(Element element, Object object) {...} public static Element addObject(Element element, String name, Object object) {...} }
The default serialization process tries to produce relatively compact XML. Thus it favours attributes over elements and serializes so-called simple types, which can be represented as strings, into attributes where it's possible and makes sense, e.g. for bean properties.
Let's look at an example, which shows the serialization of a simple bean using bean and serializer annotations to customize the serialization behaviour:
... public class Account { private long accountNo; private float debit; ... public long getAccountNo() { return accountNo; } public void setAccountNo(long accountNo) { this.accountNo = accountNo; } @Alias("balance") public float getDebit() { return debit; } public void setDebit(float debit) { this.debit = debit; } public Currency getCurrency() { return currency; } public void setCurrency(Currency currency) { this.currency = currency; } @DateSerializer("yyyy-MM-dd HH:mm:ss") public Calendar getOpeningDate() { return openingDate; } public void setOpeningDate(Calendar openingDate) { this.openingDate = openingDate; } @Exclude public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } }
Here you see how the bean's serialized to the ResultDocument within
a ContextResource:
...
public class ContextAccountImpl implements ContextAccount {
private Account account;
...
public void insertStatus(ResultDocument resdoc, Element elem) throws Exception {
ResultDocument.addObject(elem,"account",account);
}
}The resulting DOM fragment looks like this:
<formresult serial="1199439160721"> ... <data> <account accountNo="2000123" currency="EUR" balance="332.54" openingDate="2003-11-04 09:15:38"/> </data> ... </formresult>
The data element is the ContextResource's root node as configured
in the configuration file. Calling addObject with the additional
account argument, the serialized bean isn't added directly to the
data element, but an additional element is used. The bean's properties are serialized
as attributes of this element.
The debit property is renamed to balance using
the @Alias annotation. The comment property is
excluded using the @Exclude annotation. The openingDate
property is serialized using the built-in DateSerializer, which can
be customized using the @DateSerializer annotation. Thus you can
provide your own date format pattern (must be a pattern supported by
java.text.SimpleDateFormat).
Only simple type properties, i.e. properties which can be serialized to string values, can
be represented as attributes. If the Account bean would have an additional
property customer of a bean type, e.g. a Customer class,
this property would be serialized as a child element:
<formresult serial="1199439160721"> ... <data> <account accountNo="2000123" balance="EUR" debit="332.54" openingDate="2003-11-04 09:15:38"> <customer customerId="100000" firstName="Mike" lastName="Foo"/> </account> </data> ... </formresult>
Collections and Arrays are represented using
an element for each entry. The element name is derived from the the simple name
of the entry's class (without package name and starting lowercase):
<formresult serial="1199439160721"> ... <data> <account accountNo="2000000" currency="EUR" balance="3124.49" openingDate="2003-10-23 08:05:10"/> <account accountNo="2000123" currency="EUR" balance="332.54" openingDate="2003-11-04 09:15:38"/> <account accountNo="2001405" currency="EUR" balance="25123.11" openingDate="2005-01-13 10:10:10"/> </data> ... </formresult>
The element name can be changed using the @ClassNameAlias annotation, e.g.
to rename the account element to bankaccount:
@ClassNameAlias("bankaccount") public class Account { ... }
Maps are represented using an entry element for each
map entry. Key and value are represented by child elements (whereas the element names are
derived from the class names):
<formresult serial="1199439160721"> ... <data> <entry> <long>2000000</long> <account accountNo="2000000" currency="EUR" debit="3124.49" openingDate="2003-10-23 08:05:34"/> </entry> <entry> <key>2001405</key> <account accountNo="2001405" currency="EUR" debit="25123.11" openingDate="2005-01-13 10:10:34"/> </entry> <entry> <key>2000123</key> <account accountNo="2000123" currency="EUR" debit="332.54" openingDate="2003-11-04 09:15:34"/> </entry> </data> ... </formresult>
![]() | Changing the tag name for map entries |
|---|---|
The tag name, that is used for the entries in the map can be changed using the
|
Circular object references are handled by adding a xpathref attribute to
the according element. Its value is an absolute XPath expression referencing the according
object's element:
<formresult serial="1199702214819"> ... <data> <account accountNo="2000123"> <customer customerId="100000"> <accounts> <account accountNo="2000000"> <customer xpathref="/formresult/data[1]/account[1]/customer[1]"/> </account> <account xpathref="/formresult/data[1]/account[1]"/> <account accountNo="2001405"> <customer xpathref="/formresult/data[1]/account[1]/customer[1]"/> </account> </accounts> </customer> </account> </data> ... </formresult>
In this example the Account bean has a reference to a Customer
bean, which itself has a reference to all of its Accounts. You can see that all
beans, which were already serialized (as ancestors in the tree) contain an according back-reference.
Pustefix already provides several XML serializers for common serialization tasks.
Simple serializers serialize scalar values (like strings, numbers or booleans). They are added as a new attribute on the current tag.
Complex serializers are used to serialize complex data structures. These always result in new tags that are being added to the document.
The @ForceElementSerializer will create an XML tag for primitive
values instead of writing the value to an XML attribute.
It can be combined with any simple type serializer (see the section called “Simple serializers”.
Strings that contain XML code can be inserted as XML fragment the the resulting document by using
the @XMLFragmentSerializer:
public class FragmentBean { private String myFragment = "<foo><bar baz=\"true\"/>character data</foo>"; @XMLFragmentSerializer public String getMyFragment() { return myFragment; } }
The XML, that is returned by the getMyFragment method is not treated
as a simple string, but as an XML fragment and thus, the content is not escaped, when
inserted in the document:
<?xml version="1.0" encoding="utf-8"?> <result> <myFragment> <foo><bar baz="true"/>character data</foo> </myFragment> </result>
If you don't like the default serialization mechanism or you use unsupported types, you can
write your own serializers. There are two types of serializers: SimpleTypeSerializers,
which can produce String values (e.g. for primitive types),
and ComplexTypeSerializers, which can produce structured XML data
(e.g. for bean types).
Implementing your own serializer just requires to implement the SimpleTypeSerializer
or ComplexTypeSerializer interface and create a custom annotation to be able
to attach your serializer to a bean property.
Let's look at an example of a SimpleTypeSerializer:
a custom String serializer, which allows to configure if
Strings should be ouput lower- or uppercase. Here's the implementation:
... import de.schlund.pfixcore.oxm.impl.AnnotationAware; import de.schlund.pfixcore.oxm.impl.SimpleTypeSerializer; import de.schlund.pfixcore.oxm.impl.annotation.StringSerializer; ... public class StringTypeSerializer implements SimpleTypeSerializer, AnnotationAware { private boolean doLowerCase; public void setAnnotation(Annotation annotation) { StringSerializer s=(StringSerializer)annotation; doLowerCase=s.value(); } public String serialize(Object obj, SerializationContext context) throws SerializationException { if(obj instanceof String) { String str=(String)obj; if(doLowerCase) str=str.toLowerCase(); else str=str.toUpperCase(); return str; } throw new SerializationException("Illegal type: "+obj.getClass().getName()); } }
The serializer implements the SimpleTypeSerializer interface.
Its serialize method checks if the passed object is of type String
and calls toLowerCase or toUpperCase before returning
the new String. The doLowerCase property controls which method is used.
This property is set within the setAnnotation method. The method is defined
in the AnnotationAware interface. This method is called by the framework
after the serializer is instantiated and passes the annotation set at the according bean
property. So you can access the configured values and configure your serializer.
Let's look at the according annotation definition:
...
@SimpleTypeSerializerClass(StringTypeSerializer.class)
@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface StringSerializer {
boolean value();
}
You have to annotate the custom annotation with a SimpleTypeSerializerClass
annotation with the serializer class as value, make the annotation available to
methods and fields using the @Target annotation and make it visible
at runtime using the @Retention annotation. The rest of the annotation
definition can be done according to your needs. In the example we just define a boolean
property indicating if the String should be converted to lower- or uppercase. Here you
see how the annotation is applied to serialize a customer's lastname as uppercase:
public class Customer { ... @StringSerializer(false) public String getLastName() {...} }
Let's look at an example of a ComplexTypeSerializer. We want to customize
the serialization of a Customer bean: the firstName and
lastName properties should be output together within a name
element:
public class Customer { ... public long getCustomerId() {...} public String getFirsstName() {...} public String getLastName() {...} public List<Account> getAccounts() {...} ... }
The serializer just implements ComplexTypeSerializer. We don't need to
implement AnnotationAware because our annotation will have no parameter
we may want to read:
public class CustomerTypeSerializer implements ComplexTypeSerializer { public void serialize(Object obj, SerializationContext context, XMLWriter writer) throws SerializationException { if(obj instanceof Customer) { Customer customer=(Customer)obj; writer.writeStartElement("name"); writer.writeCharacters(customer.getFirstName()+" "+customer.getLastName()); writer.writeEndElement("name"); context.serialize(customer.getAccounts(),writer); } else { throw new SerializationException("Illegal type: "+obj.getClass().getName()); } } }
The serialize method gets a XMLWriter object, which
is used to write the name element. Then the passed SerializationContext
is used to serialize the customer's accounts using the default serialization mechanism.
Finally we implement a custom annotation:
@ComplexTypeSerializerClass(de.schlund.pfixcore.example.bank.oxm.CustomerTypeSerializer.class) @Target({ElementType.METHOD,ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface CustomerSerializer {}
Here we apply the annotation to the Account bean's
customer property:
public class Account { ... @CustomerSerializer public Customer getCustomer() {...} ... }
Here's an excerpt of the resulting XML:
<formresult serial="1199705488403"> ... <data> <account accountNo="2000000" balance="3124.49" currency="EUR" openingDate="2003-10-23 08:05:22"> <customer> <name>Mike Foo</name> <account accountNo="..."/> <account accountNo="..."/> ... </customer> </account> </data> ... </formresult>
The annotation-based IWrapper creation provides an alternative to the usual, XML configuration based, IWrapper creation. Using this approach you create IWrappers from standard Java Beans by adding the necessary configuration data in the form of annotations.
The IWrappers are automatically created during the build process using the Sun JVM's apt
tool and a custom AnnotationProcessor which analyzes the Java bean's
source code and generates the according IWrapper sources.
You're making a bean to a template for an IWrapper by adding an
@IWrapper annotation to its class declaration. By default every
bean property that is of a so-called builtin type, i.e. has a pre-defined
IWrapperParamCaster implementation, will be automatically added
as an IWrapper parameter.
Builtin types are boolean, byte, double, float, int, long,
java.lang.Boolean, java.lang.Byte, java.lang.Double, java.lang.Float, java.lang.Integer,
java.lang.Long, java.lang.String, java.util.Date and Arrays
with components of these types.
Bean properties of an unknown type are either ignored or require a @Caster
annotation specifying an appropriate caster. Bean properties can be annotated at their
getter methods or at the field itself, if it's public. If a property of a builtin type should
be skipped you can mark the according property with a @Transient annotation.
Bean based IWrappers can be used to create new beans or fill existing beans with
the IWrapper's state. Therefore the IWrapperToBean class provides
the two static methods <T> T createBean(IWrapper wrapper, Class<T> beanClass)
and populateBean(IWrapper wrapper, Object obj).
Every IWrapper configuration element known from the XML configuration has an annotation
counterpart. Besides there are some special annotations like @IWrapper
and @Transient. In the following we'll give a short overview of
all avaible annotations:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface IWrapper {
String name() default "";
Class<? extends IHandler> ihandler() default IHandler.class;
}
@IWrapper(name="MyBeanWrapper",ihandler=MyBeanHandler.class)
public class MyBean {
...
}
The @IWrapper annotation is used to mark a class as template for
an IWrapper. The name attribute denotes the class name of the
generated IWrapper class (without package). By default the bean name with the
suffix Wrapper is used (and the same package). The ihandler
attribute denotes the IHandler implementation class.
@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Param {
String name() default "";
boolean mandatory() default true;
boolean trim() default true;
String missingscode() default "";
String[] defaults() default {};
}
@IWrapper(name="MyBeanWrapper",ihandler=MyBeanHandler.class)
public class MyBean {
...
@Param(name="MyValue",mandatory=false)
public int getValue() {...}
}
The @Param annotation is used to mark a bean property as parameter
and configure its name and all the other options known from the
IWrapper XML configuration. This annotation is optional, leaving it out, the property name
is used as name and the other attributes are set to their default values.
@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Caster {
Class<? extends IWrapperParamCaster> type();
Property[] properties() default {};
}
@IWrapper(name="MyBeanWrapper",ihandler=MyBeanHandler.class)
public class MyBean {
...
@Caster(type=SomeClassCaster.class)
public SomeClass getValue() {...}
}
The @Caster annotation denotes the caster implementation class.
The nested properties attribute can be used to set properties via
@Property annotations. That's the same as the cparam
elements in the XML configuration (the params/properties are set using according methods
prefixed with put_).
@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Property {
String name();
String value();
}
The @Property annotation is used as nested annotation within
the properties array attribute of various annotations. It consists
of simple name/value pairs.
@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PreCheck {
Class<? extends IWrapperParamPreCheck> type();
Property[] properties() default {};
}
@IWrapper(name="MyBeanWrapper",ihandler=MyBeanHandler.class)
public class MyBean {
...
@PreCheck(
type=de.schlund.pfixcore.generator.prechecks.RegexpCheck.class,
properties={
@Property(name="regexp",value="/^(M|L|XL)$/")
}
)
public String getValue() {...}
}
The @PreCheck annotation denotes the precheck implementation class
with optional properties/parameters.
@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PostCheck {
Class<? extends IWrapperParamPostCheck> type();
Property[] properties() default {};
}
@IWrapper(name="MyBeanWrapper",ihandler=MyBeanHandler.class)
public class MyBean {
...
@PostCheck(
type=de.schlund.pfixcore.generator.postchecks.IntegerRange.class,
properties={
@Property(name="range",value="0:2")
}
)
public int getValue() {...}
}
The @PostCheck annotation denotes the postcheck implementation class
with optional properties/parameters.
@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Transient {}
@IWrapper(name="MyBeanWrapper",ihandler=MyBeanHandler.class)
public class MyBean {
...
@Transient
public int getValue() {...}
}
The @Transient annotation can be used to avoid that a bean property
of a builtin type is made to an IWrapper parameter.
Scripted flows are script files written in XML that can be used to control a user session. The script file has the following form:
<scriptedflow version="1.0" xmlns="http://pustefix.oss.schlund.de/scriptedflow200602"> <!-- The instructions are placed here. --> </scriptedflow>
A scripted flow is started by using the special __scriptedflow=[FLOWNAME] parameter in the query string of a request URI. In this query string additonal parameters can be given which are then made available inside the script.
Variables can be used to store character data between steps within a scripted flow. A variable is set using the <set-variable name="variablename">content</set-variable> command. The content may consist of character data as well as the special <value-of select="<XPath-Expression>"/> command. Within XPath expressions variables can be referenced using $variablename.
Parameters are set when a scripted flow is started. If the query string contains e.g. "name=value" the value can be accessed from within XPath expressions using the special variable name $__param_name$. Parameters can not be overwritten from within the script and never change during the execution.
There is a special variable called $__pagename$ which contains the name of the page that would have been sent to the browser, if the last request was done directly by the browser. You can use this variable to check on which page you are at the moment. This variable cannot be written to by the script.
Scripted Flows are programmed using assorted statements which are explained here:
Gives control back to the user. The output page is rendered based on the current ResultDocument (which was generated by the last request) and sent to the browser. When the browser sends the next request (e.g. by sending a form or clicking on a link) it is processed as usual but instead of returning the resulting document directly to the browser the active scripted flow is continued right at the location it was suspended, using the new ResultDocument as the base for further processing.
<interactive-request> <param name="thename">static text combined with <value-of select="$thevar"/> calculated text (if needed)</param> <param name="otherparam">other text</param> </interactive-request>
This statement can take an arbitrary number of param tags as children. These tags can be used to provide values to pre-fill html form input elements.
Triggers a virtual request which is processed by the context like a user request. This statement can have an optional attribute page which specifies the name of the page the request should be sent to. If omitted, the current page (as defined by the Context) is used. This statement can take an arbitrary number of param tags below itself. These tags can be used to provide arguments to the request (e.g. to simulate submitted form data). The tag follows the scheme:
<virtual-request page="thepage" dointeractive="false|true|reuse"> <param name="thename">static text combined with <value-of select="$thevar"/> calculated text (if needed)</param> <param name="otherparam">other text</param> </virtual-request>
The dointeractive attribute is used in case the virtual request
returns an error. If set to "true" or "reuse",
the system will automatically fall back to do an interactive request (the
default is "false" which will just go on with the scripted flow
ignoring any errors). All errors will be cleared from the document used in the
interactive request. The difference between "true" and
"reuse" is that in the latter case, the param tags given to the
virtual request will be reused in the automatic interactive request, providing for
pre-filled input fields.
Please note that the __sendingdata or __sendingauthdata
parameter will not be added automatically but has to be added by the script developer,
if needed. The ResultDocument returned by this request is used as the basis for
further processing (e.g. XPath testing).
Sets the value of a variable. As the param tag within the virtual-request tag this tag can contain text data combined with the value-of tag. See Parameters & Variables for details on how to use variables.
Allows conditional execution of a code block. Based on a XPath expression which
is given using the test attribute the VM decides whether to execute the statements
given below the <if> tag or not. The XPath expression can
test for variables and parameters as well as querying the ResultDocument returned
by the last request.
This is the multi-branch equivalent to <if>. The form is
<choose> <when test="..."> <!-- ... --> </when> <when test="..."> <!-- ... --> </when> <otherwise> <!-- ... --> </otherwise> </choose>
The <otherwise> branch is optional and only executed if
none of the <when> conditions is met. Only the first
<when> branch whose condition is met is executed,
branches following this branch are not evaluated regardless of their condition.
Loops as long as condition is true. Like the <if> statement
this one takes a test attribute specifying a XPath expression. When the condition is
true, the code block below this tag is executed, otherwise the next statement after
the <while> statement is executed. After executing the block
the condition is rechecked, so the block is executed repeatedly until the condition
becomes false.
Jumps out of a <while> loop. This statement is allowed
anywhere below a <while> statement. It can be used to quit
the loop immediately and to go on with the next statement after the loop.
Consider these questions:
If you answered any of the questions above with yes ... Then this is for you!
This feature brings scripting support to IHandlers and States. It's possible to develop any of them in any Bean Scripting Framework supported language, by providing a script file's location inside any IWrapper definition file or inside a pagerequest's definition. Script files can be located in either the application's docroot or the classpath.
Instead of providing a class name inside an IWrapper's IHander definition like this:
<ihandler class="de.schlund.pfixcore.example.TShirtHandler"/>
you can define that a file containing dynamic language code should be used as an IHandler. It's done like this:
<ihandler class="script:sample1/script/ScriptingTShirt.js"/>
... that's it! (see below under "Path Definitions" on details about difference between scripts under docroot and scripts placed in classpath).
Simply create a file containing the dynamic language code inside the docroot or the classpath.
If you want to script a State, then simply use the above definition format inside a pagerequest's state definition, like in the following example:
<pagerequest name="scriptingstate"> <state class="script:sample1/script/ScriptingState?.bsh"/> </pagerequest>
The following rules have to be considered, when scripting IHandlers and States alike.
de.schlund.pfixcore.generator.IHandler and/or
de.schlund.pfixcore.workflow.State)
as functions inside your scripts.
Because the StateFactory caches and reuses State-instances across sessions and requests, you should be aware that script-wide global variables inside your scripts will be available to all requests and session.
This is basically the same behaviour as for States implemented in Java.
There are two alternatives for defining the location of your script file.
/ (slash character) to the location,
the script file is searched for in the classpath of your application.
No magic (or great effort) is applied when searching for the script file.
Instead simply ScriptingIHandler.getClass().getResourceAsStream()
is called to get the source of the script file.
Of course the Java platform can't interpret every arbitrary on it's own. Special Language libraries must be available to the application in order to execute script code in the respective languages.
Pfixcore comes with the Mozilla's Javascript implementation and the Beanshell distribution, so you can write IHandlers and States in Javascript or Beanshell without any further requirements.
For other languages, like groovy, python or ruby, you'll need their respective implementations for the Java platform. The BSF site holds information about where to find these implementations.
J2EE suppports filters than can be put in the request/response chain in order to modify the request and/or
response that is handled by the servlet. In order to be put into the chain, filters have to implement the
javax.servlet.Filter interface and to be declared in the webapplication deployment descriptor.
If needed, multiple filters can automatically be chained together.
Pustefix provides a special class called de.schlund.pfixxml.AbstractContextServletFilter that can be
used as a base for implementing servlet filters in Pustefix environments. This class takes care of casting the servlet
request and response objects provided by the web container to the corresponding HTTP servlet request or response types.
Besides the class automatically extracts the Context object from the HTTP session (if available) and passes it to the child class.
You should override the void doFilter(HttpServletRequest, HttpServletResponse, FilterChain, Context)
method to implement your own filter code. This code must either handle the request completely or has to call the
doFilter() method on the FilterChain object to pass the request to the next
instance in the filter chain. If you have to make sure that some code is always run after a request has been completely
processed, you can wrap the call to doFilter() within a try{}...finally{} block.
![]() | Overriding the init() method |
|---|---|
When overriding the |
The context instance provided by the base class is not fully initialized at this stage of processing. Basically only
session focused methods are available. Calling a method not being available will result in an
IllegalStateException being thrown.
Configuration of filters takes place in the Section 3.4, “Project configuration files”. Filter declarations are placed
directly within the <project/> tag:
<filter name="filtername"> <active>true</active> <class>my.filter.class</class> <foreigncontext>config</foreigncontext> <init-param> <param-name>myParam</param-name> <param-value>myValue</param-value> </init-param> </filter>
The name of the filter has to be unique within the whole project and is used in mappings. The filter is only
considered active if the <active> tag contains the value true.
The <class/> tag contains the fully qualified class name of the filter class.
The <foreigncontext/> tag takes a reference to a ContextXMLServlet.
The context instance of this servlet (if present within the session) will be provided to the filter code.
In addition to that an arbitrary number (0..n) of <init-param/> tags can be used to pass
configuration information to the filter. However the special parameter name "contextRef" is reserved
and may not be used.
Mappings can be performed by adding a <use-filter>filtername</use-filter> tag within a
<servlet> tag. This will force requests made to the servlet to be pre-processed by the servlet
filter.
Mappings to URIs can be performed by adding a <filter-mapping/> tag to the project node of
the configuration file. This filter-mapping tag uses exactly the same schema used in web application
deployment descriptors.