2. Core concepts
An appNG application relies on three core concepts: XML, Java and XSLT. The basic idea is that you define the structure of your user interface in XML and implement the business logic in Java. Finally, XSLT is the part that bridges the gap between the internal XML based structure and the HTML based user interface.
The following diagram illustrates these core concepts:
Below a short explanation of the diagram’s elements:
-
Source-XML
The XML files defining the structure of the user interface.
The following XML schema is used for the source-XML:
http://www.appng.org/schema/platform/appng-platform.xsd -
Business Logic
The implementation of the business logic in Java. -
Template
An appNG Template is a set of resources, used to transform the target-XML into HTML.
A Template consists of several XSL-stylesheet and all the assets (like JavaScript, CSS, icons) needed to render the HTML. It also contains aplatform.xml
file which is the blue-print for the generated target-XML. -
Target-XML
The target-XML is one single XML document using the template’splatform.xml
as a blue print. This XML document is being enriched with XML fragments that result from combining the static source-XML with the dynamic business logic. -
HTML
The resulting HTML that is delivered to the user’s browser.
3. Structure of an appNG application
An appNG application first and foremost is a standard Apache Maven project. Additionally, it uses an application-home
folder that contains the non-Java resources.
The listing below gives a rough overview about how an appNG application is composed:
├── application-home
│ ├── conf
│ │ └── <XML sources>
│ ├── dictionary
│ │ └── <resourcebundles>
│ ├── resources
│ │ └── <custom resources>
│ ├── sql
│ │ └── <type>
│ │ └── <DDL Scripts>
│ ├── xsl
│ │ └── <XSL stylesheets>
│ ├── application.xml
│ └── beans.xml
├── src
│ └── main
│ ├── java
│ └── resources
│ └── test
│ ├── java
│ └── resources
└── pom.xml
3.1. application-home/application.xml
(required)
This file describes the basic attributes of an application, like its name, version and the version of appNG it has been built for. Additionally, it defines which roles and permissions are required, which configuration properties exist and which kind of database (if any) is needed to run the application.
The XML schema definition (XSD) for application.xml
is located here:
http://www.appng.org/schema/application/appng-application.xsd
<?xml version="1.0" encoding="UTF-8"?>
<application xmlns="http://www.appng.org/schema/application" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.appng.org/schema/application http://www.appng.org/schema/application/appng-application.xsd">
<name>myartifactid</name> (1)
<display-name><![CDATA[myapplication]]></display-name>
<description><![CDATA[enter description here]]></description>
<long-description><![CDATA[enter long description here]]></long-description>
<version>1.0-SNAPSHOT</version>
<timestamp>20160829-1007</timestamp>
<appng-version>0.11.0</appng-version>
<roles> (2)
<role admin-role="true"> (3)
<name>Admin</name>
<description>an administrator with all permissions</description>
<permission id="output-format.html" />
<permission id="output-type.webgui" />
</role>
</roles>
<permissions> (4)
<permission id="output-format.html" />
<permission id="output-type.webgui" />
</permissions>
<properties> (5)
<property id="hibernateShowSql">false</property>
<property id="hibernateFormatSql">false</property>
</properties>
<datasources> (6)
<datasource type="mysql" />
</datasources>
</application>
1 | general information (name,version etc.) |
2 | the available roles, referencing permissions |
3 | a role with attribute admin-role=true is automatically added to the default admin group in appNG named Administrators on installation |
4 | the available permissions |
5 | the available properties |
6 | the database type(s) supported by the application |
3.2. application-home/beans.xml
(required)
Since each appNG application get it’s own Spring Application Context, a beans.xml
file must be provided to set up this context.
3.2.1. placeholders
Inside beans.xml
, you can use several placeholders, as explained below:
-
application properties as defined in
application.xml
Syntax:${<propertyName>}
Example:${myAppProperty}
-
site properties defined for the org.appng.api.model.Site
Constants for the property names are available in org.appng.api.SiteProperties
Syntax:${site.<propertyName>}
Example:${site.mailHost}
-
platform properties
Constants for the property names are available in org.appng.api.Platform.Property
Syntax:${platform.<propertyName>}
Example:${platform.devMode}
Platform properties are only available if the application is configured as a privileged application. |
Nested beans and profiles
You can use nested <beans>
inside beans.xml
, which can nicely be combined using the profile
-attribute and the application property activeProfiles.
3.3. application-home/conf
(optional)
This folder contains the XML definitions of the application’s Actions, Datasources and Pages. This folder can have any number of subfolders.
3.4. application-home/dictionary
(optional)
This folder contains the resource bundle files used for internationalization.
3.5. application-home/lib
(required)
This folder contains the application’s JAR file as well as 3rd party JARs that are not provided by the appNG platform.
3.6. application-home/sql
(optional)
For every <datasource>
listed in application.xml, there must be a subfolder containing the DDL scripts for that type. Since appNG uses Flyway (https://flywaydb.org) to keep the database up to date, the naming scheme V<version>__<name>.sql
must be used for those scripts, for example V1.2.5__add_new_table.sql
.
3.7. application-home/resources
(optional)
This is the place for custom CSS-, Javascript- and image-files, as well as other resources needed by your application. These assets are most likely used by some Custom XSL stylesheets.
3.8. application-home/xsl
(optional)
This optional folder can contain the custom XSL stylesheets of the application. See Custom XSL stylesheets for more details on that topic.
3.9. pom.xml
(required)
The pom.xml
makes use of the parent pom appng-application-parent
and configures the Maven Assembly Plugin.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>mygroupid</groupId>
<artifactId>myartifactid</artifactId>
<version>1.0-SNAPSHOT</version>
<name>myapp</name>
<description>enter description here</description>
<parent>
<groupId>org.appng</groupId>
<artifactId>appng-application-parent</artifactId>
<version>0.11.0</version>
</parent>
<properties>
<projectId>${project.name}</projectId>
<displayName>myapplication</displayName>
<longDescription>enter long description here</longDescription>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.appng</groupId>
<artifactId>appng-testsupport</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
The dependency tree of a standard appNG application looks like this:
\- org.appng:appng-api:jar:1.15.0:provided
+- org.appng:appng-forms:jar:1.15.0:provided
| +- commons-fileupload:commons-fileupload:jar:1.3.2:provided
| +- org.slf4j:slf4j-log4j12:jar:1.7.25:provided
| | \- log4j:log4j:jar:1.2.17:provided
| +- org.owasp.esapi:esapi:jar:2.1.0.1:provided
| | +- commons-configuration:commons-configuration:jar:1.10:provided
| | | \- commons-lang:commons-lang:jar:2.6:provided
| | +- commons-beanutils:commons-beanutils-core:jar:1.8.3:provided
| | +- xom:xom:jar:1.2.5:provided
| | +- org.beanshell:bsh-core:jar:2.0b4:provided
| | +- org.owasp.antisamy:antisamy:jar:1.5.3:provided
| | | +- net.sourceforge.nekohtml:nekohtml:jar:1.9.16:provided
| | | \- commons-httpclient:commons-httpclient:jar:3.1:provided
| | \- org.apache.xmlgraphics:batik-css:jar:1.8:provided
| | +- org.apache.xmlgraphics:batik-ext:jar:1.8:provided
| | +- org.apache.xmlgraphics:batik-util:jar:1.8:provided
| | \- xml-apis:xml-apis-ext:jar:1.3.04:provided
| \- org.jsoup:jsoup:jar:1.10.2:provided
+- org.appng:appng-xmlapi:jar:1.15.0:provided
| +- org.apache.commons:commons-lang3:jar:3.5:provided
| \- net.sf.saxon:Saxon-HE:jar:9.6.0-6:provided
+- org.appng:appng-tools:jar:1.15.0:provided
| \- net.sf.jmimemagic:jmimemagic:jar:0.1.5:provided
+- javax.servlet:javax.servlet-api:jar:3.1.0:provided
+- org.springframework:spring-context:jar:4.3.9.RELEASE:provided
| +- org.springframework:spring-aop:jar:4.3.9.RELEASE:provided
| \- org.springframework:spring-expression:jar:4.3.9.RELEASE:provided
+- org.springframework.ws:spring-ws-core:jar:2.4.0.RELEASE:provided
| +- commons-logging:commons-logging:jar:1.1.3:provided
| +- org.springframework.ws:spring-xml:jar:2.4.0.RELEASE:provided
| +- org.springframework:spring-oxm:jar:4.3.9.RELEASE:provided
| +- org.springframework:spring-web:jar:4.3.9.RELEASE:provided
| \- org.springframework:spring-webmvc:jar:4.3.9.RELEASE:provided
+- wsdl4j:wsdl4j:jar:1.6.3:provided
+- javax.validation:validation-api:jar:1.1.0.Final:provided
+- commons-io:commons-io:jar:2.5:provided
+- org.springframework.data:spring-data-commons:jar:1.13.4.RELEASE:provided
| \- org.slf4j:jcl-over-slf4j:jar:1.7.25:provided
+- com.fasterxml.jackson.core:jackson-core:jar:2.9.0.pr2:provided
+- com.fasterxml.jackson.core:jackson-databind:jar:2.9.0.pr2:provided
+- com.fasterxml.jackson.core:jackson-annotations:jar:2.9.0.pr2:provided
\- joda-time:joda-time:jar:2.9.9:provided
4. The Application
4.1. Output-format and -type
The output-format determines the basic format of the output, such as HTML, PDF or XML. The default output-format is html. A format can provide several output-types, each of them offering a different layout.
An output-type offers a certain layout for a output-format.
For example, there could be one output-type optimized for desktop clients and one optimized for mobile clients. The default output-type for the format html is webgui. That’s the reason why every application that provides a GUI must add the built-in permissions output-format.html
and output-type.webgui
to each role that should have access to that GUI.
See chapter 9 for more information about permissions.
4.2. Pre-defined Beans
The Spring application context of an appNG application comes with a number of pre defined beans of different scopes, as listed here:
-
conversionService
Type: org.springframework.core.convert.ConversionService
Scope: singleton
-
messageSource
This mesage source is built from the resource bundle file(s) provided in the application’sdictionary
folder.
Type: org.appng.api.support.ResourceBundleMessageSource
Scope: singleton
-
datasource
Only available if there is a<datasource>
present inapplication.xml
. Usually used to configure a JPAEntityManagerFactory
, as described in this section.
Type: javax.sql.DataSource.html
Scope: singleton -
environment
Theenvironment
is used to retrieve and set attributes of different Scopes.
Type: org.appng.api.Environment
Scope: request -
request
A wrapper for the current javax.servlet.http/HttpServletRequest.html, offering additional framework methods.
Type: org.appng.api.Request
Scope: request -
selectionFactory
A factory for building selections, see here for more on that topic.
Type: org.appng.api.support.SelectionFactory
Scope: singleton
4.3. Configurable Application Features
There are some built-in features an application can use by utilizing pre-defined application properties in application.xml
. See below for a list of these properties:
-
featureIndexing (boolean)
If set totrue
, the application can add documents to the site’s search index. See the chapter about indexing and searching for details. -
featureImageProcessing (boolean)
If set totrue
, the application can obtain a pre configured org.appng.tools.image.ImageProcessor. See Application#getFeatureProvider() and FeatureProvider#getImageProcessor(File sourceFile, String targetFile) for details.
-
activeProfiles (String)
A comma-separated list of active profiles, those can be used in<beans profile="…">
ofbeans.xml
.
4.4. Internationalization (I18n)
Supporting different languages is a frequent requirement for many applications. Therefore, different resource bundles can be provided in the application-home/dictionary
-folder. Any XML element that binds to a org.appng.xml.platform.Label can make use of the built in internationalization capabilities:
-
<action><config><title>
-
<action><config><description>
-
<datasource><config><title>
-
<datasource><config><description>
-
<applicationConfig><config><title>
-
<applicationConfig><config><description>
-
<page><config><title>
-
<page><config><description>
-
<event><config><title>
-
<event><config><description>
-
<link><label>
-
<link><confirmation>
-
<section><title>
-
<element><title>
-
<field><label>
-
<selection><title>
-
<optionGroup><title>
The id
of a Label
is used as the resource bundle key. It can be parametrized using the params
-attribute. This attribute allows
-
fixed terms, surrounded by single quotes
-
(
<page>
-/<action>
-/<datasource>
-) parameter expressions using the syntax${<param-name}
-
field references using the syntax
#{<field-name>}
Consider the following label:
<label id="item.delete.confirm" params="'ID', ${current.id}" />
and the corresponding entry for item.delete.confirm
from the resource bundle:
item.delete.confirm=Do you really want to delete the item with {0}: {1}?
At runtime, the <label>
would be resolved to
<label id="item.delete.confirm">Do you really want to delete the item with ID: 4711?</label>
4.4.1. The implicit i18n
variable
In order to make it possible to retrieve values from the dictionary through an expression, a variable named i18n
of type org.appng.api.support.I18n is added to the expression evaluation context. This is especially useful if different date- and number-formats should be used for different locales. Check out the description of the format-attribute here to see an example.
4.5. Using JPA
An application can easily make use of Spring Data JPA for implementing the data access layer. See the Reference Documentation for more details.
If your application want’s to use JPA, follow these simple steps:
-
Add the following dependency to your
pom.xml
(the version is inherited from the parent pom):<dependency> <groupId>org.appng</groupId> <artifactId>appng-persistence</artifactId> </dependency>
-
Annotate your persistent domain objects with the required JPA annotations:
@Entity public class Employee { .... @Id public Integer getId(){ return id; }
-
Add a
<datasource>
to your application.xml:<datasources> <datasource type="mysql" /> </datasources>
-
For the chosen database type, provide the DDL scripts:
application-home └── sql └── mysql └── V1.0.0__init_tables.sql
-
Create a org.appng.persistence.repository.SearchRepository for each of your entity classes:
public interface EmployeeRepository extends SearchRepository<Employee, Integer> { }
-
In your
beans.xml
, add the following configuration:<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:jpa="http://www.springframework.org/schema/data/jpa" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd"> <context:component-scan base-package="com.myapp" /> <jpa:repositories base-package="com.myapp.repository" base-class="org.appng.persistence.repository.SearchRepositoryImpl" /> (1) <tx:annotation-driven /> (2) <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager" /> (3) (4) <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="persistenceProviderClass" value="org.hibernate.jpa.HibernatePersistenceProvider" /> <property name="persistenceUnitName" value="myapp"/> (5) <property name="dataSource" ref="datasource" /> (6) <property name="jpaVendorAdapter"> <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" /> </property> <property name="jpaProperties"> (7) <props> <prop key="hibernate.show_sql">${hibernateShowSql:true}</prop> (8) <prop key="hibernate.format_sql">${hibernateFormatSql:false}</prop> <prop key="hibernate.id.new_generator_mappings">false</prop> </props> </property> <property name="packagesToScan"> (9) <list> <value>com.myapp.domain</value> </list> </property> </bean> <bean id="entityManager" class="org.springframework.orm.jpa.support.SharedEntityManagerBean" /> (10) </beans>
1 Enable Spring Data JPA repositories, defining the base-package
where your repositories reside. As abase-class
, useorg.appng.persistence.repository.SearchRepositoryImpl
.2 Enable annotation based transaction management (using org.springframework.transaction.annotation.Transactional). 3 Define a transaction manager. 4 Define an EntityManagerFactory
.5 Choose a meaningful name for the persistence unit. 6 The referenced bean datasource
of typejavax.sql.DataSource
is provided by the platform.7 Define the JPA properties. 8 Use some application-properties, also provide a default value. 9 Define the package(s) where your entities reside. 10 Define an javax.persistence.EntityManager
. -
Inject the repositories into your service class(es) and annotate them with
@org.springframework.transaction.annotation.Transactional
:@org.springframework.stereotype.Service @org.springframework.transaction.annotation.Transactional public class EmployeeService { private EmployeeRepository employeeRepository; @Autowired public EmployeeServiceImpl(EmployeeRepository employeeRepository) { this.employeeRepository = employeeRepository; } // transactional methods here }
For more details on declarative transaction management in Spring, see chapter 17.5 of the Spring reference documentation.
4.5.1. Working with SearchRepository
and SearchQuery<T>
Often your business logic needs to perform search queries bases on dynamic filter criteria (see Filtering for details about filters). A convenient way to handle this is to use a org.appng.persistence.repository.SearchQuery<T>, which was built exactly for this purpose.
In the following example, employees should be returned
-
whose last name contains a certain text
-
whose first name starts with a certain text
-
who are born after a certain date.
The implementation could look like this:
public Page<Employee> searchEmployees(String lastName, String firstName,
Date bornAfter, Pageable pageable) {
SearchQuery<Employee> query = employeeRepository.createSearchQuery();
query.contains("lastName", lastName);
query.startsWith("firstName", firstName);
query.greaterThan("dateOfBirth", bornAfter);
Page<Employee> employees = employeeRepository.search(query, pageable);
return employees;
}
This code is easy to read and thus quite self explaining. But wait, what if some or all of the arguments (except pageable
) are null
?
The answer is: Everything is fine and works well. The reason for that is, that all query methods of SearchQuery<T>
are null
-safe, meaning the given criteria is being ignored if the argument is null
. Anyhow, you can use isNull(String name)
if you explicitly want to check for null.
You can make use of the following criteria methods:
-
equals()
/notEquals()
-
isNull()
/isNotNull()
-
greaterThan()
/lessThan()
-
greaterEquals()
/lessEquals()
-
in()
/notIn()
-
like()
/notLike()
-
startsWith()
/endsWith()
-
contains()
In cases where using criteria methods is not sufficient, you can use SearchRepository.search(String queryString, String entityName, Pageable pageable, Object… params) and pass your custom query string to it.
4.5.2. Adding Auditing with Envers
Adding support for Hibernate Envers can be done in these steps:
-
Add
org.hibernate.envers.Audited
and other Envers annotations to your entities. -
Let repositories extend
org.appng.persistence.repository.EnversSearchRepository
. -
Use
org.appng.persistence.repository.EnversSearchRepositoryImpl
asbase-class
of<jpa:repositories>
.
You can extend the aforementioned class and override getRevisionEntity() for providing you own revision entity. -
Provide the DDL scripts for the auditing tables and place them in
application-home/sql/<type>
.
Your repository then offers theses methods, defined by org.springframework.data.repository.history.RevisionRepository
-
Revision<N,T> findLastChangeRevision(ID id)
-
Revision<N,T> findRevision(ID id, N revisionNumber)
-
Revisions<N,T> findRevisions(ID id)
-
Page<Revision<N,T>> findRevisions(ID id, Pageable pageable)
Also check those methods provided by SearchRepository
:
-
Collection<T> getHistory(ID id)
-
T getRevision(ID id, Number revision)
-
Number getRevisionNumber(ID id)
4.5.3. Using Querydsl
Adding support for Querydsl can be done in three easy steps:
-
configure QueryDSL in the
pom.xml
, check the Querydsl Reference Guide for details -
let repositories extend
org.appng.persistence.repository.QueryDslSearchRepository
-
use
org.appng.persistence.repository.QueryDslSearchRepositoryImpl
asbase-class
of<jpa:repositories>
The repository then implements org.springframework.data.querydsl.QueryDslPredicateExecutor
4.6. Custom XSL stylesheets
In every non-trivial application, you will reach the point where the standard rendering of the used template is not sufficient. At that point, you will need to write some custom XSL stylesheets.
There are several points in the source XML documents where you can place a reference to such a custom stylesheet:
-
<page><config>
-
<action><config>
-
<event><config>
-
<datasource><config>
-
<applicationRootConfig><config>
As an example, we want to display the following field of an action with a jQuery UI Slider:
<field name="rating" type="int">
<label id="rating" />
</field>
When writing custom XSLT, you need to know about the resources the used template ships with. As the standard appNG Template ships with jQuery and jQuery UI, no additional <script> -resources need to be imported.
|
The XSL stylesheet for this would look like shown below:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns="http://www.appng.org/schema/platform" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema" exclude-result-prefixes="xs">(1)
<xsl:output method="xhtml" />(2)
<xsl:template
match="action[@id='ratingAction']/config/meta-data/field[@name eq 'rating']" (3)
priority="2" mode="form"> (4)
(5)
<xsl:param name="field-htmlid" tunnel="yes" />
<xsl:param name="field-binding" tunnel="yes" />
<xsl:param name="field-value" tunnel="yes" />
<xsl:param name="field-attributes" tunnel="yes" />
<div class="fieldcontainer">(6)
<div class="label">(7)
<label class="text" for="rating"><xsl:value-of select="label/text()"/> </label>
</div>
<div class="field">(8)
<input type="text" name="{$field-binding}" id="{$field-htmlid}"
class="text" style="width:80px !important" readonly="readonly" />
<div id="r_slider" style="margin-top:5px;margin-bottom:10px;width:80px" />
</div>
<script>(9)
$(function() {
$("#r_slider").slider({
value: <xsl:value-of select="$field-value"/>, min: 0, max: 5,step: 1,
slide: function( event, ui ) {
$("#<xsl:value-of select="$field-htmlid"/>").val(ui.value);
}
});
$("#<xsl:value-of select="$field-htmlid"/>").val($("#r_slider").slider("value"));
});
</script>
</div>
</xsl:template>
</xsl:stylesheet>
1 | the root element of the stylesheet, defining the required namespaces |
2 | the output method is xhtml |
3 | the <xsl:template> uses an XPath expression to match the required field |
4 | the template applies for display-mode form |
5 | we receive some tunnel parameters from higher prioritized templates |
6 | the <div> -container for a field |
7 | the <div> -container for a field’s label |
8 | the <div> -container for the actual <input> field |
9 | the Javascript markup to build the slider |
Now, the custom stylesheet rating.xsl
needs to be referenced inside the ratingAction
:
<action id="ratingAction">
<config>
<title id="rating.create" />
<template path="recipe.xsl" /> (1)
...
</config>
...
</action>
1 | reference the template by the relative path to the application-home/xsl folder |
For further reference, have a look at the corresponding recommendations from the W3C:
4.6.1. Referencing custom resources
If your custom XSL stylesheets need to include custom resources from application-home/resources
, the schema to build the path for this resources is:
/template_<applicaton-name>/<relative-resource-path>
This example assumes that jquery.colorpicker.js
is located at application-home/resources/colorpicker
<script src="/template_myapp/colorpicker/jquery.colorpicker.js">
5. Datasources
5.1. Metadata and fields
A datasource represent either a single item or a collection of items. It needs to define the kind and structure of the returned data.
For getting started, imagine the following Java Class Employee
:
public class Employee {
private Integer id;
private String lastName;
private String firstName;
private Date dateOfBirth;
//getters and setters here
}
This class can be used by a datasource that represents a collection of items (employees, in that case):
<datasource id="employees"
xmlns="http://www.appng.org/schema/platform"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.appng.org/schema/platform
http://www.appng.org/schema/platform/appng-platform.xsd"> (1)
<config>
<title id="employees" />
<params>(2)
<param name="selectedId" />
</params>
<meta-data bindClass="com.myapp.domain.Employee" (3)
result-selector="${current.id eq selectedId}">(4)
<field name="id" type="int">(5)
<label id="id" />
</field>
<field name="lastName" type="text">
<label id="lastName" />
</field>
<field name="firstName" type="text">
<label id="firstName" />
</field>
<field name="dateOfBirth" type="date" format="yyyy-MM-dd">
<label id="dateOfBirth" />
</field>
</meta-data>
</config>
<bean id="employees">(6)
<option name="mode" value="list" />
</bean>
</datasource>
1 | Each datasource needs an unique id. |
2 | It can be parameterized using a <params> element. |
3 | The <meta-data> defines which bindclass the datasource binds to. |
4 | The keyword current acts as a loop variable for the collection. The expression given in result-selector is used to mark an element as selected. |
5 | A property of the bindclass can be referenced with the name attribute of the <field> . Any valid JavaBeans property can be used here, including nested properties. Depending on the type of the field, a format pattern can be provided. |
6 | Finally, the name of the Spring bean tells appNG which implementation to call. The <bean> can be parameterized using several <option> elements. |
Now let’s check how the implementation of the bean employees
(see number 6 from above) could look like:
import org.appng.api.ActionProvider;
import org.appng.api.DataContainer;
import org.appng.api.DataProvider;
import org.appng.api.Environment;
import org.appng.api.FieldProcessor;
import org.appng.api.Options;
import org.appng.api.Request;
import org.appng.api.model.Application;
import org.appng.api.model.Site;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Component;
import com.myapp.domain.Employee;
import com.myapp.service.EmployeeService;
@Component
@Scope("request") (1)
public class Employees implements DataProvider { (2)
private EmployeeService service;
@Autowired
public Employees(EmployeeService service) {
this.service = service;
}
(3)
public DataContainer getData(Site site, Application application, Environment environment,
Options options, Request request, FieldProcessor fp) {
DataContainer dataContainer = new DataContainer(fp);
String mode = options.getOptionValue("mode", "value");(4)
if ("list".equals(mode)) {
Page<Employee> employees = service.findEmployees(fp.getPageable());
dataContainer.setPage(employees); (5)
} else {
// do something else
}
return dataContainer; (6)
}
}
1 | Spring annotations are used to define the bean. |
2 | The interface to implement is org.appng.api.DataProvider. |
3 | The single method to implement is getData(…) . |
4 | Values of the <options> defined in the source XML can be accessed through the org.appng.api.Options parameter. |
5 | Since appNG uses the Page abstraction of Spring Data, we retrieve a Page object from our service. |
6 | A org.appng.api.DataContainer is being returned. |
This was straight forward, wasn’t it? A datasource returning a collection of items is used to display those items in tabular form and perform some sorting and filtering on this table.
Next, let’s see how to write datasource returning a single item.
The datasource:
<datasource id="employee"
xmlns="http://www.appng.org/schema/platform"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.appng.org/schema/platform
http://www.appng.org/schema/platform/appng-platform.xsd">
<config>
<title id="employee" />
<params>(1)
<param name="id" />
</params>
<meta-data bindClass="com.myapp.domain.Employee">(2)
<field name="id" type="int" readonly="true">(3)
<label id="id" />
</field>
<field name="lastName" type="text">
<label id="lastName" />
</field>
<field name="firstName" type="text">
<label id="firstName" />
</field>
<field name="dateOfBirth" type="date" format="yyyy-MM-dd">
<label id="dateOfBirth" />
</field>
</meta-data>
</config>
<bean id="employees">(4)
<option name="mode" value="single" id="${id}"/>
</bean>
</datasource>
1 | We need the id of the item. |
2 | Use the same bindclass and fields as before. |
3 | Since we don’t want the id to be changed, mark it as read-only. |
4 | We use the same Spring bean as before, passing the id parameter through an option attribute, using the syntax ${<param-name>} . |
In the implementing bean, we now just have to add the else-case:
else if ("single".equals(mode)) {
Integer id = request.convert(options.getOptionValue("mode", "id"), Integer.class);(1)
Employee employee = service.getEmployee(id);(2)
dataContainer.setItem(employee);(3)
}
1 | Convert the option to an Integer . |
2 | Retrieve the Employee from the service . |
3 | Call setItem on the datacontainer . |
A datasource returning a single item is primarily used by action to perform some operations. See Actions for more details.
5.1.1. Field attributes
-
name
(string
, required)
The name of the field. Must be a valid JavaBeans property path. -
type
(enum
, required)
The type of the field. See Field Types and display modes for details. -
binding
(string
, optional)
The name of the HTTP parameter this field binds to. Not needed in most cases. One exception is when using thebinding
-attribute of<meta-data>
, which acts as a prefix, and this prefix should not be used for this field. -
readonly
(boolean expression
, optional)
Whether or not this is a read-only field. Only relevant in display mode form.
Supports expressions using the syntax${<param-name>}
. -
hidden
(boolean expression
, optional)
Whether or not this is a hidden field.
Supports expressions using the syntax${<param-name>}
. -
format
(string expression
, optional)
Some field types do support additional formatting, see Field Types and display modes for details.
Supports expressions using the syntax${<param-name>}
. For example, the implicit i18n-variable can be used to read the date format from a resource bundle:<field name="dateOfBirth" type="date" format="${i18n.message('dateFormat')}" >
-
displayLength
(integer
, optional)
Controls after how many characters the field’s value should be truncated.
Only relevant in display mode table.
5.1.2. Conditional fields
A field may contain a <condition>
that uses all of the datasource’s parameters in it’s expression
. Additionally, the current
-variable can be used. See the section about expressions for more details.
Example, assuming a parameter action
exists:
<condition expression="${action eq `edit` and current.id gt 5}" />
5.2. Field Types and display modes
As mentioned before, a <field>
can be of different types. The type of a field determines to which Java types it can be read from/written to and how this field is being rendered by the template. But there is a second factor that affects rendering, the display mode. An appNG template must support (at least) the two display modes form and table.
When a datasource is referenced by (meaning: embedded into) an action, the display mode form is used. This means that a field renders as an HTML form element (<input>
,<select>
or <textarea>
). The field attributes hidden
and readonly
and also its <condition>
are evaluated here.
When a datasource is used "standalone" inside a page, the display mode table is used. The datasource gets rendered as a HTML <table>
, using the field labels as table header (<th>
).
A conclusion from the above is that not every field type is feasible for every display mode. Some field type do support the format
attribute, which controls how the field’s value is being formatted.
See below for a list of all field types.
5.2.1. text
Target Java Type: java.lang.String
Form Mode: <input type="text">
Table Mode: standard string representation
5.2.2. longtext
Target Java Type: java.lang.String
Form Mode: <textarea>
Table Mode: standard string representation
5.2.3. richtext
Target Java Type: java.lang.String
Form Mode: <textarea>
with richtext editing capabilities
Table Mode: standard string representation
Supported format: a comma separated list of allowed HTML tags and attributes, e.g format="p[align],div,span,a[href,target]"
(only relevant in form mode)
5.2.4. password
Target Java Type: java.lang.String
Form Mode: <input type="password">
Table Mode: standard string representation
5.2.5. url
Target Java Type: java.net.URL
Form Mode: <input type="text">
Table Mode: standard string representation
5.2.6. int
Target Java Type: java.lang.Integer
Form Mode: <input type="text">
Table Mode: formatted string representation (standard format: #
)
Supported format: a DecimalFormat pattern, e.g. format="#0.00"
,
5.2.7. long
Target Java Type: java.lang.Long
Form Mode: <input type="text">
Table Mode: formatted string representation (standard format: #
)
Supported format: a DecimalFormat pattern, e.g. format="#0.00"
5.2.8. decimal
Target Java Type: java.lang.Float
, java.lang.Double
Form Mode: <input type="text">
Table Mode: formatted string representation (standard format: #.##
)
Supported format: a DecimalFormat pattern, e.g. format="#0.00"
5.2.9. checkbox
Target Java Type: java.lang.Boolean
, boolean
(primitive)
Form Mode: <input type="checkbox" value="true">
Table Mode: standard string representation
5.2.10. coordinate
Target Java Type: org.appng.tools.locator.Coordinate
Form Mode: Depends on the template, usually a Google Map with a marker is shown. Therefore, the field needs to define two child fields latitude
and longitude
of type decimal
:
<field name="coordinate" type="coordinate">
<label>coordinate</label>
<field name="latitude" type="decimal" format="#0.0000000" >
<label id="latitude" />
</field>
<field name="longitude" type="decimal" format="#0.0000000" >
<label id="longitude" />
</field>
</field>
Table Mode: not supported
5.2.11. date
Target Java Type: java.util.Date
Form Mode: <input type="text">
with the formatted string representation. The template usually adds a user friendly date picker widget.
Table Mode: formatted string representation (standard format: yyyy-MM-dd HH:mm:ss
)
Supported format: a SimpleDateformat pattern, e.g. format="yy/MM/dd"
5.2.12. file
Target Java Type: org.appng.forms.FormUpload (form mode) or String
(table mode)
Form Mode: <input type="file">
Table Mode: standard string representation and a file icon
In table mode, the Java target type is a String , representing the name of the file. The template can then display a file icon corresponding to the file extension.
|
5.2.13. file-multiple
Target Java Type: java.util.Collection<org.appng.forms.FormUpload>
Form Mode: <input type="file" multiple="multiple">
Table Mode: not supported
5.2.14. image
Target Java Type: java.lang.String
, interpreted as the relative path to an image resource
Form Mode: <img src="…">
Table Mode: <img src="…">
5.2.15. linkpanel
Target Java Type: None, has a special meaning, see Linkpanels and Links for details.
5.2.16. List types
List types are mainly used in form mode to display checkboxes, radio buttons or selections. In order to do that, some special handling inside the DataProvider<T>
is necessary. Generally speaking, for each field of a list type, a org.appng.xml.platform.Selection must be added to the DataContainer
returned by a DataProvider
. The id of the selection must match the name of the field, so the template can make the connection between them.
Let’s make an example. In our bindclass, we have the following field:
private Collection<Integer> selectedIds = new ArrayList<Integer>();
This field is mapped in the datasource:
<field name="selectedIds" type="list:select">
<label id="selectedIds" />
</field>
We want this field to be rendered as <select multiple="multiple">
(multiple because a regular select can only have one value). What we have to do in the DataProvider
now is:
DataContainer dataContainer = new DataContainer(fieldProcessor); (1)
SelectionFactory selectionFactory = getSelectionFactory(); (2)
Integer[] all = new Integer[] { 1, 2, 3 }; (3)
DemoBean d = getDemoBean(); (4)
Collection<Integer> selected = d.getSelectedIds(); (5)
Selection selectedIds = selectionFactory
.fromObjects("selectedIds", "selectedIds", allIds, selected); (6)
selectedIds.setType(SelectionType.SELECT_MULTIPLE); (7)
dataContainer.getSelections().add(selectedIds); (8)
1 | create a DataContainer to be returned |
2 | retrieve a SelectionFactory |
3 | define the values that can be selected |
4 | retrieve the bind-object |
5 | retrieve the selected ids |
6 | call one of the helper method of the selectionFactory to build a Selection . Those helper methods always take the id of the selection as the first argument. This id must match the field’s name. |
7 | set the appropriate org.appng.xml.platform.SelectionType |
8 | add the selection to the dataContainer |
As you see,the simplest way to create a selection is using one of the helper methods of the SelectionFactory. In most real world applications, you will build selections not from simple Java types, but from your domain model.
Therefore, it is a good idea to let your domain model classes implement org.appng.api.model.Identifiable or org.appng.api.model.Named. If done so, you can use one of the fromIdentifiable(…)
or fromNamed(…)
helper methods of org.appng.api.support.SelectionFactory. Also check the fromEnum(…)
methods to be used with java.lang.Enum
.
list:checkbox
Selection Type: SelectionType.CHECKBOX
Target Java Type: java.util.Collection<[String|Integer]>
Form Mode: multiple <input type="checkbox">
Table Mode: an HTML unordered list (<ul>
) containing the collection values
list:radio
Selection Type: SelectionType.RADIO
Target Java Type: String
or Integer
Form Mode: multiple <input type="radio">
Table Mode: standard string representation
list:select
Selection Type: SelectionType.SELECT
/ SelectionType.SELECT_MULTIPLE
Target Java Type: [String|Integer]
/ java.util.Collection<[String|Integer]>
Form Mode: <select>
or <select multiple="multiple">
Table Mode: an HTML unordered list (<ul>
) containing the single value/ the collection values
5.3. The current
variable
In a datasource, the implicit variable current
is always available. It the single-item-case, current
refers to that single item. In case the datasource returns multiple items, you may consider current
as the loop variable.
5.4. Linkpanels and Links
5.4.1. Adding inline links for each item
Like shown in the above example, it is easy to write a datasource that returns a collection of items. Now imagine for each of those items, you want to provide a link to edit or delete this item.
Therefore we need to add a <field type="linkpanel">
to the datasource’s <meta-data>
. Because we can not define the linkpanel inside the field, we need to add it to the <config>
element:
<datasource id="employees"
xmlns="http://www.appng.org/schema/platform"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.appng.org/schema/platform
http://www.appng.org/schema/platform/appng-platform.xsd">
<config>
<title id="employees" />
<params>
<param name="selectedId" />
</params>
<meta-data bindClass="com.myapp.domain.Employee"
result-selector="${current.id eq selectedId}">
<field name="id" type="int">
<label id="id" />
</field>
<field name="lastName" type="text">
<label id="lastName" />
</field>
<field name="firstName" type="text">
<label id="firstName" />
</field>
<field name="dateOfBirth" type="date" format="yyyy-MM-dd">
<label id="dateOfBirth" />
</field>
<field name="actions" type="linkpanel"> (1)
<label id="actions" />
</field>
</meta-data>
<linkpanel location="inline" id="actions"> (2)
<link target="/employees/update/${current.id}" mode="intern"> (3)
<label id="employee.update" /> (4)
<icon>edit</icon> (5)
</link>
<link target="/employees?delAction=delete&delId=${current.id}" mode="intern">(6)
<label id="employee.delete" />
<icon>delete</icon>
<confirmation id="employee.delete.confirm"
params="${current.firstName},${current.lastName}" />(7)
</link>
</linkpanel>
</config>
<bean id="employees">
<option name="mode" value="list" />
</bean>
</datasource>
1 | We define a field of type linkpanel . |
2 | Next, a linkpanel whose id matches the name of the linkpanel field, must be defined. |
3 | The link to edit an item uses mode="intern" , because it points to the page "employees". The page is responsible for including the action for editing the item. See Actions and Pages for more information on that topics. |
4 | A link has a label, that is used for a mouseover tooltip. |
5 | A link can use from a set of predefined icons that each appNG Template must provide. See List of icons for a list of predefined icons. |
6 | The link to delete the item uses some GET-parameters. Please note that for the question mark (?) and the ampersand (&), the corresponding Numeric character references ? and & must be used in order to not break the XML document during XSLT transformation. |
7 | Because we want to avoid accidentally deleting an item, we add a parametrized <confirmation> . The entry in the application’s dictionary would look like this: employee.delete.confirm=Delete {0} {1}? |
5.4.2. Adding links for the whole datasource
Finally, a link to create a new item should be provided by the datasource. This is done by adding a linkpanel with location top
, bottom
or both
. No field of type linkpanel must be added to the meta-data!
<datasource id="employees">
<config>
...
<meta-data>
...
</meta-data>
<linkpanel location="both" id="other">
<link target="/employees/create" mode="intern">
<label id="employee.create" />
<icon>new</icon>
</link>
</linkpanel>
</config>
...
</datasource>
The link generated this way is shown above/under/above and under the resulting table, depending on the chosen location
(one of top
, bottom
, both
).
5.4.3. Links to webservices
If you want to provide a link to a webservice, use the following approach:
<link mode="webservice" target="downloadService?id=${current.downloadId}"> (1)
<label id="download" />
<icon>download</icon>
</link>
1 | Use the webservice -mode for the link. The target starts with the name of the bean implementing the webservice, followed by the parameters required by that service. |
More about webservices can be found in this chapter.
5.5. Field references
As an alternative to the current
-variable, there’s the field reference syntax. As a prefix, it uses the number/hash-sign (#
) instead of the dollar sign ($
). With this syntax, you can reference field-values by their name.
Field references can be used
-
inside inline-links
-
in the
target
-attribute -
as a
<label>
-parameter -
as a
<confirmation>
-parameter
-
-
as a
<title>
-parameter of an<action>
, referencing a single-item datasource
This said, the datasource employees could also define it’s links like shown below:
<link target="/employees/update/#{id}" mode="intern">
<label id="employee.update" params="#{firstName}, #{lastName}"/>
<icon>edit</icon>
</link>
<link target="/employees?delAction=delete&delId=#{id}" mode="intern">
<label id="employee.delete" params="#{firstName}, #{lastName}"/>
<icon>delete</icon>
<confirmation id="employee.delete.confirm" params="#{firstName}, #{lastName}" />
</link>
Then, the resourcebundle would look like
employee.update = Edit {0} {1}
employee.delete = Delete {0} {1}
employee.delete.confirm = Really delete {0} {1}?
5.6. Paging, Sorting and Filtering
5.6.1. Paging and Sorting
Sorting is a first class citizen of the appNG application platform. If you use the org.springframework.data.domain.Page abstraction of Spring Data JPA together with repository methods that take a org.springframework.data.domain.Pageable object as an argument, it is easy to provide those features.
For paging, nothing needs to be changed in the source XML of the datasources. Just call fieldProcessor.getPageable() and use the returned Pageable
as a method argument for a repository search method. Within the page, you can use the pageSize
-attribute of the <datasource>
to change the default page size of 25 for that datasource.
If you can not/do not want to provide paging and sorting by implementing it inside your business logic, you can use DataContainer.setPage(Collection<?> items, Pageable pageable) and let the framework handle it. You can even disable paging and sorting by using DataContainer.setItems(Collection<?> items) |
For (multi-column) sorting, just add the <sort />
-element to the <field>
you want to sort by, and that’s it!. For each field that is sortable, the sort icons will be displayed in the table header for that field.
The <sort/>
element offers the following attributes:
-
prio
(integer
)
The priority for the default-sort, less means higher. Start with 0 and increase by 1. -
order
([ASC
|DESC
]) -
order
([ASC
|DESC
])
The direction for the default-sort -
name
(string
)
The name of the property to be used in a JPA search query. Only necessary if different from the field’s name. -
ignore-case
(boolean
)
Whether or not the search should be case insensitive or not.
If you want to define a default sort order for a datasource, just use the prio
and order attributes
.
In the following example, we define a default sorting by last name and first name, both descending and ignoring case:
<field name="lastName" type="text">
<sort order="DESC" prio="0" ignore-case="true" />
<field>
<field name="firstName" type="text">
<sort order="DESC" prio="1" ignore-case="true" />
<field>
5.6.2. Filtering
If your application is dealing with large sets of data, the ability to filter this data is a common requirement. That’s why appNG offers the concept of filters. A filter is a SelectionGroup
that is added to a DataContainer
. To that SelectionGroup
, you add your filter criteria in form of a Selection
, obtained from a SelectionFactory
. See the section about building selections for details.
Filtering is done by rendering a HTML <form>
containing the filter criteria. That form submits its data with the HTTP GET
-method, which leads to bookmarkable filter-criteria.
Although there are many different ways how to implement the filter functionalities in detail, there has evolved some kind of best practices for this.
If your datasource only requires a few filter criteria, it is feasible to manually read them from the request as shown below:
import org.apache.commons.lang3.StringUtils;
...
public DataContainer getData(...) {
DataContainer dataContainer = new DataContainer(); (1)
SelectionGroup group = new SelectionGroup();
dataContainer.getSelectionGroups().add(group);
String filterByName = request.getParameter("nameFilter"); (2)
Selection nameFilter =
selectionFactory.fromObjects("nameFilter", "label.nameFilter",
new String[] { StringUtils.trimToEmpty(filterByName) });
nameFilter.setType(SelectionType.TEXT);
group.getSelections().add(nameFilter);(3)
Page<Employee> employees =
service.searchEmployees(name, fieldProcessor.getPageable());
dataContainer.setPage(employees);
return dataContainer; (4)
}
1 | create a DataContainer and add a SelectionGroup (read: filter) |
2 | manually read the value of the filter criteria from the request |
3 | use a SelectionFactory to create a Selection , respecting the current value taken from the request |
4 | retrieve the data and return it |
Advanced Filtering Techniques
In most cases where filtering is required, more than one filter criteria is available for the user. Since we do not want to manually read the string values from the request and if necessary convert them to the right type, another approach can be taken.
-
Define and implement a
DataProvider
that wraps your filter criteria@Component @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) (1) public class Filter implements DataProvider { private String nameFilter; (2) private Integer idFilter; public DataContainer getData(Site site, Application application, Environment environment, Options options, Request request, FieldProcessor fp) { try { request.fillBindObject(this, fp, request, site.getSiteClassLoader()); (3) DataContainer dataContainer = new DataContainer(fp); dataContainer.setItem(this); return dataContainer; (4) } catch (BusinessException e) { throw new ApplicationException("error filling filter", e); } } // getters and setters here }
1 The filter needs to be request-scoped 2 define the filter criteria 3 do a request.fillBindObject(…)
with theFilter
itself4 return a DataContainer
Note the proxyMode
for the filter is set toScopedProxyMode.TARGET_CLASS
. This is necessary if you want to inject the filter into a bean with a longer-lived scope. Because a "regular"DataProvider
uses singleton scope, we need the make the filter a scoped proxy. Check out the Spring Reference Documentation for more details about scoped proxies.The corresponding source XML definition:
<datasource id="filter" xmlns="http://www.appng.org/schema/platform" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.appng.org/schema/platform http://www.appng.org/schema/platform/appng-platform.xsd"> <config> <meta-data bindClass="com.myapp.business.Filter"> <field name="nameFilter" type="text"> <label id=nameFilter" /> </field> <field name="idFilter" type="int"> <label id="idFilter" /> </field> </meta-data> </config> <bean id="filter" /> </datasource>
-
Include this datasource in your page
The important thing here is to include the filter datasource before the datasource you want to filter. Because there is no need to display this datasource, we mark the<section>
ashidden
.<section hidden="true"> <element> <datasource id="filter" /> </element> </section>
-
In the datasource to be filtered, inject the filter and build your selections from it
private SelectionFactory selectionFactory; private Filter filter; @Autowired public DemoBeans(SelectionFactory selectionFactory, Filter filter) { this.selectionFactory = selectionFactory; this.filter = filter; } public DataContainer getData(...) { DataContainer dataContainer = new DataContainer(fp); SelectionGroup group = new SelectionGroup(); // build selections with a SelectionFactory and add them to the group // group.getSelections().add(selection); dataContainer.getSelectionGroups().add(group); }
5.7. Datasource inheritance
A datasource can inherit from another datasource. This can be defined by a special datasource id notation where the id of the inherited datasource is appended with two double colon after the datasource id <datasource id="child::parent">
.
If a datasource id contains the inheritance separator "::"
, appNG clones the parent datasource definition and uses the first part of the id as id for the final datasource. Other elements of the datasource are added or overwritten:
-
title
If the child defines another title than the parent datasource, the title gets overwritten. Otherwise the title of the parent datasource is used. -
parameter
The list of parameters are copied from the inherited datasource. Parameters, defined in the child datasource, are added to the existing list of parameters. If the child has a parameter with the same name as a parameter from parent datasource, the parameter gets overwritten by the child datasource definition. -
bind class
The bind class definition is mandatory anyhow. The child can define another bind class than the parent datasource. This is possible, if the bind class provides the same attributes referenced in the datasource fields, conditions and links as the original bind class of the parent datasource. -
fields
The list of fields are copied from the parent datasource. New fields will be prepended to the list of existing fields. If the parent datasource has a linkpanel-field, the new fields will be prepended to the linkpanel-field. Fields with the same name will overwrite existing fields which have been copied from the parent datasource. -
bean id
The complete bean definition is copied from the parent datasource. If the child defines another bean id than the parent datasource, the bean id gets overwritten. -
bean options
The list of bean options are copied from the parent datasource. Bean options in the child datasource are added to the existing list. If the child datasource has an bean option with the same option name, the inherited option gets overwritten. Bean options are also inherited if the bean id is different. -
linkpanels
The list of linkpanels is copied from the parent datasource. If the child provides additional linkpanels, the panels are added to the list from the parent datasource.If the child provides a link panel with the same id as an existing link panel, the linkpanel from parent datasource and all its links gets overwritten by the linkpanel definition of the child datasource.
-
config permissions
Permissions are copied from the parent datasource. If the child datasource defines it’s own permissions, those from the parent datasource get overwritten. -
config labels
The list of labels are copied from the parent datasource. New labels provided by the child are added. If the child datasource provides a label with the same id that an inherited label, the inherited label gets overwritten overwritten.
A datasource definition which has only an id-attribute and no other elements is not valid. Though it is necessary to add at least the mandatory elements. The simplest possible example would be:
<datasource id="employees2::employees" (1)
xmlns="http://www.appng.org/schema/platform"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.appng.org/schema/platform
http://www.appng.org/schema/platform/appng-platform.xsd">
<config>(2)
<meta-data bindClass="com.myapp.domain.Employee" /> (3)
</config>
<bean /> (4)
</datasource>
1 | datasource employees2 inherits datasource employees |
2 | a datasource must have a config node |
3 | a config node must have a meta-data node with bindClass attribute. It uses the same bind class as the parent datasource |
4 | a datasource must have a bean node |
The datasource can be referenced with id employees2
in pages and actions. It is an exact clone of the datasource employees
just with another id. This new datasource will not provide any additional elements or features compared to the parent datasource.
Even this simple clone is useful. It makes it possible to place the same datasource with different parameters on the same page because of the different ids. |
An example for a more complex inheritance would look like this:
<datasource id="teamleads::employees"
xmlns="http://www.appng.org/schema/platform"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.appng.org/schema/platform
http://www.appng.org/schema/platform/appng-platform.xsd">
<config>
<title id="teamleads" /> (1)
<meta-data bindClass="com.myapp.domain.TeamLead" > (2)
<field name="teamSize" type="int"> (3)
<label id="teamSize"/>
</field>
</meta-data>
<linkpanel location="inline" id="actions"> (4)
<link target="/teamlead/showteam/${current.id}" mode="intern">
<label id="teamlead.team" />
<icon>user</icon>
</link>
</linkpanel>
</config>
<bean>
<option name="kind" value="teamlead" /> (5)
</bean>
</datasource>
1 | The datasource gets a new title. |
2 | The bind class is now TeamLead which has to provide all referenced attributes from the parent datasource. It also has to provide all attributes referenced in this child datasource |
3 | The datasource gets an additional field showing the size of the team |
4 | The linkpanel with id="actions" of the datasource employee gets overwritten. It will now only provide one link to show the teamlead’s team members. All inherited links has been removed with the inherited linkpanel. If the other links are required, they must be duplicated in the child definition. |
5 | This adds an option to the inherited bean option list. |
After appNG processed the inheritance, the final datasource will have the following structure:
<datasource id="teamleads"
xmlns="http://www.appng.org/schema/platform"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.appng.org/schema/platform
http://www.appng.org/schema/platform/appng-platform.xsd">
<config>
<title id="teamleads" />
<params>
<param name="selectedId" />
</params>
<meta-data bindClass="com.myapp.domain.TeamLeads"
result-selector="${current.id eq selectedId}">
<field name="id" type="int">
<label id="id" />
</field>
<field name="lastName" type="text">
<label id="lastName" />
</field>
<field name="firstName" type="text">
<label id="firstName" />
</field>
<field name="dateOfBirth" type="date" format="yyyy-MM-dd">
<label id="dateOfBirth" />
</field>
<field name="teamSize" type="int">
<label id="teamSize"/>
</field>
<field name="actions" type="linkpanel">
<label id="actions" />
</field>
</meta-data>
<linkpanel location="inline" id="actions">
<link target="/teamlead/showteam/${current.id}" mode="intern">
<label id="teamlead.team" />
<icon>user</icon>
</link>
</linkpanel>
</config>
<bean id="employees">
<option name="mode" value="list" />
<option name="kind" value="teamlead" />
</bean>
</datasource>
Because the bean id is not overwritten by the child datasource, the called bean for this datasource is still the bean employees
. An example enhancement for the class Employees
using the additional option kind
would be:
// (...)
@Component
@Scope("request")
public class Employees implements DataProvider {
//(...)
public DataContainer getData(Site site, Application application, Environment environment, Options options, Request request, FieldProcessor fp) {
DataContainer dataContainer = new DataContainer(fp);
String mode = options.getOptionValue("mode", "value");
String kind = options.getOptionValue("kind", "value"); (1)
if ("list".equals(mode)) {
if("teamlead".equals(kind){
Page<TeamLead> teamLeads = service.findTeamLeads(fp.getPageable()); (2)
dataContainer.setPage(teamLeads);
} else {
Page<Employee> employees = service.findEmployees(fp.getPageable());
dataContainer.setPage(employees);
}
} else {
// do something else
}
return dataContainer;
}
// (...)
1 | Retrieve the option kind . If the bean has been called for datasource employees , the variable will be null. If it has been called for datasource teamleads the value will be "teamlead" . |
2 | Get the team leads instead of all employees and put them in the data container. |
The FieldProcessor provides the datasource name as String with method fp.getReference() . This could also be used instead adding the option kind in the example above.
|
It would also be possible to define a new DataProvider bean (TeamLeads) to provide data for this datasource instead enhancing the existing bean Employees .
|
5.7.1. Multiple inheritance
Multiple inheritance is not supported. A datasource can have only one parent. But it is possible to build an inheritance hierarchy:
<datasource id="employees2::employees" (1)
xmlns="http://www.appng.org/schema/platform"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.appng.org/schema/platform
http://www.appng.org/schema/platform/appng-platform.xsd">
<config>
<meta-data bindClass="com.myapp.domain.Employee" />
</config>
<bean />
</datasource>
<datasource id="employees3::employees2 (2)
xmlns="http://www.appng.org/schema/platform"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.appng.org/schema/platform
http://www.appng.org/schema/platform/appng-platform.xsd">
<config>
<meta-data bindClass="com.myapp.domain.Employee" />
</config>
<bean />
</datasource>
1 | employees2 inherits employees |
2 | employees3 inherits employees2 (which inherited employees) |
AppNG will first process the inheritance of employees
into employees2
with all rules defined above. After that it will process the inheritance of employees2
into employees3
with the same rules no matter that this datasource already inherited another datasource.
5.8. Datasources as a service
It is possible to retrieve a single datasource with a special service URL. The schema for such an URL is
http(s)://<host>[:<port>]/service/<site-name>/<application-name>/datasource/<format>/<datasource-id>
The supported formats are xml and json.
Examples:
As you can see in the second example, you can pass parameters defined by the datasource as GET-parameters.
Datasources that should be available through a service URL must be secured by a permission. If no permission is present, the access will be denied. Anonymous permissions might be used. |
5.8.1. Defining paging and sorting
When used as a service, paging and sorting capabilities can also be used by a specially named GET-Parameter.
As an example, let’s take the datasource employees
as defined here.
To sort this datasource by lastname ascending and by firstname descending and to retrieve the second page with a page size of 25, the URL would be:
Let’s further analyse the query-part of the URL.
It contains a single GET-Parameter that defines the above mentioned criteria for paging and sorting. The syntax is <criterion>:<value>
, where multiple criteria are separated with a semicolon (;
).
sortEmployees= (1)
page:1; (2)
pageSize:25; (3)
firstName:asc; (4)
lastName:desc (5)
1 | The name of the parameter is sort plus the capitalized id of the datasource. |
2 | Since appNG uses zero-indexed pages, the number of the second page is 1 |
3 | Define the size of the page. |
4 | Sort by firstname, ascending. |
5 | Sort by lastName, descending. |
Since appNG supports multi-field-sorting, the order of the fields within the sort-parameter is essential. |
6. Actions
While a datasource represents the structure/state of the application’s data, actions are there to manipulate the state of that data. In most cases, an action will be rendered as an HTML <form>
with various <input>
-fields.
Let’s see how a typical action looks like:
<event id="employeeEvent"
xmlns="http://www.appng.org/schema/platform"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.appng.org/schema/platform
http://www.appng.org/schema/platform/appng-platform.xsd"> (1)
<config></config>
<action id="update"> (2)
<config>
<title id="employee.update" /> (3)
<params> (4)
<param name="id" />
<param name="form_action" />
</params>
</config>
<condition expression="${'update' eq form_action and not empty id}" /> (5)
<datasource id="employee"> (6)
<params>
<param name="id">${id}</param>
</params>
</datasource>
<bean id="employees"> (7)
<option name="mode" value="update" id="${id}" />
</bean>
</action>
</event>
1 | Actions are always grouped by an <event> . That makes it easy to collect all actions that perform on the same domain object within on event. The id of the event must be globally unique. |
2 | Within the event, each action must have a unique id. |
3 | An action must define a title that is used as a headline when rendered as HTML. |
4 | An action can be parametrized using a <params> element. |
5 | An action can define an execute condition. Only if the condition matches, the action will be executed. |
6 | An action can make use of a datasource and pass its parameters to that datasource. |
7 | Just like for datasources, the name of the Spring bean implementing the action must be given. Also, any number of options can be passed to that implementation. |
As you see, we use the datasource employee defined before. Thus the type of the data received by the action is an Employee
.
Next, the implementation:
import org.appng.api.ActionProvider;
import org.appng.api.Environment;
import org.appng.api.FieldProcessor;
import org.appng.api.Options;
import org.appng.api.Request;
import org.appng.api.model.Application;
import org.appng.api.model.Site;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import com.myapp.domain.Employee;
import com.myapp.service.EmployeeService;
@Component
@Scope("request") (1)
public class Employees implements ActionProvider<Employee> { (2)
private EmployeeService service;
@Autowired
public Employees(EmployeeService service) {
this.service = service;
}
(3)
public void perform(Site site, Application application,
Environment environment, Options options, Request request,
Employee formBean, FieldProcessor fieldProcessor) {
(4)
String mode = options.getOptionValue("mode", "value");
Integer id = request.convert(options.getOptionValue("mode", "id"), Integer.class);
if ("update".equals(mode)) {
(5)
service.updateEmployee(formBean, request, fieldProcessor);
} else {
...
}
}
}
1 | Spring annotations are used to define the bean. |
2 | The interface to be implemented is org.appng.api.ActionProvider<T>. Because the datasource uses the bindclass Employee , we use this as type parameter. |
3 | The single method to implement is perform(…) , which takes a parameter formBean of the required type Employee . |
4 | We retrieve the mode and the id from the options. |
5 | We call the service to update the employee. |
As you can see, there is no need for manually binding request parameters to the formBean
, appNG is doing that job for you. Now, you might ask how the implementation of service.updateEmployee(…)
might look like and why it needs the request
and fieldProcessor
arguments. Good catch!
In web applications, we always face the problem that the data received from the user has to be merged with the real, probably persistent data. This problem even gets worse as the user most times is only allowed to edit certain properties of the data. So what to do? Merge each property manually? This is not the appNG way.
But this is:
public void updateEmployee(Employee formBean, Request request, FieldProcessor fp) {
Employee current = getEmployee(formBean.getId()); (1)
request.setPropertyValues(formBean, current, fp.getMetaData()); (2)
String message =
request.getMessage("employee.updated", e.getFirstName(), e.getLastName()); (3)
fp.addOkMessage(message);
}
1 | We retrieve the current data. |
2 | For each non-readonly (aka writable) field of the FieldProcessor , the field value is written from the formBean to the current data. |
3 | Retrieve a parametrized message from the resourcebundle and add it to the FieldProcessor . |
How does this work? Well, the FieldProcessor
contains the org.appng.xml.platform.MetaData with all the org.appng.xml.platform.FieldDefinitions defined in the datasource employee. Because of this, the request
can easily write those fields to the target object, including possible necessarry type conversions.
6.1. Validation
Validation is a first class citizen in the appNG application framework. Therefore, the Bean Validation API 2.0 as specified in JSR-349 is used. Just add some validation annotations to the POJO bindclass that you datasource uses. As long as the formBean
of an ActionProvider
is not valid, the action will not perform and the error messages will be shown.
Example:
@NotNull(message = ValidationMessages.VALIDATION_NOT_NULL) (1)
@Size(min = 3, message = "{firstname.toShort}") (2)
public String getFirstName() {
return firstName;
}
...
@FileUpload(fileTypes = "jpg,png", maxSize = 10, unit = Unit.MB) (3)
public org.appng.forms.FormUpload getUpload() {
return upload;
}
@Valid
public Salary getSalary() { (4)
return salary;
}
1 | A predefined message from org.appng.api.ValidationMessages can be used |
2 | Alternatively, a custom validation message from the application’s resourcebundle can be used. Don’t forget the curly braces! |
3 | A org.appng.forms.FormUpload can be validated using @FileUpload . |
4 | Nested properties can be validated using @Valid . |
You can use any Constraint in your application, may it be the standard constraints from the javax.validation.constraints
-package, those from Hibernate Validator (shipped with appNG), or any custom and/or compound constraints.
6.1.1. Client side validation
An appNG template ships with support for client-side validation. Because it can not know all possible constraints you are using in your action’s bindclass, only these standard constraints are eligile for client-site validation:
If your action’s bindclass uses some additional constraints, it should be considered to set clientValidation="false" for your <action> . Otherwise, the client-validation would pass, whereas the server-side validation fails. This could be confusing to the user.
|
6.1.2. Programmatic validation
By implementing org.appng.api.FormValidator in your ActionProvider
/ DataProvider
or in your bind-object, validation can be done programmatically.
Example:
In this example, a credit card number is validated inside the bind-object BankAccount
using Apache Commons Validator's CreditCardValidator
.
import org.apache.commons.validator.routines.CreditCardValidator;
import org.appng.api.Environment;
import org.appng.api.FieldProcessor;
import org.appng.api.FormValidator;
import org.appng.api.Options;
import org.appng.api.Request;
import org.appng.api.model.Application;
import org.appng.api.model.Site;
import org.appng.xml.platform.FieldDef;
public class BankAccount implements FormValidator { (1)
private String creditCardNumber;
(2)
public void validate(Site site, Application application,
Environment environment, Options options,
Request request, FieldProcessor fieldProcessor) {
(3)
if (!new CreditCardValidator().isValid(creditCardNumber)) {
FieldDef creditCardNumber = fieldProcessor.getField("creditCardNumber"); (4)
fieldProcessor.addErrorMessage(creditCardNumber, "Invalid credit card number!"); (5)
}
}
// getters and setters here
}
1 | Implement org.appng.api.FormValidator . |
2 | The single method to be implemented is validate(…) . |
3 | Check if the credit card number is valid. |
4 | If not, get the FieldDef for creditCardNumber from the FieldProcessor by it’s binding . |
5 | Add an error-message for the field. |
You can also use the FieldProcessor
inside your ActionProvider
/ DataProvider
to add validation error messages to a field or to the whole action/datasource respectively. In case of an action, make sure not to process any further and just return
from the action.
Example:
public SaveBankAccount implements ActionProvider<BankAccount> {
public void perform(Site site, Application application,
Environment environment, Options options, Request request,
BankAccount formBean, FieldProcessor fieldProcessor) {
(1)
if (!new CreditCardValidator().isValid(formBean.getCreditCardNumber())) {
FieldDef creditCardNumber = fieldProcessor.getField("creditCardNumber"); (2)
fieldProcessor.addErrorMessage(creditCardNumber, "Invalid credit card number!");
fieldProcessor.addErrorMessage("Your input contains errors!"); (3)
return; (4)
}
// continue
}
}
1 | Check if the credit card number is valid. |
2 | If not, add the error-message to the creditCardNumber field. |
3 | Add a global error message. |
4 | Stop processing and return. |
6.1.3. Using validation groups
The Bean Validation API supports the concept of validation groups, as can bee seen in section 5.1.3 that specification.
AppNG does support validation groups by adding those to the <meta-data>
of an application’s datasource. Usually, an interface defined inside the class to be validated is used as a validation group.
Example:
<meta-data bindclass="com.myapp.domain.Employee">
<validation>(1)
<group class="javax.validation.groups.Default" /> (2)
<group class="com.myapp.domain.Employee$AddressFields"
condition="${current.active}" /> (3)
</validation>
<!-- list of <field> elements follows here -->
</meta-data>
1 | Add a <validation> -element to the <meta-data> to define groups. |
2 | Adds the Default group. |
3 | Adds a custom validation group. If this is an inner interface, the dollar sign ($ ) needs to be used to specify the fully qualified class name for the group. The condition tells the framework only to use this group if the (boolean) property active of current Employee is true . |
6.2. Actions as a service
It is possible to retrieve and perform a single action with a special service URL. The schema for such an URL is
http(s)://<host>[:<port>]/service/<site-name>/<application-name>/action/<format>/<event-id>/<action-id>
The supported formats are xml and json.
Examples:
In order to execute the action, the client of the webservice must send an HTTP-POST request containing all the mandatory fields plus the action parameters that are required to satisfy the execute-<condition>
of the action.
Actions that should be available through a service URL must be secured by a permission. If no permission is present, the access will be denied. Anonymous permissions might be used. |
7. Pages
7.1. <page>
and <pages>
A <page>
is a single view presented to the user. On a page you can compose several Actions and Datasources within any number of sections and section elements.
An example of a page is shown below:
<page id="employees"
xmlns="http://www.appng.org/schema/platform"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.appng.org/schema/platform
http://www.appng.org/schema/platform/appng-platform.xsd"> (1)
<config>
<title id="page.employees" />
<url-schema> (2)
<url-params>
<url-param name="action" />
<url-param name="id" />
</url-params>
<get-params>
<get-param name="delAction" />
<get-param name="delId" />
</get-params>
<post-params> (3)
<post-param name="form_action" />
</post-params>
</url-schema>
</config>
<structure>
<section>
<element>
<action id="create" eventId="employeeEvent"> (4)
<condition expression="${action eq 'create'}" />
<params>
<param name="form_action">${form_action}</param>
</params>
</action>
</element>
<element>
<action id="update" eventId="employeeEvent"> (5)
<condition expression="${action eq 'update' and not empty id}" />
<params>
<param name="id">${id}</param>
<param name="form_action">${form_action}</param>
</params>
</action>
</element>
<element>
<datasource id="employees"> (6)
<params>
<param name="selectedId">${id}</param>
</params>
</datasource>
</element>
</section>
<section hidden="true"> (7)
<element>
<action id="delete" eventId="employeeEvent" onSuccess="/employees"> (8)
<params>
<param name="action">${delAction}</param>
<param name="id">${delId}</param>
</params>
</action>
</element>
</section>
</structure>
</page>
1 | Each page must have a unique id. |
2 | A page must define die URL schema. |
3 | Since a page can not 'know' every possible POST parameter only the built-in parameter form_action needs to be set here. |
4 | The create action is included, but only if ${action eq 'create'} . |
5 | The update action is included, but only if ${action eq 'update' and not empty id} . |
6 | Always include the 'employees' datasource, passing the id as a parameter. |
7 | Include a hidden section with the 'delete'-action, passing the get-parameters delId and delAction . |
8 | After a successful delete action, the user gets redirected to the page employees . This is done to cleanup the URL from the GET-parameters used for deletion. |
7.2. <url-schema>
As shown in the example above, the current URL in conjunction with the URL-schema of the page is responsible for what action and datasources are being shown on that page.
As you already might know, an appNG application URL has the following form:
http(s)://<host>[:<port>]/<manager-prefix>/<site>/<application>/<page>/<url-param-1-to-n>?<get-params>
Example:
So with this URL, the runtime-representation of the URL-schema is:
<url-schema>
<url-params>
<url-param name="action">update</url-param>
<url-param name="itemid">7</url-param>
</url-params>
<get-params>
<get-param name="foo" />
</get-params>
<post-params>
<post-param name="form_action" />
</post-params>
</url-schema>
When rendering the page, appNG first checks the include conditions for every action and datasource. In the next step, every action whose execute-condition matches is beeing executed. This includes the execution of the datasources referenced by those actions. Finally, the datasources included on the page are processed.
7.3. <applicationRootConfig>
and <navigation>
There is a single place where some general configuration for all pages must be done. There must be one source XML file containing an <applicationRootConfig>
. The only exception for that rule is, if the application does not offer a GUI at all, but only offers some other services. See Implementing services for an overview about what services this could be.
Anyhow, if you build an application with a GUI, you need to provide an <applicationRootConfig>
. A typical exemplar of that looks like this:
<applicationRootConfig
xmlns="http://www.appng.org/schema/platform"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.appng.org/schema/platform
http://www.appng.org/schema/platform/appng-platform.xsd">
<name>My Cool App</name>(1)
<config>
<session>
<session-params>(2)
<session-param name="param1" />
<session-param name="param2" />
</session-params>
</session>
</config>
<navigation location="top" id="topnav">(3)
<link mode="intern" target="/employees">
<label id="employees" />
</link>
<link mode="intern" target="/settings">
<label id="settings" />
</link>
</navigation>
</applicationRootConfig>
1 | provide a name, just for informational purposes |
2 | define which post-/get-/url-parameters should be persisted in the user’s HTTP session |
3 | provide the top level navigation for the application, linking to certain pages |
Note that each page page-parameter -may it be a GET-, POST- or URL-parameter- that is defined as a session-parameter, will be saved into the session and does not change until a different value is assigned or the session expires. If a page defines a parameter that it not contained in the current request, but there is a session parameter with that name, the value from the session will be taken.
A common use-case for session parameters is using them for filter criteria of a datasource. See here for more about filtering.
With great power comes great responsibility! Using session parameters is a powerful feature. Decide carefully which parameters should get stored in the session. |
8. Expressions
AppNG uses the JSP Expression language syntax to provide support for dynamic expressions. See here for a list of operators.
Expressions can be used to
-
pass page-parameters to an
<action>
/<datasource>
-
pass
<action>
-parameters to a<datasource>
-
pass
<action>
/<datasource>
-parameters to the attributes of<option>
elements -
control the
readonly
-,hidden
- andformat
-attribute of a<field>
-
write the
expression
for a<condition>
of a<field>
,<action>
,<datasource>
,<link>
-
set the
passive
- andfolded
-attributes of a section<element>
-
set the
hidden
-attribute of a<section>
-
set the
async
-attribute of an<action>
-
set the
default
andactive
-attribute of a<link>
-
write a
condition
for an<icon>
-
pass a parameter to the
params
-attribute of a<label>
,<title>
,<description>
,<confirmation>
9. Permissions
As described here, an application can define the available permissions it ships with in application-home/application.xml
. Those permissions can be referenced at many places inside the source XML documents.
Those places are:
-
the
<config>
-element of a(n)-
<page>
-
<action>
-
<datasource>
-
<event>
-
-
a
<link>
/<linkpanel>
-
an
<output-format>
and<output-type>
-
a
<field>
When referencing a permission at one of those places, the mode
-attribute needs to be set.
There are two different modes:
-
set
Determines if the current subject has the required permission, and applies it to the defining parent element. This means, if the permission is not present, the parent element will not be contained in the resulting target XML.
This is the mode to be used in 99.99% percent of cases. |
-
read
Reads, but does not apply, the required permission from the current subject. This means, even if the permission is missing, the parent element will be contained in the target XML.
This mode can be used if some Custom XSL stylesheets should get applied, depending on the permissions of the subject.
9.1. Anonymous permissions
There is a special kind of permissions to allow anonymous access, using the prefix anonymous
. Every user, regardless whether he’s logged on or not, owns these permissions. Permissions that use the prefix anonymous
must not be defined in application.xml
.
Anonymous permissions are especially useful when providing access to actions and datasources as a service. See Actions as a service and Datasources as a service for more details on these topics.
9.2. Field permissions
For a <field>
using permissions, it can be differentiated between reading and writing the field. Here, the <permissions>
-element allows an additional mode
-attribute, with the possible values read
and write
.
The following example shows how to use this mode:
<field name="foobar" type="text">
<label id="foobar" />
<permissions mode="read">
<permission ref="foobar.read" mode="set" />
</permissions>
<permissions mode="write">
<permission ref="foobar.write" mode="set" />
</permissions>
</field>
9.3. Programmatically checking permissions
In cases where the referencing permissions in the static XML sources is not sufficient, it is also possible to check them programmatically.
Therefore, you can retrieve a org.appng.api.PermissionProcessor from a org.appng.api.Request. Then, use the hasPermission(String reference)
-method to check for a certain permission:
if (request.getPermissionProcessor().hasPermission("doSomething")) {
// do something
}
10. JSP Tags
AppNG provides a number of tags to be used in JavaServer Pages. Those can be used if an appNG site is used to serve JSP based content. Usually, this content originates from a content management sytem.
In order to use the appNG JSP tags, a @taglib
directive must be added to the page:
<%@taglib uri="http://appng.org/tags" prefix="appNG"%>
Within JSP files, you can make use of the available implicit objects like |
10.1. <appNG:taglet>
This is probably the most common way of embedding dynamic contents provided by an appNG application into a JSP page. First, implement one of the following interfaces in your application and declare the implementing class as a Spring bean:
Example:
<appNG:taglet application="myapp" method="myTaglet">(1)
<appNG:param name="foo">bar</appNG:param>(2)
<appNG:param name="jin">fizzbar</appNG:param>
</appNG:taglet>
1 | Calls the taglet, method refers to the Spring bean name. |
2 | Adds some parameters that are being passed as a java.util.Map<String,String> to the processTaglet() -method of the respective implementation. |
Note that the content-type delivered by a taglet is variable. It can be XML, JSON, HTML or even plain text. See the Javadoc of the interfaces mentioned above for more details about the different types of taglets.
10.2. Form tags
With the form tags it it possible to build HTML forms, validate them on the server side and pass the user input to a org.appng.api.FormProcessProvider.
Example:
<appNG:form>(1)
<appNG:formData mode="not_submitted">(2)
<form action="" method="post" enctype="multipart/form-data" >(3)
(4)
<appNG:formElement rule="email"
mandatory="true"
mandatoryMessage="E-mail is mandatory!"
errorClass="error"
errorMessage="Not a valid e-mail!"
errorElementId="emailError">
<input type="text" name="email" />(5)
<div id="emailError">
</appNG:formElement>
<input type="submit" />
</form>
</appNG:formData>
<appNG:formConfirmation application="appng-webutils" method="debugProvider" mode="submitted">(6)
<appNG:param name="foo">bar</appNG:param>(7)
<appNG:param name="jin">fizz</appNG:param>
<p>Thank you for your message!<p>
</appNG:formConfirmation>
</appNG:form>
1 | Start with <appNG:form> . |
2 | Next, define the content of the form using <appNG:formData> . The mode controls whether to form is always shown (always ) or only as long it hasn’t been submitted (not_submitted ). |
3 | A regular HTML <form> needs to be defined. |
4 | Adds a <appNG:formElement> for the user’s email address (attributes explained below). |
5 | Nest the regular HTML <input> -tag inside <appNG:formElement> . |
6 | The <appNG:formConfirmation> defines which org.appng.api.FormProcessProvider to call. The method -attributes names the respective Spring bean. The mode controls whether the content of this tag should always be shown (always ) or only if the form has been submitted (submitted ). |
7 | Adds some parameters that are being passed as a java.util.Map<String,Object> to the process() -method of the respective FormProcessProvider implementation. |
The FormProcessProvider is only being called if all mandatory fields have a valid value and all rules are satisfied.
|
The <appNG:formElement>
is used as a wrapper around standard HTML form input fields, which are
-
<input>
(types:text
,radio
,password
,hidden
,file
,checkbox
) -
<textarea>
-
<select>
with nested<option>
Attributes: (all optional)
-
mandatory
- set to true if the field is mandatory -
mandatoryMessage
- the message to be displayed when no value has been entered for a mandatory field -
errorMessage
- the error message to be displayed when validation fails -
errorClass
- the CSS class to add to the input field when validation fails -
errorElementId
- the id of an element to append a <span> with the error message -
rule
- a validation rule for the input field -
desc
- a description for the input field
Rules:
Here’s a list of the possible values for the rule
attribute.
Name | Description | Example |
---|---|---|
|
only word characters ([a-zA-Z_0-9] allowed) |
|
|
must be a valid email address |
|
|
must be equal to another field or value |
|
|
must match the given regular expression |
|
|
must be a number |
|
|
must be a number with up to x digits, and y fractional digits |
|
|
must have an exact length of x |
|
|
must have a minimum length of x |
|
|
must have a maximum length of x |
|
|
must have a minimum length of x and a maximum length of y |
|
|
must have one of the comma-separated types |
|
|
must have a minimum size of x MB/KB |
|
|
must have a maximum size of x MB/KB |
|
|
must have a size between x and y MB/KB |
|
|
between x and y files must have been selected |
|
|
at least x files must have been selected |
|
|
at most x files must have been selected |
|
|
Must match a captcha value. The result of the captcha is stored in the variable |
|
Make sure you also check the Javadoc of the form tags:
10.2.1. <appNG:formGroup>
A HTML <select>
and a group of radio buttons (<input type="radio">
) must be wrapped by an <appNG:formGroup>
.
Example for a <select>
:
<appNG:formGroup name="subject" (1)
mandatory="true" mandatoryMessage="This field is mandatory!" (2)
errorClass="error" errorElementId="subject_error"> (3)
<select name="subject"> (4)
<appNG:formElement> (5)
<option value="">Please select</option>
</appNG:formElement>
<appNG:formElement>
<option value="A">Option A</option>
</appNG:formElement>
<appNG:formElement>
<option value="B">Option B</option>
</appNG:formElement>
<appNG:formElement><option value="C">Option C</option>
</appNG:formElement>
</select>
<div id="subject_error"></div> (6)
</appNG:formGroup>
1 | Defines the name of the group. |
2 | Make the field mandatory and provide a message. |
3 | Define the class and the id of the element holding the error message. |
4 | Use the group’s name also as name for the <select> . |
5 | Add a <appNG:formelement> for each option. |
6 | The element holding the error message, if any. |
A <appNG:formGroup> is rendered as a <div> , which -in case of an error- also receives the errorClass .
|
Example for a radio group:
<appNG:formGroup name="gender"
mandatory="true" mandatoryMessage="This field is mandatory!"
errorClass="invalid" errorElementId="gender_error">
<appNG:formElement>
<input type="radio" value="M" name="gender"><label>Male</label>
</appNG:formElement>
<br/>
<appNG:formElement>
<input type="radio" value="F" name="gender"><label>Female</label>
</appNG:formElement>
</select>
<div id="gender_error" class="error"></div>
</appNG:formGroup>
10.3. Search tags
These tags provide functionality to make use of the indexing and searching-features of appNG. It must contain at least one <appNG:searchPart>
tag.
See the chapter about Indexing and Searching for details on this topic.
10.3.1. <appNG:search>
This tag is used to retrieve the search results in a certain format. The 'json'-format is useful if some Javascript renders the results, while 'xml' can be used to apply a XSL stylesheet to the results.
Attributes: (defaults in braces)
-
format
('json')- one ofxml
orjson
-
parts
(false
) - whether the resulting XML/JSON should be split in parts -
highlight
(span
) - the x(ht)ml-tag used to highlight the search term within the search results.
Parameters: (defaults in braces)
-
pageSize
(25
)
the page size to use -
pageSizeParam
(pageSize
)
the name of the request parameter that contains the page-size -
pageParam
(page
)
the name of the request parameter that contains the current page -
queryParam
(q
)
the name of the request parameter that contains the search term -
maxTextLength
(150
)
the maximum length of a search result text -
dateFormat
(yyyy-MM-dd
)
the date pattern used to format dates -
fillWith
(…
)
the placeholder used when the search result text is being stripped -
xsl
the path to the XSLT stylesheet to use when format is XML -
pretty
(false
)
if the XML/JSON output should be formatted prettily
Example:
<appNG:search parts="false" format="json" highlight="span">(1)
<appNG:param name="queryParam">term</appNG:param>
<appNG:param name="pageSize">10</appNG:param>
<appNG:param name="pageParam">p</appNG:param>(2)
(3)
<appNG:searchPart
application="global"
language="de"
title="Search Results"
fields="title,contents" analyzerClass="org.apache.lucene.analysis.de.GermanAnalyzer"/>
</appNG:search>
1 | use json format |
2 | set some parameters by overriding their default |
3 | add a <appng:searchPart> with application="global" , meaning instead of calling specific application, the results from the standard global search should be used |
Predefined fields
The following field are predefined by appNG and should not be misused by putting different kind of information into them:
-
path
- then path to the document, relative to the site’s domain -
title
- the title of the document -
teaser
- a short intro text for the document -
image
- an image for the document -
date
- the date of the last change, using the patternyyyy-MM-dd HH:mm:ss
-
type
- the type of the document -
language
- the language of the document -
contents
- the textual content of the document -
id
- the Id of the document
10.3.2. <appNG:searchPart>
As shown above, an <appNG:search>
can contain different <appNG:seachPart>
-elements.
An <appNG:seachPart>
refers to an implementation of org.appng.search.SearchProvider.
Attributes:
-
application
- the application that provides theSearchProvider
-
method
- the name of the Spring bean implementingSearchProvider
-
language
- the language of the documents to find -
title
- the title for this part -
fields
- a comma-separated list of the document’s field to search in -
analyzerClass
- the class implementing org.apache.lucene.analysis.Analyzer, used when performing the search
Parameters:
Any parameter recognized by the SearchProvider
.
Example:
<appNG:searchPart
application="acme-products"
method="productSearchProvider"
language="en"
title="ACME Products"
fields="title,contents"
analyzerClass="org.apache.lucene.analysis.en.EnglishAnalyzer">
<appNG:param name="foo">bar</appNG:param>
<appNG:param name="jin">fizz</appNG:param>
</appNG:searchPart>
10.3.3. <appNG:searchable>
This tag is used to mark certain areas of a JSP page as searchable, meaning those parts should be added as a document to the site’s search index.
Attributes:
-
index
- whether the body content should be indexed -
visible
(true
) - whether or not the body content should be displayed -
field
- the name of the field. May be one of the standard fields (title
,teaser
,image
,contents
) or a user-defined one.
For JSPs, the fields id , date , type , language and path are set by appNG and thus must not be set with <appNG:searchable> .
|
Example:
(1)
<appNG:searchable index="true" field="title" visible="false">
The Hitchhiker's Guide to the Galaxy
</appNG:searchable>
(2)
<appNG:searchable index="true" field="contents">
The Hitchhiker's Guide to the Galaxy is a comic science fiction series created by Douglas Adams.
(3)
<appNG:searchable index="false">
This text is being ignored.
</appNG:searchable>
The title is the name of a fictional, eccentric, electronic travel guide, The Hitchhiker's Guide to the Galaxy, prominently featured in the series.
</appNG:searchable>
(4)
<appNG:searchable index="true" field="customfield" visible="false">
Hitchhiker
</appNG:searchable>
1 | define an invisible field for the title |
2 | define the contents -field (multiple occurences are allowed!) |
3 | tags can be nested, if some areas should be excluded (use index="false" in those cases) |
4 | adds an invisible field named customfield |
Excluding a page from being indexed
The <appNG:searchable>
tag can also be used the exclude a page from being indexed. Therefore, use the field indexPage
and set it’s value to false
. No other attributes are required.
<appNG:searchable field="indexPage">false</appNG:searchable>
As an alternative, you can use a surrounding <appNG:searchable index="false">
tag.
<appNG:searchable index="false">
<appNG:searchable index="true" field="title">Title</appNG:searchable>
<appNG:searchable index="true" field="contents">Contents</appNG:searchable>
</appNG:searchable>
10.4. Other tags
10.4.1. <appNG:param>
The <appng:param>
tag can be used to add parameters to a
-
<appNG:taglet>
-
<appNG:search>
-
<appNG:searchPart>
-
<appNG:formConfirmation>
-
<appNG:application>
Attributes:
-
name
- the name of the parameter -
unescape
- if set totrue
, HTML entities in the value are unescaped before passed to the owning tag
For the parameter’s value, you can reference to request parameters using the syntax #[<param>]
.
Example:
<appNG:param name="replyTo">#[email]</appNG:param>
10.4.2. <appNG:attribute>
With this tag, you can read/ write attributes of different Scopes from/ to the current environment.
Attributes:
-
scope
- the scope of the attribute (REQUEST
,SESSION
,PLATFORM
orURL
) -
mode
- the mode (read
orwrite
) -
name
- the name of the attribute -
value
- write mode only: the value to write
Example:
<appNG:attribute scope="SESSION" mode="read" name="foo" />
<appNG:attribute scope="REQUEST" mode="read" name="bar" />
<appNG:attribute scope="SESSION" mode="write" name="someName" value="someValue"/>
<!--
For URL-scope, the name is the zero based index of the path segment
(segments are separated by '/'). For example, if the path is
'/en/foo/bar/42' then you can access the '42' with index 3
-->
<appNG:attribute scope="URL" mode="read" name="3" />
Only the REQUEST and SESSION scope allow the write mode.
|
10.4.3. <appNG:if>
This tag is used to display the tag body only if the given condition matches. In the condition, any request parameter can be used. Expressions are supported, but no leading ${
and closing }
is required.
Attributes:
-
condition
- the condition to be satisfied
Example:
<!--assumed there's a request parameter named 'foo' -->
<appNG:if condition="foo eq 'bar'">Foobar!</appNG:if>
10.4.4. <appNG:permission>
If you want to make sure a that certain content is only visible if the logged-in user has a certain permission, use the <appNG:permission>
tag.
Attributes:
-
application
- the name of the application that provides the permission -
permission
- the name of the permission
Example:
<appNG:permission application="myapp" permission="showSecretContent">
This is secret content!
</appNG:permission>
10.5. <appNG:application>
Used to embed an appNG application inside a JSP page. This is achieved by transforming the <platform>
XML document returned by the application with a custom XSL stylesheet. Thus it is possible to adjust the appearance of the application to any required design.
Attributes:
-
application
- the name of the application that should be embedded.
Parameters: (<appNG:param>
)
-
defaultBaseUrl
The url of the page where the application is embedded -
defaultPage
The default page of the application, used when the url parameters do not contain a page name -
xslStyleSheet
The path to the XSLT stylesheet used for transformation, relative to the site’s repository folder. If omitted, the plain XML is written as an HTML comment. -
requestAttribute
The name of an environment-attribute with the scopeREQUEST
where the transformation result should be stored in. If this parameter is not set, the result is directly written to the page.
GET-Parameters:
-
xsl
- iffalse
, the plain XML is written as an HTML comment
Example:
The following example assumes you want to embed the application acme-app
into the page /en/acme
.
Every path segment after /en/acme
is passed as an url-parameter to the application.
<appNG:application name="acme-app">
<appNG:param name="defaultBaseUrl">/en/acme</appNG:param>
<appNG:param name="defaultPage">/index/welcome</appNG:param>
<appNG:param name="xslStyleSheet">/meta/xsl/acme/platform.xsl</appNG:param>
<appNG:param name="requestAttribute">acmeResult</appNG:param>
</appNG:application>
<!-- later in JSP -->
<appNG:attribute mode="read" name="acmeResult" scope="REQUEST" />
11. Indexing and Searching
Each appNG site owns its own document based index. Documents are added to/ retrieved from the index in the form of an org.appng.api.search.Document. Each site has its own indexing thread, that is waiting for org.appng.api.search.DocumentProducers to offer some org.appng.api.search.DocumentEvents
11.1. Adding documents at runtime
An application can provide a org.appng.api.search.DocumentProducer
with these steps:
-
Retrieve a org.appng.api.model.FeatureProvider by calling Application.getFeatureProvider()
-
Call FeatureProvider.getIndexer() to obtain a
Consumer< DocumentEvent, DocumentProducer>
-
Add a DocumentProducer to this
Consumer<DocumentEvent,DocumentProducer>
-
Provide org.appng.api.search.Documents by calling
DocumentProducer.put(DocumentEvent e)
Example:
import org.appng.api.search.Consumer;
import org.appng.api.search.Document;
import org.appng.api.search.DocumentEvent;
import org.appng.api.search.DocumentProducer;
...
Consumer<DocumentEvent, DocumentProducer> indexer =
application.getFeatureProvider().getIndexer();(1)
DocumentProducer documentProducer =
new DocumentProducer(EnglishAnalyzer.class, "myapp-indexer");(2)
indexer.put(documentProducer);(3)
Document docAdded = ... // (4)
documentProducer.put(new DocumentEvent(docAdded, Document.CREATE)); (5)
1 | retrieve the indexer |
2 | create a new DocumentProducer |
3 | add the producer to the indexer |
4 | create a Document , you may extend org.appng.search.indexer.SimpleDocument here |
5 | add a new DocumentEvent to the producer |
11.2. Adding Documents at the time of indexing
If the appNG site has the appNG scheduler application enabled (which will be the case in most scenarios) there’s the org.appng.search.DocumentProvider-interface that can be implemented by the application to provide an java.lang.Iterable
of org.appng.api.search.DocumentProducers.
The appNG scheduler application contains an indexing job that automatically detects all the Spring beans implementing DocumentProvider
.
import org.appng.api.search.Document;
import org.appng.api.search.DocumentEvent;
import org.appng.api.search.DocumentProducer;
import org.appng.search.DocumentProvider;
@org.springframework.stereotype.Component (1)
public class MyDocumentProvider implements DocumentProvider {
public Iterable<DocumentProducer>
getDocumentProducers(Site site, Application application) { (2)
DocumentProducer documentProducer =
new DocumentProducer(EnglishAnalyzer.class, "myapp-indexer"); (3)
for(/* loop over some domain objects */) {
Document docAdded = ...; // build document
documentProducer.put(new DocumentEvent(docAdded, Document.CREATE)); (4)
}
return java.util.Arrays.asList(documentProducer);
}
}
1 | MyDocumentProvider is a @Component , so the indexing job can detect it. |
2 | The single method to implement is getDocumentProducers(Site site, Application application) . |
3 | Create a DocumentProducer by providing the Analyzer to use and a name. |
4 | Since the indexing-job is completely rebuilding the index, we use the event type Document.CREATE. |
11.3. Adding documents through <appNG:searchable>
The <appNG:searchable>
JSP-tag can be used to add documents to the index when a site’s JSP repository is being indexed by the appNG scheduler application. Details about the <appNG:searchable>
-tag can be found in the section about search tags.
11.4. Adding documents at the time of searching
Instead of providing documents at indexing time, an application can also provide documents right a the time of searching. This can be achieved by implementing org.appng.search.SearchProvider.
The SearchProvider
can then be referenced by a <appNG:searchPart>
-tag. See the section about search tags for details.
12. Testing
12.1. General
AppNG also offers support for unit- and integration testing your appNG applications. Therefore, it uses the testing capabilities of the Spring framework. See the Reference Documentation for details on testing with Spring.
To enable test support, just add the following dependency to your pom.xml
:
<dependency>
<groupId>org.appng</groupId>
<artifactId>appng-testsupport</artifactId>
<version>${appNG.version}</version>
</dependency>
Next, use org.appng.testsupport.TestBase as a base class for your unit tests. You can test with or without JPA support enabled.
Without JPA support:
@org.springframework.test.context.ContextConfiguration(initializers = MyTest.class)
public class EmployeesTest extends TestBase {
public MyTest() {
super("myapp", "application-home");
}
}
With JPA support:
@org.springframework.test.context.ContextConfiguration(
locations = { TestBase.TESTCONTEXT_JPA }, initializers = MyTest.class)
public class EmployeesTest extends TestBase {
public MyTest() {
super("myapp", "application-home");
setEntityPackage("com.myapp.domain");
setRepositoryBase("com.myapp.repository");
}
}
12.2. Testing a datasource:
@org.junit.Test
public void testShowEmployees() throws ProcessingException, IOException {
addParameter("selectedId", "1"); (1)
initParameters(); (2)
DataSourceCall dataSourceCall = getDataSource("employees");(3)
CallableDataSource callableDataSource = dataSourceCall.getCallableDataSource(); (4)
callableDataSource.perform("aPage"); (5)
validate(callableDataSource.getDatasource());(6)
}
1 | adds a parameter |
2 | initialize the parameters, must be called after parameters have been added |
3 | retrieve a DataSourceCall representing the datasource by its id |
4 | get a CallableDataSource |
5 | perform the CallableDataSource |
6 | validate the response |
In step 6, a org.appng.testsupport.validation.WritingXmlValidator is used to compare the contents of a control file with the XML that results from marshalling the given object (in this case a org.appng.xml.platform.Datasource
). The control file must be located at src/test/resources/xml
. It’s name is derived from the name of the test class and the name of the test method. So in this example, it would be EmployeesTest-testShowEmployees.xml
.
For initially creating and later updating your control files, just set WritingXmlValidator.writeXml = true and the control files will be written to src/test/resources/xml .
|
12.3. Testing an action:
@org.junit.Test
public void testCreateEmployee() throws ProcessingException, IOException {
ActionCall action = getAction("employeeEvent", "create");(1)
action.withParam(FORM_ACTION, "create");(2)
Employee formBean = new Employee("John", "Doe");(3)
CallableAction callableAction = action.getCallableAction(formBean);(4)
FieldProcessor fieldProcessor = callableAction.perform();(5)
validate(fieldProcessor.getMessages(), "-messages");(6)
validate(callableAction.getAction(), "-action");(7)
}
1 | retrieve an ActionCall representing the action by its event-id and id |
2 | add required parameters to the action |
3 | create a form bean |
4 | retrieve a CallableAction |
5 | perform the CallableAction receive a FieldProcessor |
6 | use a validate(…) -method that takes a suffix as a parameter, validating the messages of the fieldProcessor |
7 | use the same validate method to validte the contents of the action |
Although you pass the formBean as a whole to ActionCall.getCallableAction(formBean) , this formBean is being copied internally. This copy, which is passed to the ActionProvider<T> , will contain only those properties that are mapped and writable in the datasource used by the action.
|
12.4. Adding custom bean definitions for testing
You can add custom bean definitons for your tests. For example, if you want to run a SQL script to initialize your test database, you could provide a file beans-test-xml
located at src/test/resources
.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<jdbc:initialize-database enabled="true" data-source="datasource">(1)
<jdbc:script location="classpath:/sql/init-db.sql" />(2)
</jdbc:initialize-database>
</beans>
1 | use <jdbc:initialize-database> and make a reference to the built in datasource |
2 | set the classpath location for the DDL script |
In your testcase, you just need to add beans-test.xml
to the @ContextConfiguration
-annotation
@ContextConfiguration(locations = { TestBase.TESTCONTEXT_JPA, "classpath:/beans-test.xml" }
13. Implementing services
13.1. Webservices
A webservice is the suitable solution if your application needs to provide
-
images, probably scaled and otherwise edited using an ImageProcessor
-
file attachments (aka downloads)
-
custom formatted data as JSON, HTML, XML or any other required format
The interface to implement is org.appng.api.Webservice or org.appng.api.AttachmentWebservice.
Example:
@org.springframework.stereotype.Service
public class ImageService implements Webservice {
public byte[] processRequest(Site site, Application application,
Environment environment, Request request) throws BusinessException {
Integer id = request.convert(request.getParameter("id"), Integer.class);
Image image = getImage(id)
return image.getData(id);
}
public String getContentType() {
return "image/jpg";
}
}
The URL schema for a webservice is
http(s)://<host>[:<port>]/service/<site-name>/<application-name>/webservice/<service-name>
13.2. SOAP services
The interface org.appng.api.SoapService is used to implement a SOAP compliant service based on Spring WS. For more information on Spring WS, see the Reference Documentation and this Getting Started Guide.
The following steps are required to build a SOAP service:
-
Define a XSD schema
Because Spring WS uses the contract-first approach, it is essential to have an XSD schema defining the structure of the request and response elements. The schema file should be placed in
src/main/xsd
.A minimalist XSD looks like this:
<?xml version="1.0" encoding="UTF-8"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://example.com/soap">(1) <xs:element name="exampleMethodRequest">(2) <xs:complexType> <xs:sequence> <xs:element name="requestParam" type="xs:string" /> </xs:sequence> </xs:complexType> </xs:element> <xs:element name="exampleMethodResponse">(2) <xs:complexType> <xs:sequence> <xs:element name="responseParam" type="xs:string" /> </xs:sequence> </xs:complexType> </xs:element> </xs:schema>
1 define the namespace for the schema 2 define the request and response elements, following the naming convention xyzRequest
andxyzResponse
-
Generate JAXB classes for the schema file
The jaxb2-maven-plugin can be used to generate the Java classes using the xjc binding compiler on this schema. The build-helper-maven-plugin is then used to add those classes to the project sources.
Example configuration from
pom.xml
:<build> <resources> <resource> <directory>src/main/xsd</directory>(1) </resource> <resource> <directory>src/main/resources</directory> </resource> </resources> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>jaxb2-maven-plugin</artifactId> <version>2.2</version> <executions> <execution> <id>generate</id> <goals> <goal>xjc</goal> </goals> <phase>generate-sources</phase> <configuration> <packageName>com.example.soap</packageName>(2) </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <version>1.12</version> <executions> <execution> <phase>generate-sources</phase> <goals> <goal>add-source</goal> </goals> <configuration> <sources> <source>target/generated-sources/jaxb</source>(3) </sources> </configuration> </execution> </executions> </plugin> </plugins> </build>
1 add src/main/xsd
to the project’sresources
2 generate JAXB classes in the package com.example.soap
3 add the folder with the generated JAXB classes as a source directory -
Implement the
SoapService
import org.appng.api.Environment; import org.appng.api.SoapService; import org.appng.api.model.Application; import org.appng.api.model.Site; import org.springframework.ws.server.endpoint.annotation.Endpoint; import org.springframework.ws.server.endpoint.annotation.PayloadRoot; import org.springframework.ws.server.endpoint.annotation.RequestPayload; import org.springframework.ws.server.endpoint.annotation.ResponsePayload; import com.example.soap.ExampleMethodRequest; import com.example.soap.ExampleMethodResponse; @Endpoint (1) public class ExampleSoapService implements SoapService { (2) private Site site; private Application application; private Environment environment; (3) @PayloadRoot(namespace = "http://example.com/soap", localPart = "exampleMethodRequest") (4) public @ResponsePayload ExampleMethodResponse getExample( @RequestPayload ExampleMethodRequest request) { ExampleMethodResponse exampleMethodResponse = new ExampleMethodResponse(); exampleMethodResponse.setResponseParam(request.getRequestParam()); return exampleMethodResponse; } (5) public String getSchemaLocation() { return "example.xsd"; } (6) public String getContextPath() { return "com.example.soap"; } // getters and setters here }
1 annotate with @org.springframework.ws.server.endpoint.annotation.Endpoint
2 implement org.appng.api.SoapService
3 define a @PayloadRoot
that uses thenamespace
of the XSD schema and defining thelocalPart
4 the method must use the a request-object as an @RequestPayload
argument and response-object as an@ResponsePayload
return type5 return the classpath-relative location of the schema file 6 return the package name of the generated JAXB classes
The URL schema for the WSDL of the service is
http(s)://<host>[:<port>]/service/<site-name>/<application-name>/soap/<service-name>/<service-name>.wsdl
13.3. REST services
With appNG, you can easily create REST-based services by using the Spring framework’s designated annotations. The most important ones to use here are @RestController, @RequestMapping and @PathVariable.
Let’s implement the add-operation of a simple CalculatorService
as a @RestController
.
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController (1)
public class CalculatorService {
@RequestMapping(value = "/add/{a}/{b}", (2)
method = RequestMethod.GET, (3)
produces = MediaType.TEXT_PLAIN_VALUE) (4)
public ResponseEntity<Integer> add( (5)
@PathVariable("a") Integer a, @PathVariable("b") Integer b) { (6)
return new ResponseEntity<Integer>(a + b, HttpStatus.OK); (7)
}
}
1 | Define the class ass @RestController |
2 | Define a @RequestMapping and the path it should match, using path variables |
3 | Define the HTTP method |
4 | Define the media-type this method produces |
5 | Return a ResponseEntity of the desired type |
6 | Map the path variables to the parameters |
7 | Return the response |
In order to make the necessary conversion of the ResponseEntity
work, there needs to be an appropriate HttpMessageConverter present in your application.
For the example above, this converter looks like this:
@Service
public class IntegerMessageConverter implements HttpMessageConverter<Integer> {
public List<MediaType> getSupportedMediaTypes() {
return Arrays.asList(MediaType.TEXT_PLAIN);
}
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return Integer.class.equals(clazz);
}
public void write(Integer t, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
outputMessage.getBody().write(String.valueOf(t).getBytes());
}
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return false;
}
public Integer read(Class<? extends Integer> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
return null;
}
}
If your request-/response-types are JAXB-generated classes, you have to use a MarshallingHttpMessageConverter instead. For JSON-format, there’s a MappingJackson2HttpMessageConverter available. |
The URL schema for the REST service is
http(s)://<host>[:<port>]/service/<site-name>/<application-name>/rest/path/to/service
For more details on this topic, see section 22.3.3 of Spring’s reference documentation.
13.4. Job scheduling
A org.appng.api.ScheduledJob is a (periodically or manually triggered) task that can be defined by an application. The implementation is simple:
import java.util.Map;
import org.appng.api.ScheduledJob;
import org.appng.api.model.Application;
import org.appng.api.model.Site;
public class DemoJob implements ScheduledJob {
private String description;
private Map<String, Object> jobDataMap;
public void execute(Site site, Application application) throws Exception {
// do something
}
// getters and setters here
}
With the jobDataMap
, the required configuration parameters can be passed to the job. There are some predefined parameters for a job:
-
cronExpression
A cron-expression describing when the job should be executed. The Cronmaker is a useful tool helping you to build those expressions. -
enabled
If set totrue
and thecronExpression
property is properly set, the job will automatically be scheduled when the appNG platform starts. -
runOnce
If set totrue
, this job will only run once per appNG cluster, i.e. it is not executed on each node, which is the default behavior. -
hardInterruptable
If set totrue
, this job can safely be interrupted, e.g when aSite
is being reloaded. This is achieved by running the job in a separate thread and calling Thread.interrupt.
The best way to configure a job is in beans.xml
:
<bean id="demoJob" class="com.myapp.job.Demojob">
<property name="jobDataMap">
<map>
<entry key="enabled" value="true" />
<entry key="runOnce" value="true" />
<entry key="cronExpression" value="0 0 8 ? * *" />(1)
<entry key="foo" value="bar" />(2)
<entry key="answer" value="42" />
</map>
</property>
</bean>
1 | add the standard predefined parameters |
2 | add some custom parameters |
15. Beautifying URLs
When serving JSPs or static content from a site’s repository folder, it is often a requirement to rewrite the URLs for some of those contents.
For example, a JSP located at /en/an-ugly-url
should be available at /a-beauty-url
.
To solve this problem, appNG uses UrlRewriteFilter.
For configuration, the file urlrewrite.xml
is used, usually located at /meta/conf/urlrewrite.xml
of a site’s root directory.
The configuration file |
This file needs to contain a forward
rule and a redirect
rule for each URL to be rewritten.
The urlrewrite.xml
for the above example would look like this:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE urlrewrite PUBLIC "-//tuckey.org//DTD UrlRewrite 4.0//EN" "http://www.tuckey.org/res/dtds/urlrewrite4.0.dtd">
<urlrewrite>
<rule> (1)
<from>/en/an-ugly-url</from>
<to type="redirect" last="true">/a-beauty-url</to>
</rule>
<rule> (2)
<from>/a-beauty-url</from>
<to type="forward" last="true">/en/an-ugly-url</to>
</rule>
</urlrewrite>
1 | Define a redirect rule <from> the ugly URL <to> the rewritten one. |
2 | Define a forward rule <from> the rewritten URL <to> the ugly one. |
The |
Note that the order of the rules (from the same type) is essential.
Also make sure to use |
The forward
rule is needed for internally forwarding the rewritten URL to the real resource. The user will not take notice of this.
The redirect
rule is needed for actually redirecting the user (a.k.a. browser) from the ugly URL to the desired rewritten URL.
Therefore, HTTP status 302
(found) will be used. If HTTP status 301
(moved permanently) is needed, use type="permanent-redirect"
.
Additionally, the redirect
rule is used for replacing textual matches of the ugly URL in other JSPs with their rewritten value.
For example, if a JSP contains a <a href="/en/an-ugly-url">Beautiful!</a>
, the redirect
rule is being applied here,
resulting in <a href="/a-beauty-url">Beautiful!</a>
.
The feature of replacing textual matches of the ugly URL with the rewritten one only applies to |
15.1. Dealing with file extensions and GET parameters
If the rewritten resource is a JSP and/or if it must support HTTP GET parameters, the rules need to be enhanced.
Take a look at this example:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE urlrewrite PUBLIC "-//tuckey.org//DTD UrlRewrite 4.0//EN" "http://www.tuckey.org/res/dtds/urlrewrite4.0.dtd">
<urlrewrite use-query-string="true"> (1)
<rule>
<from>/en/an-ugly-url(\.jsp)?((\?\S+)?)</from> (2)
<to type="redirect" last="true">/a-beauty-url$2</to> (3)
</rule>
<rule>
<from>/a-beauty-url((\?\S+)?)</from> (4)
<to type="forward" last="true">/en/an-ugly-url$1</to> (5)
</rule>
</urlrewrite>
1 | We need the query string to be part of the <from> element, thus we need to set use-query-string="true" . |
2 | The ugly URL may contain the extension .jsp , therefore we add a capturing group (\.jsp)? . Also it may contain GET parameters,
e.g. ?foo=bar&jin=fizz . For this case, add a capturing group ((\?\S+)?) . |
3 | The GET parameters need to be kept, so append them using $<number-of-capturing-group> |
4 | The rewritten URL also may contain GET parameters. so capture them with ((\?\S+)?) . |
5 | Again, append the GET parameters by referring to the capturing group. |
As you can see, the rewrite rules do intensely use regular expressions. Therefore, understanding those is essential. A good website for testing your expressions and getting additional explanations is regexr.com. |
16. The appNG Maven Archetype
An appNG application uses Apache Maven as a build system, thus the fastest way to setup an appNG application is to use the appNG Maven Archetype as shown below:
mvn archetype:generate -DgroupId=mygroupid -DartifactId=myartifactid -DarchetypeGroupId=org.appng -DarchetypeArtifactId=appng-archetype-application -DarchetypeVersion=1.15.0 -DinteractiveMode=false