- Using SJS
- Authoring SJS source files
- Examples
- Key concepts
- SJS source files organization
Using SJS
An SJS application description is a set of statements that are used to describe and assemble Jspresso components the same way you would do it using Spring XML. The big difference is the expression language itself.
Authoring SJS source files
General syntax
An SJS statement is made of 4 parts :
- The name of the statement, e.g. :
- The identifier of the underlying component, e.g. :
or even simpler without parenthesis :Entity('Employee')
Entity 'Employee'
- Arguments that configure the underlying component properties, e.g. :
or even simpler without parenthesis :Entity('Employee', icon:'employee-48x48.png')
Entity 'Employee', icon:'employee-48x48.png'
- Nested SJS statements between curly brackets (a "closure"). In that case, parenthesis cannot be omitted, e.g.
Or even with deeper nesting :Entity('Employee', icon:'employee-48x48.png')
Statement nesting is a key feature for SJS flexibility.split_vertical('Departments.and.teams.view', top:'Company-departments.table', cascadingModels:true) { bottom { split_horizontal (left:'Department-teams.table', right:'Team-teamMembers.table', cascadingModels:true) } }
Only statement name is always mandatory. Other parts may be required or not and SJS will enforce these rules.Some statements dont have any parameter. To summarize, every SJS statement conforms to the following scheme :
name('identifier', parameters) {closure}
where closure is a list of SJS statements. This means that there can be an arbitrary nesting of SJS statements.
Arguments types
The following types are used for SJS argument values :
- String :
'value'
- Boolean :
[true|false]
- Integer :
12
- Decimal :
12.34
- List :
['value1','value2']
- Map:
[key1:'value1', key2:'value2']
SJS will enforce argument types. Under certain circumstances, expected argument types are references themselves, in which case, SJS will check their validity. Used reference types can be :
- Domain elements, i.e. entities, components or interfaces.
- Domain element properties, i.e. fields.
- Views (form, table, split, ...).
- Actions and Action maps.
- Generic arbitrary beans for advanced usage.
Arguments syntax
Arguments are defined using the following scheme :
argumentName:argumentValue
For instance :
icon:'traceable-48x48.png'
Whenever multiple arguments need to be declared, they are coma separated, e.g. :
icon:'traceable-48x48.png', preferredWidth:45
An argument value can be a complex type, e.g. a list :
uncloned:['createTimstamp','lasUpdateTimestamp']
or a map (associative array), e.g. :
ordering:['name':'ASCENDING','firstName':'DESCENDING']
Simplified forms
Language artifacts are sometimes optional. Omitting them can improve the reading.
Parenthesis in statements
Whenever a statement does not contain a closure, parenthesis can be omitted, e.g. :
Entity 'Employee', icon:'employee-48x48.png'
Whenever the closure is present, the identifier and arguments section must be surrounded by parentheses, e.g. :
Entity('Employee', icon:'employee-48x48.png') {
string_32 'name'
string_32 'firstName'
}
Square backets in lists
Whenever a list contains more than 1 element, it has to be surrounded by square backets, e.g. :
Entity 'Employee', uncloned:['name','firstName']
Whenever a list contains only 1 element, square backets can be omitted, e.g. :
Entity 'Employee', uncloned:'name'
Properties composed in statement names
Some SJS statements allow for a property to be composed in their names in order to improve code readability :
string_n
andtext_n
wheren
is the maximum length of the string or text property, e.g.string_32
ottext_1024
.split_vertical
andsplit_horizontal
where vertical and horizontal are the allowed values for the split container orientation.
View model inference
In order to simplify the code to write, a view model can be inferred from the view name. By convention, a view identifier should begin with the model identifier, followed by a dot, followed by an arbitrary complement, e.g. :
form 'Employee.mainView'
If you follow this convention and if the model property is not set, SJS will look for a model with the identifier being the view id prefix. If it finds one, it will use it exactly as if the model property had been set. This means that if the Employee
model exists, the following statements are equivalent :
form 'Employee.mainView'
form 'Employee.mainView', model:'Employee'
Focus on the special "custom" argument
The argument named custom
is a special argument that opens SJS built-in statements to further arbitrary customization. This argument is especially useful to customize generic action instances that are assigned to a view. Another important usage of the custom
argument concerns the bean
statement that allows to create instances of arbitrary classes.
The custom argument value is a map (associative array).
Supported map value types are :
- String :
'value'
- Boolean :
[true|false]
- Integer :
12
- Decimal :
12.34
- List :
['value1','value2']
- Map:
[key1:'value1', key2:'value2']
Regarding List and Map value types, the supported keys and values are :
- String :
'value'
- Boolean :
[true|false]
- Integer :
12
- Decimal :
12.34
As a consequence, it is impossible to declare to nest maps into a custom
argument value. The following example shows a simple usage of the custom
argument on an action definition :
action 'chooseEmployeeAction',
parent: 'lovAction',
custom: [autoquery:false,
entityDescriptor_ref:'Employee',
initializationMapping:['company': 'company'],
okAction_ref:'addFromList']
There are certain situations where you need to assign a reference to a custom property. On the previous example, the okAction_ref
is such a property as it references another action whose identifier is addFromList
(same applies for the entityDescriptor_ref
property). In that case, SJS must be aware of the value being a reference to :
- generate correct Spring XML
- control the validity of the reference
Since the custom argument contains arbitrary properties, there is no way for SJS to know for the expected type of the value. That is why a reference property must be sufixed by _ref
so that the value is interpreted as a reference to another component.
Whenever a list valued property is _ref
suffixed, every of its values is interpreted as a reference.
A map valued property cannot be _ref
suffixed itself. But each of its entry can be individually.
Properties defined as arguments or closures
Some properties can be declared indifferently as an argument or as a closure. These properties are generally reference properties. Let's take an example.
A vertical split container references 2 views : 1 for the top part (the "top" property) and 1 for the bottom part (the "bottom" property).
You can simply reference existing views by using top
and bottom
arguments, e.g. :
split_vertical 'Departments.and.teams.view',
top:'Company-departments.table',
bottom:'Department-teams.table'
Or you can use closures to define your top and bottom views inline, e.g. :
split_vertical('Departments.and.teams.view') {
top {
table(parent:'Company-departments.table') {
...
}
}
bottom {
table(parent:'Department-teams.table') {
...
}
}
}
Or you can even mix both, e.g. :
split_vertical('Departments.and.teams.view',
top:'Company-departments.table') {
bottom {
table(...) {
...
}
}
}
Using a closure to define a property value allows :
- to inherit an existing component but override some of its configuration locally.
- to define brand new "anonymous" components inline.
Using a closure will often be referenced as an inline declaration. The main difference between an inline declaration and a top-level declaration is that the inline one is anonymous, i.e. it cannot be referenced from outside (using _ref
suffix for instance). You will typically favorize inlined declarations for parts of the application that are hardly to be reused. On the other hand, top-level declarations are required to be named, thus can be later referenced.
Arguments factorization
paramSet
You will often face situations where you will want to name and reuse a set of arbitrary characteristics without introducing inheritance between your components. In order to achieve this goal, SJS provides the paramSet
statement.
Declaration
As for every SJS statement, the paramSet
statement can be named. However, unlike other statements its arguments are un-constrained and a paramSet statement does not allow for a closure.
Here is an example of a paramSet
declaration :
paramSet 'roMandatory', readOnly:true, mandatory:true
Usage
Every SJS statement allows for a paramSets
argument that expects a list of paramSet identifiers. When performing generation, SJS will simply de-reference each paramSet and apply its arguments to the owning declaration.
Here is an example using the paramSet defined in the former section :
string_32 'firstName', paramSets:['roMandatory']
which is equivalent to :
string_32 'firstName',
readOnly:true,
mandatory:true
Note
As any SJS statement,
paramSet
allows for theparamSets
argument ! This feature allows to nest paramSets between eachother.
Whenever the same argument is defined in different applied paramSets, the last one in the list wins, meaning that declared paramSets are applied in the order they are declared. This way, a paramSet value can always be overriden by the owning statement.
Sometimes, you will need to remove an argument from a paramSet when using it. Overriding a paramSet argument with a null
value ill simply remove the argument from the paramSet scope.
paramSet
and paramSets
can be used in both SJS Domain and SJS Front DSLs.
template
A template is a special type of paramSet that is implicitely applied to statements that have the same name as the template identifier.
Declaration
Here is an example of a template to be applied to the application forms :
template 'form', parent:'decoratedView',
labelsPosition:'ABOVE', columnCount:2
Usage
There is nothing explicit to code in order to use a template. Once the template of the former section has been declared, the following declarations are both equivalent :
form 'Employee.pane'
form 'Employee.pane', parent:'decoratedView',
labelsPosition:'ABOVE', columnCount:2
The 'form' template as implicitely been applied to the form
statement.
template
is only supported in SJS Front DSL.
Namespaces
Namespace usage allows to simplify SJS based source code by inferring package names of classes and resources that are produced. This package determination is based on conventions covering best practices about a Jspresso project layout.
A namespace is opened using the namespace
statement, e.g. :
namespace('org.jspresso.hrsample') {
...
}
The namespace will be applied to all statements that are placed into the namespace
scope (between the curly braces).
Using the formerly defined namespace will make thefolowing declarations equivalent (note how the namespace has been applied to the image resource) :
Entity 'City', icon:'city-48x48.png'
Entity 'org.jspresso.hrsample.model.City',
icon:'classpath:org/jspresso/hrsample/images/city-48x48.png'
When a project is generated using the Jspresso archetype, a default namespace is created using the root package name. All declarations placed into standard SJS ource files are implicitely placed into this namespace.
Whenever SJS finds a "/" (slash) in a resource name or a "." (dot) in a class name, the enclosing namespace is ignored and the resource path or class name is considered absolute (i.e. not relative to the namespace).
Namespaces can be nested. In that case, nested namespace names are concatenated.
Examples
This section gathers some high-level commented examples so that you can make your mind about SJS expressiveness for Jspresso applications authoring. To go further, you can refer to the SJS reference section of this manual as well as the Jspresso reference manual.
SJS Domain
Interface('Nameable') {
string_64 'name', mandatory: true
}
Declares an interface Nameable to gather a common set of properties (in this case, the name
property of type string, maximum length 64 characters and mandatory).
Entity('Company', extend: 'Nameable',
icon: 'company-48x48.png') {
set 'employees', composition:true, ref:'Employee'
}
Declares a Company entity that implements the Nameable interface (thus inheriting the name
property). Whenever an instance of Company needs to be graphically represented (in a tree view for instance), the company-48x48.png
icon will be used.
The Company entity holds a collection of Employee entities (with a set semantics). It actually maps a 1-N relationship towards Employee that is further classified as being a composition (whenever a company is deleted from the system, all its employees are).
Entity('Employee', extend: 'Nameable',
processor:'EmployeePropertyProcessors',
uncloned:['ssn'], icon:'male-48x48.png') {
string_64 'firstName', mandatory:true, processors:'FirstNameProcessor'
date 'birthDate'
string_10 'ssn', regex:'[\\d]{10}', regexSample: '0123456789',
reference 'company', ref: 'Company', mandatory: true, reverse:'Company-employees'
set 'teams', ref: 'Team'
}
Declares an Employee entity that implements Nameable. Its ssn
(social security number) is not cloned when the entity is. It's visually represented by the male-48x48.png
icon.
A class EmployeePropertyProcessors is assigned to the Employee entity that will hold all property processors (see Jspresso reference documentation) that maintain the entity integrity and gets triggered . Each property processor will be declared on its owning property (firstName
for instance).
Employee owns a birthDate
date property and a ssn
string property that is 10 characters max and validated by a regular expression. An employee can belong to multiple teams (Team entity) through the teams
collection property.
The company
reference property declares the reverse side of the bi-directional 1-N relationship between Company and Employee. This property is made mandatory so that an employee must belong to one and only one company. Note that the Company-employees
identifier has implicitely been defined when you have declared the employees
property on the Company entity. This is true for all collection properties (set or list) that are very often to be reused. This automatic identifier is generated using the pattern [Entity]-[property name]
.
Entity('Team', extend: 'Nameable',
icon: 'team-48x48.png') {
set 'teamMembers', ref:'Employee', reverse:'Employee-teams'
}
Declares a Team entity that implements Nameable. It's visually represented by the team-48x48.png
icon.
Its teamMembers
collection property declares the reverse side of the N-N bi-directional relationship between Team and Employee. Note how the implicit Employee-teams
identifier is used to reference and connect the 2 sides of the relationship.
SJS Front
template 'form', parent:'decoratedView',
labelsPosition:'ABOVE',
columnCount:2
Declares a template for form
statements. All new forms will have decoratedView
as parent (a built-in abstract view with a titled border), labels above their matching fields and fields organized in 2 columns.
form 'Employee.pane',
fields: ['name', 'firstName', 'company.name'],
widths: [name:2],
description: 'employee.editing'
Declares a form
on Employee. since it's a form, it inherits the form template characteristics. It will display 3 fields with the name
field spanning the 2 columns (thus being alone on its row). Note the use of the dot nested notation (company.name
) to reference compound properties. A tooltip will be installed on the form and Jspresso will use employee.editing
as the internationalization key to look-up the translation of the tooltip based on the session user language.
treeNode 'Company-departments.treeNode', render:'name'
treeNode 'Department-teams.treeNode', render:'name'
tree('Company.tree', render: 'name') {
subTree('Company-departments.treeNode') {
subTree('Department-teams.treeNode')
}
}
Declares 2 tree nodes (tree levels), 1 for the company departments and 1 for the department teams. The relationships backing the tree nodes are inferred by convention from the identifiers. Each department and team nodes will use the underlying entity name as label (render
argument). Each concrete tree node will leveraged the icon declaration of it model entity, i.e. Company, Department or Team.
tabs 'Company.tab.pane',
views: ['Company.pane', 'Company.tree']
Builds a tab pane with 2 tabs, the first tab displaying the Company.pane
view (not described in this manual) and the second one displaying the Company.tree
view.
table 'Company-departments.table', actionMap:'masterDetail'
Declares a table displaying a company departments. The standard built-in masterDetail
action map is assigned to the view.
split_vertical 'Company.and.Departements.view',
top:'Company.tab.pane',
bottom:'Company-departments.table'
Declares a vertically oriented split pane. The top view of the split pane is the Company.tab.pane
whereas the bottom view is the Company-departments.table
.
Key concepts
This section translates the key concepts that are detailed in the Jspresso reference manual into SJS terminology.
Entity relationships
Entity relationships definition is only a matter of a few SJS statements. Please note that Jspresso leverages this declarations to seamlessly preserve your domain model integrity, especially when relationships are bi-dierectional (modifying 1 end of the relationship impacts the other end).
Reference relationship (1 multiplicity)
A department belongs to a company.
Entity('Company') {
...
}
Entity('Department') {
...
reference 'company' ref:'Company'
...
}
Collection relationship (N multiplicity)
A company is made of an unordered collection of departments.
Entity('Company') {
...
set 'departments',
ref:'Department'
...
}
Entity('Department') {
...
}
Note
Whenever the
departments
collection should be an indexed collection (i.e. with element order persisted), you can use thelist
statement instead of theset
statement.
Bi-directional relationship
Both relationship ends defined above must be connected between each other.
Entity('Company') {
...
set 'departments',
ref:'Department'
...
}
Entity('Department') {
...
reference 'company',
ref:'Company',
reverse:'Company-departments'
...
}
As used above, SJS implicitely creates a top-level, identified and referenceable component for each relation end. The pattern used for auto generated identifiers is :
[OwningComponent]-[associationNameEnd]
In the example above, the Company-departments
relation end is referenced as the reverse end of the company
one.
Note
The default generated relationship end identifier can be explicitely overriden by using the id argument.
Making a bi-directional association is allowed for any type of relationship end :
- a 1-1 association is defined by making 2 references reverse of each other.
- a 1-N association is defined by making a reference reverse of a collection. This is also the default type of association created when the association is not bi-directional.
- a N-N association is defined by making 2 collections reverse of each other.
For instance, if a department can be managed by an employee and an employee can manage at most a department, we can define the 1-1 association as follows :
Entity('Employee') {
...
reference 'managedDepartment',
ref:'Department',
reverse:'Department-manager'
...
}
Entity('Department') {
...
reference 'manager',
ref:'Employee'
...
}
Note
You can connect 2 relationship ends by only setting the
reverse
argument on one of the ends. The other end is automatically updated.
Business rules
There are several ways that are offered by Jspresso in order to inject business logic into the application domain model. All of them are described into the Jspresso reference manual. This section explains how you would translate these concepts using SJS.
Lifecycle interceptors
Lifecycle interceptors allow to inject java code that will be executed on entity lifecycle events :
onCreate
(in-memory creation)onPersist
(in perstsistent store creation)onUpdate
(in persistent store update)onDelete
(in persistent store deletion)onLoad
(load from persistent strore)
This is how you can declare lifecycle interceptors using SJS statements :
Interface('Traceable',
interceptors:'TraceableLifecycleInterceptor') {
date_time 'createTimestamp'
date_time 'lastUpdateTimestamp'
}
in SJS, lifecycle interceptors packages are determined by convention using the following pattern :
[namespace].model.service
Here is the code of the lifecycle interceptor (TraceableLifecycleInterceptor.java
) , given that the application namespace is org.jspresso.hrsample
:
package org.jspresso.hrsample.model.service;
import ...
public class TraceableLifecycleInterceptor extends
EmptyLifecycleInterceptor<Traceable> {
@Override
@SuppressWarnings("unused")
public boolean onPersist(Traceable traceable, IEntityFactory entityFactory,
UserPrincipal principal, IEntityLifecycleHandler entityLifecycleHandler) {
traceable.setCreateTimestamp(new Date());
return true;
}
@Override
@SuppressWarnings("unused")
public boolean onUpdate(Traceable traceable, IEntityFactory entityFactory,
UserPrincipal principal, IEntityLifecycleHandler entityLifecycleHandler) {
traceable.setLastUpdateTimestamp(new Date());
return true;
}
}
Property processors
Property processors allow to inject java code that will be executed on property modification phases :
before
to check the incoming new property value and potentially reject it with a deailed explanation.in-between
to perform some systematic transformation on the incoming value before actually passing it along.after
to trigger some extra computation once the property has actually been modified.
As detailed in the Jspresso reference guide, SJS follows the convention of grouping all the property processors of an entity into an outer, englobing class. Each property processor (defined on the property level) is then an inner class of this containg class (defined on the entity level).
For instance, you can declare property processors like this :
Entity('Employee',
processor:'EmployeePropertyProcessors') {
string_32 'firstName', processors:'FirstNameProcessor'
date 'birthDate', processors:'BirthDateProcessor'
}
SJS determines property processors package by convention using the following pattern :
[namespace].model.processor
Here is the code of the property processors containing class (EmployeePropertyProcessors.java
) , given that the application namespace is org.jspresso.hrsample
:
package org.jspresso.hrsample.model.processor;
import ...
public class EmployeePropertyProcessors {
/**
* Birth date property processor.
*/
public static class BirthDateProcessor extends
EmptyPropertyProcessor<Employee, Date> {
/**
* Checks that the employee age is at least 18.
* <p>
* {@inheritDoc}
*/
@Override
public void preprocessSetter(Employee employee, Date newBirthDate) {
if (newBirthDate == null
|| employee.computeAge(newBirthDate).intValue() < 18) {
throw new IntegrityException("Age is below 18", "age.below.18");
}
}
}
/**
* First name property processor.
*/
public static class FirstNameProcessor extends
EmptyPropertyProcessor<Employee, String> {
/**
* Formats the new first name. The formatting is :
* <li>Capitalize the 1st letter
* <li>Lower case all the other letters
* <p>
* {@inheritDoc}
*/
@Override
public String interceptSetter(Employee employee, String newFirstName) {
if (newFirstName != null && newFirstName.length() > 0) {
StringBuffer formattedName = new StringBuffer();
formattedName.append(newFirstName.substring(0, 1).toUpperCase());
formattedName.append(newFirstName.substring(1).toLowerCase());
return formattedName.toString();
}
return super.interceptSetter(employee, newFirstName);
}
}
}
Services
Arbitrary services can be declared and implemented on entities. A service defnition is made of :
- an interface that specify the service.
- an implementation class that implements the service code itself.
A service can implement several methods and an entity can implement several services. Services are declared on an entity using a map (associative array). The map keys are the services interfaces and the map values are the matching services implementation classes.
For instance, you can declare services like this :
Entity ('Employee',
services:[EmployeeService:'EmployeeServiceDelegate']) {
...
}
SJS determines services package by convention using the following pattern :
[namespace].model.service
Here is the code of the service interface (EmployeeService.java
) , given that the application namespace is org.jspresso.hrsample
:
package org.jspresso.hrsample.model.service;
import ...
public interface EmployeeService {
/**
* Computes the employee age.
*
* @param birthDate
* the employee birth date.
* @return the computed age based on the birth date or null if the birth date
* is not available.
*/
Integer computeAge(Date birthDate);
}
and service implementation (`EmployeeServiceDelegate.java`) :
package org.jspresso.hrsample.model.service;
import ...
public class EmployeeServiceDelegate implements IComponentService {
/**
* Computes the employee age.
*
* @param employee
* the employee this service execution has been triggered on.
* @param birthDate
* a birth date (might be different than the actual employee birth
* date).
* @return the age computed from the birth date passed as parameter.
*/
public Integer computeAge(Employee employee, Date birthDate) {
if (birthDate != null) {
return new Integer(
(int) ((new Date().getTime() - birthDate.getTime())
/ (1000L * 60 * 60 * 24 * 365)));
}
return null;
}
}
Note
The service implementation class does not itself implement the service interface. This is discussed in the Jspresso reference manual.
Computed properties
Jspresso allows to define computed properties on entities. These computed properties are calculated using some java code that is injected in the entity definition. As described in the Jspresso reference manual, all computed properties should be grouped in a single class that is used as an "extension" of the core entity.
For instance, you can declare entity computed properties like this :
Entity ('Employee',
extension:'EmployeeExtension') {
date 'birthDate'
integer 'age', computed:true
}
SJS determines extension class package by convention using the following pattern :
[namespace].model.extension
Here is the code of the extension class (EmployeeExtension.java
) , given that the application namespace is org.jspresso.hrsample
:
package org.jspresso.hrsample.model.extension;
import ...
public class EmployeeExtension extends
AbstractComponentExtension<Employee> {
/**
* Constructs a new <code>EmployeeExtension</code> instance.
*
* @param extendedEmployee
* The extended Employee instance.
*/
public EmployeeExtensionSimple(Employee extendedEmployee) {
super(extendedEmployee);
}
/**
* Computes the employee age.
*
* @return The employee age.
*/
public Integer getAge() {
return getComponent().computeAge(getComponent().getBirthDate());
}
}
Notice how the computeAge
service defined in the former section is used to compute the age
property.
Actions
Actions are application components that allow the user to interact with the application. An action can trigger any type of behaviour like, e.g. :
- creating an entity and adding it to a collection
- generating a report and displaying it to the user
- opening a transaction to perform some complex operation on the domain model
- navigate programmatically between modules and workspaces
- [place your own action here]...
Jspresso offers tens of built-in standard actions that can be reused or even assembled to make more complex ones.
Most of the time, an action is materialized as a UI component (generally a button) using an icon and/or a label and/or a descriptive tooltip. However, actions can be faceless, e.g. module startup actions, list double-click actions, ...
Action structure
An action is a piece of behavioural code by itself but it also provides a structure that makes it suitable for combining : actions can be chained together.
Chaining occurs when an action is registered on another one as :
next
action, i.e. the execution flow passes definitively to the next action.wrapped
action, i.e. the execution flow comes back to the original action once the wrapped action has finished its execution.
Actions are classified into 2 main categories that influence the way they can be chained together :
- frontend actions act on the frontend layer, i.e. they can deal with the UI.
- backend actions are UI agnostic, i.e. they only deal with the domain or backend services.
All chaining combination rules are valid except that frontend actions cannot be chained (next
or wrapped
) behind backend actions. In general, backend actions are chained next
another backend action or wrapped
a frontend action.
Action lists and action maps
Actions are grouped into action lists that in turn are grouped into action maps. Every view can be assigned an action map. Most of the time, an action map is visually represented as a toolbar. Jspresso comes with standard built-in action maps that can be used as-is on views. For insance the masterDetail
built-in action map contains the actions to work on a master-detail UI.
Using SJS, action maps can be assigned to views using the actionMap
argument, e.g. :
table 'Company-departments.table',
actionMap:'masterDetail'
Actions can be inherited and customized, e.g. :
action 'addFromList',
parent:'lovOkFrontAction',
next:'addAnyToMasterFrontAction'
In the example above, the addFromList action inherits the built-in front.lov.okaction and chains the built-in front.any.add action as next
action.
You can also define new action maps that use existing actions or even declares new ones inline, e.g. :
actionMap {
actionList('EDIT') {
action(parent:'lovAction',
custom:[
autoquery:false,
entityDescriptor_ref:'Employee',
initializationMapping:['company':'company'],
okAction_ref:'addFromList'
])
action(ref:'removeAnyCollectionFromMasterFrontAction')
}
}
Actions can be written in plain java or groovy.
Arbitrary beans
bean, set, list and map
SJS Front provides a general, loosely-typed syntax to instanciate arbitrary java beans that can be referenced afterwards in standard SJS components. This is particularly useful to perform arbitrary customizations that would not be otherwize possible due to the evident limitation of the strongly typed SJS syntax.
SJS statements that compose this feature are bean
, set
, list
and map
.Their meaning is very close to their Spring companions, i.e. <bean>
, <set>
, <list>
and <map>
tags.
The bean
statement provides the following arguments :
- an identifier
parent
: the parent component identifier.class
: the bean fully-qualified class name.custom
: a map of custom properties (refer to the related documentation chapter).
bean
statements can be nested; in that case, the nested bean definition is considered a property of the enclosing one. list
, set
and map
statements are always declared into the bean statement closure. in other words, they cannot be made top-level components and thus individually referenced from outside.
Here is a quite complex bean definition :
bean('siteArchiveConfigSet',
class:'com.bluevox.cms.backend.action.AbstractResourceArchiveAction$ConfigSet',
custom:[siteFileName:'pages.xml']) {
bean('siteDumper', class:'com.bluevox.cms.backend.action.SiteDumper',
custom:[
templateResourcePath:'/com/bluevox/cms/backend/action',
templateName:'XmlSiteDumper.ftl'
]
)
}
The bean identifier
A bean identifier is interpreted differently depending on the position of the bean
statement.
When the bean
statement is a top-level statement, the identifier is used the classic way. It allows to reference the bean in other SJS components.
When the bean
statement is inlined into another component closure, the identifier is leveraged as being the property name of the enclosing component.
When the bean
statement is declared into the map
statement closure, the identifier becomes the map key under which the bean is stored. This behaviour is also true for any SJS component defined inline into the map
statement.
When the bean
statement is declared into the list
or set
statement closure, no identifier is allowed.
Note
Although
bean
,set
,list
andmap
break the limits, they require a relatively good knowledge of the Spring DI container so that the generated code fulfills the requirements. SJS won't be of any help for controlling the actual runtime types of the beans.
SJS source files organization
Default organization
When a project is initialized using the Jspresso application archetype, 5 standard SJS source files are created :
application.groovy
is a wrapper for the other files. You will rarely modify it unless you want to change the default project setup (to split the SJS files in functional domains for instance).model.groovy
contains the application domain model (compiled using SJS Domain).view.groovy
contains the application views (compiled using SJS Front).frontend.groovy
contains everything that is frontend related but distinct from the views, e.g. modules and workspaces for instance (compiled using SJS Front).backend.groovy
contains everything that is backend related but distinct from the domain model, e.g. backend actions for instance (compiled using SJS Front).
All the boiler plate SJS code is already written in application.groovy
. This means that you don't have to deal with the application namespace, error reporting, and so on. All you have to do is directly write you application SJS code into the pre-initialized SJS files. For instance, the following declaration in model.groovy
is enough to generate an entity :
Entity('Person') {
string_32 'name'
integer 'age'
}
Further modularization
You can further modularize SJS applications using the include
statement :
include('filename.groovy')
included files are merged exactly as if all their statements had been written directly into the in the including SJS source file. This is actually the same mecanism that is used by the application.groovy
wrapper to include the standard SJS source files.
Identifiers unicity scope and "spec" section
As a general rule, component identifers must be unique across the appliction. If 2 components are declared using the same identifier, SJS will raise a compilation error duringthe build.
However, SJS allows an advanced usage in order to open specific scopes in which you can define components with the same identifier. A typical usage of this feature is to declare different implementation for the same identifier depending on the UI channel (e.g. Qooxdoo vs Flex vs Swing). This specific scopes feature is only available for SJS Front and not for SJS Domain.
Opening a specific scope is achieved using the spec
statement, e.g. :
spec('qooxdoo') {
...
}
spec('flex') {
...
}
spec('swing') {
...
}
All the declarations that are placed inside the spec closure belong to this scope.
Contrary to what you might believe, components that are declared in a specific scope are visible from their inner AND outer scopes. They are only isolated from their same level scopes.
Scopes meaning are not limited to UI based customization. You could define staging scopes like development pre-production and production.
Scopes are then used in application.groovy
to write specific Spring XML context files that are afterwards used to compose specific Spring contexts to declare in beanRefFactory.xml
.
In application.groovy
:
frontendBuilder.writeOutputFile('swing',
project.properties['outputDir'],
'swing-'+project.properties['viewOutputFileName'])
This will produce a custom swing-dsl-view.xml
Spring context file in the build output directory. This custom Spring context file is then available for inclusion in a custom Spring context.
In beanRefFactory.xml
:
<bean
id="hrsample-swing-context"
class="org.springframework.context.support.ClassPathXmlApplicationContext"
lazy-init="true">
<constructor-arg>
<list>
...
<value>org/jspresso/hrsample/spec/swing-dsl-view.xml</value>
...
</list>
</constructor-arg>
</bean>