Programming guide

From Complete Cyclos documentation wiki
Jump to: navigation, search

Contents

General

The Cyclos code is internally developed as 3 base projects, cyclos3 for the service layer (service and entity definitions), cyclos3_impl for service implementation (dao, security, and service implementations) and cyclos3_web for the web layer (Struts actions and forms, JSPs, JavaScripts). Some additional projects are used internally, like cyclos3_standalone (that configures Jetty as a servlet container on the standalone packages) and cyclos3_build (contains the Ant build file that generates the downloadable packages).

Cyclos code conventions

All Java code should follow these code conventions:

  • Names for variables, classes, methods and constants follows the standard Java Conventions;
  • All brackets are opened on the same line as the statement;
  • Every public class and method should be commented with JavaDocs;
  • The this reference should be avoided;
  • Variables and parameters should be declared as final whenever they are not modified;
  • Unused imports should be removed, and star imports (java.util.*) should be avoided;
  • Brackets should always be used, even on simple statements.

Eclipse (from 3.3 Europa) now supports set a formatter and an option to cleanup the code every time a class is saved, which helps enforcing those conventions. For setting up see Environment setup.


Coding conventions for help pages

The help files are located in the ''cyclos3_web'' project, under pages > general > translation_files > helps, and are ordered by locale.

The helps are divided in modules or categories like advertisments.jsp or account management.jsp. Please do not add any new modules to this. The helps are used to build (automatically) manuals (admin and member) and they are used for the help pop-ups. The help pop-ups jump just to an anchor inside one of the help files. The head description is used for the general description of the function and is obligatory. The sub description is optional and only used for very large help files (like account management) that have sections that can be considered as categories themselves (e.g Currencies in Account management)

Setup of a help file:

  • head description (obligatory, just one)
  • sub description (optional, zero or more, should only appear in the manual, not in the help pop-ups!)
  • "where to find it?" section. Shortly covers where the functionality can be found. The

text "where to find it?" is a header in itialics. Must be repeated for every subheader section, if more than one is available. Otherwise it will go right below the head description.

  • "How to get it working?" section. Shortly covers which settings and configuration must be

done in order to get the functionality working. Applies mostly only for admin, though in some cases it may also be relevant for members. The header is in italics. Must be repeated for every subheader section, if more than one is available. Otherwise it will go right below the head description.

  • As all the above belong to the header, it is followed by a horizontal ruler (hr). Between (sub)header descriptions, where to find it, and how to get it working sections there will be no horizontal rulers.
  • context specific help window section (optional, zero or more)
  • possibly more submodule heads followed by more context specific help windows
  • optional: glossary heading followed by one or more glossary items, each explaining specific terms used in this help file.

Conventions for helps via the template

  • Each of the items listed above under "setup" has a specific code syntax. These are available in a template file you can download:

helptemplates.xml. You can import them via window - preferences - web and xml - jsp files - templates. Call the templates via code assist (ctrl-space, and the first few letters of their titles).

  • the template has the following content. It's obligatory to use the template in all the cases listed in this table, to enforce consistency over the complete help system.
name description example
"Quotes" putting quotes around a term, usually to refer to a literal on the screen. Note that the "" are part of the name. "description"
HAdmin A text which should only be visible for admins <span class="admin">Text for admin only</span>
HBroker A text which should only be visible for brokers <span class="broker">Text for broker only</span>
HDividingHeader For headers making a main division in the page, but which are not linked to or classified via p-description tags. the "member activity results" header in the statistics help file
HExternal link a link to another file and subject in the help system. see <a href="${pagePrefix}${cursor}#anchorName"><u>payment filter</u></a>
HGH Glossary header The header for the start of the glossary, to be used at most once in a help file
HGlossary item help the start of an item in the glossary of terms in a help file <a name="median"></a><b>Median</b><p> The median bla bla blah ... more explanation <hr class='help'>
HHeader for help module the main header of the file, to be used at its start
HInternal link a link to another anchor INSIDE the same help file see <a href="#bla"><u>bla</u></a>
HMember A text which should only be visible for member <span class="member">Text for member only</span>
HOperator A text which should only be visible for operators <span class="operator">Text for operator only</span>
HPicture To display an icon. There are several icons available, like edit.gif, delete.gif, view.gif, etc. See the directory cyclos3_web\WEB_INF\images\system on your local cyclos install for an overview of all available icons. <img border="0" src="${images}/edit.gif" width="16" height="16">
HSubmodule Header help header for submodule in the new help system; the text is also to be processed into the project site descriptions
HUnknown link to be used during the development of the new help system, to link to other files in the help system which have not yet been written. Later on, after all files are written, you should perform a search for "unknownAnchor" and replace them all with real anchors. see <a href="${pagePrefix}payments#unknownAnchor"><u>payment filters</u></a >
HWindow Help Help window section. This is the content of one single help window. If two windows link to one and the same help window, you may copy/paste the anchor line (with the <a href.... so that it appears twice (with of course an adapted anchor) <a name="bla"></a> <h3>Bla's</h3> This window explains about bla's... <hr class="help">



Some more help file Conventions

  • please use the format tool in eclipse (ctrl shift f), but make sure the anchors (e.g. <a name=...>) of a new subject start after a blank line.
  • Take care that the title in the help file is the same as the title in the page where the help is referring to. So if the page says "Modify group", then the help page title should NOT be "Edit group", but "Modify group". Be aware of this, sometimes this is not correct in the existing skeleton which has been made.
  • refer to menu items with the following format:
"Menu: Account > Member payment"

Note that there are never parenthesis.

  • All literal texts on the forms are refered to in the same way. This applies to button texts, text on drop downs, or on any other element in the form. Example:
The "Submit" button
  • To highlight only use quotes in the format " (see check list table above)
  • You can combine the <span class=... tags above to make a text visible for various

classes together, like this:

<span class="admin broker">Text for admin and broker only</span>
  • Start every sentence or stand alone word with Capital character.
  • bullet lists:
    • no space between items of the list, unless it is a very long list with sublists in the list items. In that case use twice a <br><br>
    • title of the list in bold (<b>..</b>).
    • you may also use bold for list item titles, for example <b>full members:</b> bla bla... The bold title is followed by a colon (:) (inside the bold). This is NOT followed by a <br>
    • NO <br> after the end of the item; it is useless as each <li> tag autmatically starts at a new line.
    • Be AWARE: span tags to limit the contents to certain groups CANNOT be combined with list items. So this:

<span class="admin">
<li>bla bla
</span>

will go wrong because the span tag will be ignored, resulting in a list item still visible for members and brokers. In stead of this, use:


<li class="admin">bla bla

  • All html tags are in lower case.
  • line width is 80, not 72 (the default in eclipse. Set via window > preferences > web and xml > html files.)
  • The end of each help file should contain about 20 <p> </p> tags, to prevent that the last help window anchor will not appear in top of the help screen when that specific help window is requested. See the custom_fields.jsp help file for an example.


Links in help pages
Try to use links whenever this is helpful. You can use internal links (within the same help page) as follows:

<a href="#ads_results"><u>See ads results</u></a> (example of a link in the advertisements help page to an anchor ads_results)

<a href="${pagePrefix}groups#group_filters"><u>group filter</u></a> (example of a link from any help page to the anchor group_filers in the groups help page)

<a href="${pagePrefix}groups"><u>Groups</u></a> (example of a link from any help page to groups help page)

<img border="0" src="${images}/delete.gif" width="16" height="16"> (A link to an image)

Tips on the helps...

  • You can make the link under a help button visible in mozilla by changing some options: in mozilla, go to Menu: extra > options > content > javascript > advanced, and select the show status in toolbar checkbox.
  • Use the p-sub-description tag only for important issues, not for every help window.
  • Note that the h2 tags for the titles are INSIDE the p-sub-decription tags. This is often wrong in the existing bare bone help files which have not yet been filled with text. We advise to check the existing syntax and improve if necessary (preferably by using the templates).
  • Though the formatting tool in eclipse (ctrl-shift-F) is quite useful, it can also be a pain. One issue is that it decides that some lines are to be followed by a white line. The more often you press ctrl-shift-F while editing the document, the bigger will the gaps of many white lines become, as for every time ctrl-shift-F is pressed, a white line is added. A more save way is to use the formating tool only on selected areas, when doing this the rest of the page will not be formatted (this will not work if there is a

    tag within the selected text). Another solution is to just put a <br> after the line causing the gap.

  • You can use regular expressions in eclipse to search for certain patterns. Use the search dialog in eclipse (ctrl-F), and be sure to have the "regular expressions" checkbox checked. Examples of useful searches to check on:
    • Search for any link (<a href="..") which is NOT underlined (links should always be underlined, or else they are not visible to the user):
[^/][^u][^>]</a>

or

<a href=.*">[^<][^u]
    • Search for a tag without closing tag, for example for <u>:
<u>[^</]*</[^u]

Best practices

  • No code in the model layer (entities, DAOs, services) should reference any class on the web layer. Also, DAO's should not reference a service;
  • The code should be refactored whenever possible, avoiding replication of logic in more than one place.

Use of Java Enumerations

Enumerations are widely used on Cyclos. Always when there are multiple possible values for a given attribute, and those values are static, an enumeration is used. Examples are: invoice direction (incoming or outgoing), advertisement status (active, permanent, scheduled or expired) are examples of enumerations. In order to reduce duplication, whenever enumerations are used on JSP pages (like options on a select input), the enum items are not hard-coded on the JSP file, but the enum items are stored by the Action on the request, and that collection is iterated to generate the content.

Exemple code for the Action class:

//Store all enum items, following the item declaration order, on the statusList request attribute
RequestHelper.storeEnum(request, Ad.Status.class, "statusList");
//Store only specific items, on a different order
request.setAttribute("statusList", Arrays.asList(Ad.Status.ACTIVE, Ad.Status.SCHEDULED));

Then, in order to use them on the JSP page, the default is to have a translation key with a common prefix and ending with the enum item. On the given example, ad.status.<ITEM>:

<html:select property="query(status)" styleClass="InputBoxEnabled">
    <html:option value="">
        <bean:message key="global.search.all"/>
    </html:option>
    <c:forEach var="status" items="${statusList}">
        <html:option value="${status}">
            <bean:message key="ad.status.${status}"/>
        </html:option>
    </c:forEach>
</html:select>

In order to persist enum items, the enum class must implement StringValuedEnum or IntValuedEnum. They both declare a method called getValue, which returns a String or int respectively. That value will be stored on the database. Also, a custom hibernate type will have to be declared on the hibernate mapping.

Enum class:

public static enum Status implements StringValuedEnum {
    PENDING("N"), CHECKED("C"), PROCESSED("P");

    private final String value;

    private Status(final String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }
}

Hibernate mapping:

<property name="status" column="status" length="1" not-null="true">
    <type name="nl.strohalm.cyclos.utils.hibernate.StringValuedEnumType">
        <param name="enumClassName">nl.strohalm.cyclos.entities.accounts.external.ExternalTransfer$Status</param>
    </type>
</property>

Enumerations are also used to declare all relationships an entity has. Those enum classes must implement the Relationship interface, which has a getName method. Here's an example:

public static enum Relationships implements Relationship {
    CUSTOM_VALUES("customValues"), LOANS("loans"), MEMBERS("members");
    private final String name;

    private Relationships(final String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

Lazy loading relationships

In older cyclos versions, none of the relationships between child - parent were lazy. For example: if loading a transfer, you also were loading the complete from and to account, their owners, the type, ... a lot of entities even if all we need is a single transfer field. So, in cyclos3, all relationships are lazy, which means: if we load a transfer, none of it's relationships will be loaded. If one tries to access the relationship, an exception will occur, unless you have explicitly fetched the property, which can be done using the FetchService.fetch method or by loading or searching entities passing which relationships will be fetched. Each entity has an enumeration with it's relationships, so only those really needed are read from the db.

It is also possible to fetch relationships recursively, for example, I want to load a transfer and show the username of the member who performed the payment. The transfer has a from account, which has a member, which has an user. This is done with the RelationshipHelper.nested method.

Here are some examples:

//Fetching a pre-existent instance
Member member = ...;
member = fetchService.fetch(member, 
    Element.Relationships.GROUP, 
    RelationshipHelper.nested(Member.Relationships.BROKER, Element.Relationships.USER));

//Loading an instance with fetched relationships
Ad ad = adService.load(10L, Ad.Relationships.OWNER, Ad.Relationships.CURRENCY);

//Searching entities with a fetch
MemberQuery query = new MemberQuery();
query.fetch(Element.Relationships.GROUP, Member.Relationships.BROKER);
query.setName("john");
List<Member> members = (List<Member>) elementService.search(query);

It is also possible to fetch collections on the same way, but it's not possible to recursively fetch relationships for elements in collections. Example:

//To retrieve a member's advertisements with their categories, I cannot use the nested, because ads is a collection
Member member = (Member) elementService.load(id, Member.Relationships.ADS);
//To fetch ad categories, the ads collection itself must be passed to the fetchService.fetch method
Collection<Ad> ads = fetchService.fetch(member.getAds(), Ad.Relationships.CATEGORY);

JavaScript

Including a JavaScript file

Cyclos provides it's own tag, <cyclos:script src="/path/to/script" /> which appends the Cyclos application version to the script path as a request variable, forcing browsers to not using cached files among different versions, which could lead in script errors.

Third party JavaScript libraries

The following external libraries are used on Cyclos:

  • Prototype (http://www.prototypejs.org/): Used as a library for Ajax requests and DOM manipulation.
  • Behaviour (http://www.bennolan.com/behaviour/): Heavily used in order to apply event handlers to page elements. Since it's used on every page, and several times per page uses a CSS selector, it's own selector function is changed to use Prototype's $$ function on Firefox and other Gecko-based browsers or Ext's DOMQuery on other browsers, due to a large performance difference between those 2 implementations on different browsers.
  • Scriptaculous (http://script.aculo.us/): Used to render the auto-complete for member selections and for drag-n-drop.
  • JavaScripTools (http://javascriptools.sourceforge.net/): Used as a function library and to apply input masks.
  • ExtJS (http://extjs.com/): As said before, a trimmed-down version of Ext 1.1 is used because of it's DOMQuery function.

JavaScript code conventions for Cyclos

  • No JavaScript in JSP. Normally, each JSP has a corresponding javascript file with the same name and the js extension. You should declare its use in the header of the jsp, like this:
<script src="<c:url value="/pages/reports/statistics/forms/statisticsForms.js"/>"></script>
  • No java code in the JSP. Scriptlets are avoided because they make it harder to read and maintain the pages.
  • Render elements of a Style class
  • Comment your JavaScript. Each function should have a javadoc-like comment telling what it does. You can use the /* and */ for making these comments.
  • Use of Prototype addons to elements, arrays and other functions, like:
// For each element with the "trAdmin" class, invoke Element.show or Element.hide, depending on the variable shouldShow
$$(".trAdmin").each(Element[shouldShow ? "show" : "hide"]);

// For each input "delete" class, set a pointer cursor and add another CSS class
$$("input.delete").each(function(input) {
    setPointer(input);
    input.addClassName("highlight");
});
  • Accessing data on the JSP or translation keys from JavaScript: Since JavaScript files are not processed by the application server, when a specific variable (example: the logged user's name) or translation message (from the resource bundle) needs to be used on the JavaScript file, we must pass them as variables to the script. Please note the <cyclos:escapeJS> tag, which is important to avoid problems when using characters that, if written directly to the page, could break the JavaScript syntax, like quotes or line breaks. Here is an example:
<script>
    var removeConfirmation = "<cyclos:escapeJS><bean:message key="group.removeConfirmation"/></cyclos:escapeJS>";
    var groupName = "<cyclos:escapeJS>${group.name}</cyclos:escapeJS>";
</script>

Predefined class names

The head.js and layout.js files declares several CSS classes that may be used by controls and other elements to add behaviour automatically. They are:

  • Input elements:
    • required - Adds an asterisk to mark required fields.
    • number - Applies a mask for input fields to accept only numbers.
    • pattern - Applies an InputMask pattern that should be declared on an additional element attribute called maskPattern.
    • float - Applies a number mask for numbers using decimal places and thousand separators.
    • floatHighPrecision - Applies a number mask for numbers using high precision decimal places and thousand separators.
    • floatNegative - Applies a number mask for negative numbers using decimal places and thousand separators.
    • date - Applies a date mask, according to the date format configured on Cyclos, as well as appends the format right after the field, so the user knows the field order.
    • dateNoLabel - Applies a date mask, according to the date format configured on Cyclos, without appending the format after the field.
    • dateTime - Applies a date and time mask, according to the date and time formats configured on Cyclos, as well as appends the format right after the field, so the user knows the field order.
    • dateTimeNoLabel - Applies a date and time mask, according to the date and time formats configured on Cyclos, without appending the format after the field.
    • upload - Should be used on file inputs, append the configured size limit right after the field, so the user knows the size limit for file uploads.
  • Textarea elements:
    • maxLength - Restricts the maximum length of the field, which should be declared on a maxlength attribute.
    • richEditor - Transforms the text area in a rich text editor.
    • required - Adds an asterisk to mark required fields.
  • Select elements:
    • required - Adds an asterisk to mark required fields.
  • Image elements:
    • help - Adds the help tooltip.
    • remove - Adds the remove tooltip.
    • edit - Adds the edit tooltip.
    • permissions - Adds the permissions tooltip.
    • print - Adds the print tooltip.
    • exportCSV - Adds the export to CSV tooltip.
    • view - Adds the view tooltip.
    • preview - Adds the preview tooltip.

Predefined JavaScript variables

Several variables are made available to every JavaScript files. Here are them:

  • context: The path to the application context.
  • pathPrefix: A prefix for actions according to the logged user. Will be something like <context>/do/member when a member is logged or <context>/do/admin when an administrator is logged.
  • isAdmin, isMember,isBroker,isOperator: They are booleans that will reflect the logged user.
  • numberParser, highPrecisionParser, dateParser, dateTimeParser: Are NumberParser or DateParser instances that reflect the current localization settings.

Use JSP data in JavaScript files

The only JavaScript code used on JSP pages is data passed to the scripts. The <cyclos:escapeJS> tag is used to ensure the text written will be sanitized for JavaScript string, avoiding breaking the syntax. Examples:

//A resource bundle message
var confirmationMessage = "<cyclos:escapeJS><bean:message key="ad.remove.confirmation"/></cyclos:escapeJS>"

//An id (a number cannot break the syntax - no escapeJS tag)
var currentMemberId = "${member.id}";

//A description could contain line breaks, quotes or other data that would break the syntax - use the escapeJS tag
var groupDescription = "<cyclos:escapeJS>${group.description}</cyclos:escapeJS>";

//A collection of data to JavaScript
var transferTypes = [];
<c:forEach var="tt" items="${transferTypes}">
    transferTypes.push({id:${tt.id}, name:"<cyclos:escapeJS>${tt.name}</cyclos:escapeJS"});
</c:forEach>

Actions for result lists

The result lists often have actions, like remove, edit, view, check/uncheck all and so on. Normally, the Behavior library is used on the JavaScript file using CSS selectors and a function related function to apply behaviour to page elements, like these:

Behaviour.register({

//View details
'img.details': function(img) {
    setPointer(img);
    img.onclick = function() {
        self.location = pathPrefix + "/editExternalAccount?externalAccountId=" + img.getAttribute("externalAccountId");
    }
},
//Remove a record
'img.remove': function(img) {
    setPointer(img);
    img.onclick = function() {
    if (confirm(removeConfirmation)) {
        self.location = pathPrefix + "/removeExternalAccount?externalAccountId=" + img.getAttribute("externalAccountId");
    }
},
//Add a new record
'#newButton': function(button) {
    button.onclick = function() {
        self.location = pathPrefix + "/editExternalTransferType?account="+externalAccountId;
    }
},
//Check all rows
'#selectAllButton': function(button) {
    button.onclick = checkAll.bind(self, "adInterestsIds", true);
},
//Uncheck all rows
'#selectNoneButton': function(button) {
    button.onclick = checkAll.bind(self, "adInterestsIds", false);
}
});

The elements class name is used by Behaviour. Remember that an element can have several class names, separated by spaces. Also, many times an argument must be passed to the Behaviour function, like the record identifier. Here is an example for the above 'img.details' selector:

<img class="edit details" externalAccountId="${externalAccount.id}" src="<c:url value="/pages/images/edit.gif"/>" />

Hide & Show elements in pages

Prototype extensions are used to show or hide elements. Here are a few examples:

//Show all rows with a myClass class
$$('tr.myClass').each(Element.show);

//Show or hide rows with a myClass class, according to a flag
$$('tr.myClass').each(Element[willShow ? 'show' : 'hide']);

Pseudo inheritance in javascript

Though not needed much, sometimes we use a form of pseudo-inheritance in javascript. We do this in the statistics part. There is one common javascript file, which defines the functions for all the forms. Then, for each form there is an additional javascript file, which defines extra functions or variations on these common functions. If you define a function in the specific javascript file which has the same name as a function in the common javascript file, the common function will be overridden and is lost. You can still use the common function by using the following construct:

var superNameOfFunctionInCommonScriptFile = NameOfFunctionInCommonScriptFile;
NameOfFunctionInCommonScriptFile = function () {
   superNameOfFunctionInCommonScriptFile();
   additionalFunctionCalls();     
}

JSP

Read form elements

Default struts lets you read form elements whenever there is a property with getter (and maybe a setter). But cyclos forms don't have properties, except for query: The use this:

getQuery(String key)

and

setQuery(String key, Object value)

The way to read form elements into the jsp is by using this inside the jsp:

query(keyName)

Predefined variables

Several variables are made accessible through session or application scopes, so they can be easily accessed through JSP expression language, like ${varName}. Those variables are:

  • Settings
    • localSettings: LocalSettings instance, containing basic Cyclos configurations.
    • accessSettings: AccessSettings instance, containing access and user related configurations.
    • alertSettings: AlertSettings instance, containing system and member alerts.
    • logSettings: LogSettings instance, containing file logging configurations.
    • mailSettings: MailSettings instance, containing mail-related configurations.
    • mailTranslation: MailTranslation instance, containing translations for mails sent by Cyclos.
    • messageSettings: MessageSettings instance, containing translations for notifications sent by Cyclos.
  • Logged user data
    • loggedUser: The logged user.
    • loggedElement: The logged element.
    • isAdmin: boolean indicating whether the logged user is an administrator.
    • isMember: boolean indicating whether the logged user is a member.
    • isBroker: boolean indicating whether the logged user is a broker.
    • isOperator: boolean indicating whether the logged user is an operator.
    • loggedMemberHasAccounts: boolean indicating whether the logged user has at least one account.
    • loggedMemberHasSingleAccount: boolean indicating whether the logged user has a single account.
    • loggedMemberHasLoanGroups: boolean indicating whether the logged user has is part of a loan group.
    • actionPrefix: Prefix that can be used on forms to arbitrary actions, appending a common prefix according to the user type, like admin or member.
    • pathPrefix: Prefix that can be used to build URL's according to the logged user, like '<context>/do/admin' or '<context>/do/member'

Menu items and permissions

The <cyclos:menu> tag generates a menu item and may be nested within another <cyclos:menu> tag to create sub-menus. It have an url attribute that is relative to the context root and a resource bundle key to retrieve the item label. Optionally, both 'module' and 'operation' attributes may be specified to ensure that only when the logged user has that permission granted, the menu item will be displayed. When a menu item has no sub-items (i.e., no permissions), the root menu item is not shown.

Here's an example:

<cyclos:menu key="menu.admin.bookkeeping">
    <cyclos:menu 
        url="/do/admin/overviewExternalAccounts" 
        key="menu.admin.bookkeeping.overview" 
        module="systemExternalAccounts" 
        operation="details" />  
</cyclos:menu>

Multi drop down

The Multi drop down is a Cyclos custom widget that combines a select box with check boxes, which is a convenient way to select multiple options occupying a small space on the page. There is a custom tag, <cyclos:multiDropDown that generates the control. It has a name (the checkboxes name) and several other options, as described on the tag library documentation.

Below is an example:

<cyclos:multiDropDown name="query(groupFilters)" emptyLabelKey="member.search.allGroupFilters">
    <c:forEach var="groupFilter" items="${groupFilters}">
        <cyclos:option value="${groupFilter.id}" text="${groupFilter.name}" />
    </c:forEach>
</cyclos:multiDropDown>

Result pagination

Normally, search pages, whose Action extends BaseQueryAction will store the search results on the request under a specific name. Assuming that name as members, the following code generates page navigation links:

<cyclos:pagination items="${members}"/>

It is very important that the action's databinder has a registered property for page parameters, which can be registered using a helper, like this:

binder.registerBinder("pageParameters", DataBinderHelper.pageBinder());

When that property is not registered on the data binder, navigating to other pages will have no effect, and the first page will be shown again.

Checking permissions

On many situations it is important to check whether a given permission is granted to the logged user, in order to determine whether a button will be displayed, for example. To do that, there is a function on the Cyclos tag library called cyclos:granted, which takes 2 parameter: module name and operation name. When the operation name is null, return true when there is at least 1 operation under the given module. An usage example would be:

<c:if test="${cyclos:granted('memberReports', 'showAccountInformation')}">
... do stuff
</c:if>

Toggle row color in list

Every table that shows results use 'zebra' rows, alternating row colors. To do that, Cyclos use the toggle tag library, and rows are created using:

<tr class="<t:toggle>ClassColor1|ClassColor2</t:toggle>">
...
</tr>


Controls

Data binding

General info about DataBinders

The general idea of data binders is: a form is submitted, containing a bunch of strings and some basic types, but we need to convert internationalized data (dates/numbers), entities, nested entities, collections... This is done by the DataBinders.

There are several DataBinder implementations:

  • PropertyBinder: reads/writes a bean property, optionally using a custom Converter to convert from/to String.
  • BeanBinder: has many nested binders, one for each bean property.
  • SimpleCollectionBinder: reads a string array, converting each value to an object, building a collection.
  • BeanCollectionBinder: Uses a BeanBinder to read several string arrays. It is assumed that each array corresponds to a property, and that all arrays items on the same index corresponds to the same bean. Each bean is placed on a single collection.
  • MapBinder: uses a nested binder for the keys and another for the values.

The PropertyBinder uses a Converter to convert from/to objects and strings. There are several converters out-of-the-box, like:

  • CoercionConverter: Uses the CoercionHelper.coerce method to convert data. It is used by default when no converter is specified.
  • NumberConverter: Converts from/to strings and numbers.
  • UnitsConverter: Converts from/to strings and numbers using a currency pattern.
  • CalendarConverter: Converts from/to strings and calendars.
  • StringTrimmerConverter: Trims a string.
Reading/writing JavaBeans from/to the form

In order to read or write an entire object from or to a form, a BeanBinder is used. Tipically, the Action class will have an instance variable called that will be initialized upon the first use, on a getDataBinder method. To create the BeanBinder, a factory method can be used, and then properties should be declared using the registerProperty method.

For a matter of consistency, there is a BaseBindingForm class that declares a protected Map<String, Object> called values. In order to use nice names on the jsp, and not values, child forms declares 4 methods, according to the object it is editing. Assuming a form to edit a person, those methods would be: getPerson() - returning the whole Map, getPerson(key) - returning the value associated with that key, setPerson(Map) - setting the whole map and setPerson(key, value) - setting a specific key's value. The 4 methods are necessary because on the page, the Struts tags will interpret properties as mapped properties, on this example, person(key), and we will need to read the Map as a whole in order to do the data binding. Examples ahead.

Given a JavaBean called Person, with an id, name, birthDate, income and a boolean flag called active, this would be a data binder example on the action class:

private DataBinder<Person> dataBinder = null;

private DataBinder<Person> getDataBinder() {
    if (dataBinder == null) {
        LocalSettings settings = SettingsHelper.getLocalSettings(getServlet().getServletContext());

        BeanBinder<Person> binder = BeanBinder.instance(Person.class);
        binder.registerBinder("id", PropertyBinder.instance(Long.class, "id", IdConverter.instance()));
        binder.registerBinder("name", PropertyBinder.instance(String.class, "name"));
        binder.registerBinder("birthDate", PropertyBinder.instance(Calendar.class, "birthDate", settings.getDateConverter()));
        binder.registerBinder("price", PropertyBinder.instance(BigDecimal.class, "price", settings.getNumberConverter()));
        binder.registerBinder("active", PropertyBinder.instance(Boolean.TYPE, "active"));

        dataBinder = binder;
    }
}

The form object would be something like this:

public class PersonForm extends BaseBindingForm {
    public Map<String, Object> getPerson() { 
        return values;
    }

    public Object getPerson(String key) {
        return values.get(key);
    }

    public void setPerson(Map<String, Object> map) {
        values = map;
    }

    public void setPerson(String key, Object value) {
        values.put(key, value);
    }
}

The JSP file, the form (without layout or field labels) would be something like:

<ssl:form method="POST" action="${formAction}">
    <html:hidden property="person(id)"/>
    <html:text property="person(name)" styleClass="large"/>
    <html:text property="person(birthDate)" styleClass="small date"/>
    <html:text property="person(income)" styleClass="small float"/>
    <html:checkbox property="person(active)" styleClass="large"/>
</ssl:form>

Then, to read the person object, the code on action is:

PersonForm form = context.getForm();
Person person = getDataBinder().readFromString(form.getPerson());

Before editing, the person could be set on the form like this

PersonForm form = context.getForm();
Person person = ...;
getDataBinder().writeAsString(form.getPerson(), person);
DataBinders and nested JavaBeans

Now lets assume that the previous Person has an Address, which is not an empty, but a nested JavaBean (Hibernate calls it component). It is possible to register a BeanBinder inside another BeanBinder instance. An example would be:

BeanBinder<Address> addressBinder = BeanBinder.instance(Address.class);
addressBinder.registerBinder("street", PropertyBinder.instance(String.class, "street"));
addressBinder.registerBinder("number", PropertyBinder.instance(Integer.class, "number"));
addressBinder.registerBinder("city", PropertyBinder.instance(String.class, "city"));

BeanBinder<Person> personBinder = ...
personBinder.registerBinder("address", addressBinder);

Then, for the binding process, a nested object must exist on the form, so Struts can set the nested value. A nested MapBean must be created on the form constructor, or an error will occur on form submit. The MapBean is a class that Struts sees as a dynamic bean, with properties declared dynamically. The MapBean constructor receives the property names it must respond to. Here is an example:

public PersonForm() {
    setPerson("address", new MapBean("street", "number", "city"));
}

Then, at the jsp page, the fields can be used by separating the component from it's properties with a period:

<html:text property="person(address).street" styleClass="large" />
<html:text property="person(address).number" styleClass="small number" />
<html:text property="person(address).city" styleClass="large" />
DataBinders and collections of values

The SimpleCollectionBinder transforms string collections (or arrays) in object collections or object collections into string collections. In order to use it, a hack must be done on the Form, or Struts will set the first element only. In order to Struts handle the property as a collection, an empty collection must be on the form.

Following the previous Person example, let's assume that the person class now have a collection of Interests that the user selects using a multi drop down component.

The following code should be set on the form constructor. This is VERY important!!!:

public PersonForm() {
    setPerson("interests", Collections.emptyList());
}

The data binder should have the property registered. Remember that when no converter is specified, a coercion converter is used. For entity types, the entity is converted from / to it's identifier as string, making it easy to read several entity references from an array of strings, like:

BeanBinder<Person> binder = BeanBinder.instance(Person.class);
...
binder.registerBinder("interests", SimpleCollectionBinder.instance(Interest.class, "interests"));

Then, on the JSP, the collection is built by creating several fields with the same name. In case of the multi drop down, several checkboxes with the same name are generated. Here is an example, assuming the action sets a collection as a request attribute called 'interestList'. It is also assumed that the 'person' instance is set under that name on the request. Also note the cyclos:contains tag that tests whether a item is contained within a collection:

<cyclos:multiDropDown name="person(interests)" emptyLabelKey="person.pleaseSelectInterests">
    <c:forEach var="interest" items="${interestList}">
        <cyclos:option value="${interest.id}" text="${interest.name}" selected="${cyclos:contains(person.interests, interest)}"/>
    </c:forEach>
</cyclos:multiDropDown>
DataBinders and collections of beans

The BeanCollectionBinder mixes the ideas of both SimpleCollectionBinder and BeanBinder to create collections of JavaBeans. It reads the form data by reading a string array for each property and then, positionally, assemble a JavaBean with all properties and adding it to a returned collection.

Still following the previous example, now we are going to add a collection of Preferences for the person, assuming the preference have an id, a name and a value.

First, the form must respond to the nested object, and again we will use the MapBean. But now, as the properties are collections and no longer atomic values, the first constructor argument of the MapBean should be set to true, indicating that it's a holding collections. Here is the code that should be placed on the form constructor:

public PersonForm() {
    setPerson("preferences", new MapBean(true, "id", "name", "value"));
}

Then, the DataBinder code on the action would have this added to it:

BeanBinder<Person> binder = BeanBinder.instance(Person.class);
...
BeanBinder<Preference> preferenceBinder = BeanBinder.instance(Preference.class);
preferenceBinder.registerBinder("id", PropertyBinder.instance(Long.class, "id", IdConverter.instance()));
preferenceBinder.registerBinder("name", PropertyBinder.instance(String.class, "name"));
preferenceBinder.registerBinder("value", PropertyBinder.instance(String.class, "value"));
binder.registerBinder("preferences", new BeanCollectionBinder(preferenceBinder, "preferences"));


Initialize empty Collections

Struts can only set nested beans if they exist on the form. so, take a look on the constructor of forms that use nested beans normally a Map stores the properties (so we don't need to declare each one) that map is on the BaseBindingForm it's overriden with a proper name, like, BaseQueryForm defines a getQuery, setQuery... but, the constructor should set an empty nested bean there's a class called MapBean, that receives a list of accepted properties if you don't do that, struts will throw an exception saying the nested bean doesn't exist collections have a true before the properties on the MapBean constructor you may check the EditLocalSettingsForm

Validating forms

Form validation is not done in javascript, but coordinated via the action.

In the action...

To validate a form, you have to override BaseQueryAction.validateForm or BaseFormAction.validateForm. The body of the method would typically look like this:

protected void validateForm(final ActionContext context) {
   //get the form
   final StatisticsFinancesForm form = context.getForm();    
   //get the object to validate
   final Object paymentFilterIds = form.getQuery("paymentFilters");
   //do the validation
   statisticalService.validate(paymentFilterIds);
}

As you can see, it is the Service which is responsible for the validation itself.

In the Service...

In the Service implementation, a validate method would typically look like this:

private Validator validator = null;

public void validate(final LoanGroup loanGroup) {
    getValidator().validate(loanGroup);
}

private Validator getValidator() {
    if (validator == null) {
        validator = new Validator("loanGroup");
        validator.property("name").required().maxLength(100);
        validator.property("description").required().maxLength(1000);
        validator.property("fictional").key("global.fictional").required();
        validator.property("fictional2").displayName("Fictional II").required();
    }
    return validator;
}

The semantics for the validator are:

  • The validator is created with a default resource bundle prefix.
  • Validation properties are added through the property method, which takes the property name. The default resource bundle key will be <default prefix>.<name>. When the key is different, the key method may be used to specify the key. When a fixed string will be the property display name, it can be set through the displayName method.
  • Every property method return the property descriptor itself, allowing chained method invocations.
  • On this example, the name property is both required and has a maximum length of 100 characters.

The following property validations are included out of the box:

  • required: The property is required.
  • maxLength: Validates a maximum length.
  • minLength: Validates a minimum length.
  • length:.Validates both minimum and maximum length.
  • fixedLength: Validates that the value has a fixed length.
  • regex: Validates the value matching the given regular expression.
  • anyOf: Validates the property value is any of the given values.
  • noneOf: Validates the property value is none of the given values.
  • positive: Validates the property as a positive number.
  • positiveNonZero: Validates the property as a positive number greater than zero.
  • greaterEquals: Validates the property as a number that should be greater than the given number.
  • lessEquals: Validates the property as a number that should be less than the given number.
  • between: Validates the property as a number that should be between the 2 given numbers.
  • inetAddr: Validates the property as an IP address or host name.

Other validations may be included as well, using the add method. The validation should implement PropertyValidation and return a subclass of ValidationError when an error exist or null when the property is valid.

It is also property to have general validations (not tied to an specific property). The Validator.general method register a general validation that should implement GeneralValidation.

Calling validation from javascript

You call the validation of the form from the javascript file belonging to the form, for example via an onclick of the submit button, or in a construction like this:

form.onsubmit = function() {
    return requestValidation(form);
}

Http filters

Transaction Filter

The transaction filter applies the open session in view pattern, allowing the whole request to use a single database transaction.

Encoding Filter

The servlet specification defines that the HttpServletRequest.setEncoding() method must be called before parsing the request parameters. This filter sets it according to the character encoding defined on the Cyclos configuration.

Logged User Filter

Filter used to verify if the user is logged and to disconnect the user after the time out time configured in the system (settings - access settings)

Web Services White List

Applies the 'whitelist' for web services access, allowing specific hosts to access the services

Action context & helpers

Resource bundle messages from control

The action classes may access a message translation using the ActionContext.message method, optionally passing arbitrary arguments, for example:

String memberName = context.message("member.name");
//If my.message is 'I am {0}, hello {1}', the output will be 'I am John, hello Joe'
String anotherMessage = context.message("my.message", "John", "Joe");
Redirect pages (forward with arguments)

Since Cyclos use the redirect after post pattern, sending a redirect after any form submission (to avoid an action to be re-executed when the member reloads the browser), and Struts forwards does not support parameters, Cyclos use a custom api to send a redirect passing request parameters. The ActionHelper class declare the redirectWithParam and redirectWithParams methods. The former receives a parameter name and value, and the latter a Map<String, Object> with the parameters. Here is an example:

// A single parameter named 'memberId'
ActionHelper.redirectWithParam(request, forward, "memberId", 10);

// Several parameters
Map<String, Object> params = new HashMap<String, Object>();
params.put("memberId", 6);
params.put("groupId", 10);
params.put("return", true);
ActionHelper.redirectWithParams(request, forward, params);
Error handling

The BaseAction class handles some common exceptions, like ValidationException and PermissionDeniedException. Other exceptions are caught to generate an application error log.

Specific business exceptions should be handled by the actions, like:

Transfer transfer;
try {
    transfer = doPayment(context, payment);
} catch (final NotEnoughCreditsException e) {
    return context.sendError("payment.error.enoughCredits");
} catch (final MaxAmountPerDayExceededException e) {
    return context.sendError("payment.error.maxAmountOnDayExceeded");
} catch (final UnexpectedEntityException e) {
    return context.sendError("payment.error.invalidTransferType");
} catch (final UpperCreditLimitReachedException e) {
    return context.sendError("payment.error.upperCreditLimit");
}
Retrieve settings

Use the SettingsHelper static methods getLocalSettings, getAccessSettings and so on, passing the request or servlet context.

Send messages to JSP

When an action is performed, normally a JavaScript alert is shown to the user to confirm the action. To do this, an action must send a message to the next page, using:

context.sendMessage("member.profile.changed");

Service Layer

Creating new permissions

In order to create a new permission, you should add a new enum item to the corresponding class:

  • AdminSystemPermission: Permissions of administrators over system data;
  • AdminMemberPermission: Permissions of administrators over members;
  • AdminAdminPermission: Permissions of administrators over other administrators;
  • MemberPermission: Permissions of members over it's own data;
  • BrokerPermission: Permissions of brokers over their managed members;
  • OperatorPermission: Permissions of operators over their owner member's data;
  • BasicPermission: Permissions common to all types of users.

Also, check out the subclasses of GroupPermissionsDTO for more details on how to apply the permissions themselves.

Security layer

There are 2 implementations for each service interface: one for enforcing security and another one for the actual business implementation. The security layer contains a reference to the local service interface (which is only implemented by the actual service implementation). Each implemented method should check the applicable permissions and finally return the value returned by the service implementation.

The security implementations are located on the same packages as their corresponding service implementations, and use extensively the PermissionService.permission(...) method.

Data access layer

BaseDAO methods

The BaseDAOImpl class, from which all other DAO implementation classes extend, provide several methods for it's implementations, like:

  • load(id, relationship...): Loads an entity using it's identifier, optionally fetching specified relationships together.
  • insert(entity): Inserts the entity on the database, returning it's updated version;
  • update(entity): Updates the entity on the database, returning it's updated version;
  • delete(id...): Deletes the entities with the given identifier(s);
  • list, uniqueResult, iterate: Several variations, used to run an HQL query and return the result in different forms.

HQL query helper

The HibernateHelper class offers several method to help building an HQL query, based on a StringBuilder to build the query string and a Map containing the named parameters.

Here are some:

  • getInitialQuery: Returns the StringBuilder object for querying a given entity type. Optionally receives the fetch to embed on the query itself using the 'left join fetch' statement if possible;
  • addParameterToQuery: Adds a condition for property = value when the value is not null or empty;
  • addLikeParameterToQuery: Adds a condition for upper(property) like '%' + upper(value) + '%' when the value is not null or empty;
  • addParameterToQueryOperator: Adds a condition for property operator value when the value is not null or empty;
  • addInParameterToQuery: Adds a parameter for property in (values) when value is not null or empty;
  • addInElementsParameter: Adds a parameter for value in property collection when value is not null or empty;
  • addPeriodParameterToQuery: Adds a parameter for property >= begin and property <= end when value is not null or empty;
  • appendOrder: Adds the order by statement

Example:

//Assuming an entity called Person, here is the PersonDAOImpl
public List<Person> search(PersonQuery query) {
    Map<String, Object> namedParameters = new HashMap<String, Object>();
    StringBuilder hql = HibernateHelper.getInitialQuery(Person.class, "p", query.getFetch());
    HibernateHelper.addLikeParameterToQuery(hql, namedParameters, "p.name", query.getName());
    HibernateHelper.addParameterToQueryOperator(hql, namedParameters, "p.income", ">=", query.getMinimumIncome());
    HibernateHelper.addParameterToQuery(hql, namedParameters, "p.type", query.getType());
    HibernateHelper.addInParameterToQuery(hql, namedParameters, "p.group", query.getGroups());
    HibernateHelper.addPeriodParameterToQuery(hql, namedParameters, "p.creationDate", query.getCreationPeriod());
    HibernateHelper.appendOrder(hql, "p.name");
    return list(query, hql.toString(), namedParameters);
}

Steps to create a functionality

There are several steps to add functionality, because it involves changing and configuring all application layers and configuration files.

Here there is a bottom-up listing of steps:

Entities

Normally new functionality will require new persistent entities or, at least, changes to existing entities. The package they're created is a subpackage of nl.strohalm.cyclos.entities. On either case, the ['http://www.hibernate.org hibernate] mapping should be created / modified, and new mapping files should be declared on the persistence.xml file.

Entities always extend the Entity class, which declares the identifier (always a generated Long) and other methods, like equals and hashCode (both use the identifier).

There also should be an enumeration representing the relationships, as explained here.

Here is an example:

public class ExternalAccount extends Entity {

    public static enum Relationships implements Relationship {
        MEMBER_ACCOUNT_TYPE("memberAccountType"),  SYSTEM_ACCOUNT_TYPE("systemAccountType"), 
        FILE_MAPPING("fileMapping"), TYPES("types"), IMPORTS("imports");
        
        private final String name;
        private Relationships(final String name) {
            this.name = name;
        }
        public String getName() {
            return name;
        }
    }

    private static final long                  serialVersionUID = -3694123388080600042L;
    private String                             name;
    private MemberAccountType                  memberAccountType;
    private Collection<ExternalTransferType>   types;
    
    @Override
    public String toString() {
        return getId() + " - " + getName();
    }

    // Omitting getters and setters
}

The entity should also be mapped on a Hibernate xml file. Here is the example for the previous ExternalAccount class, which is called ExternalAccount.hbm.xml:

<hibernate-mapping>
    <class name="nl.strohalm.cyclos.entities.accounts.external.ExternalAccount" table="external_accounts">
    <cache usage="read-write"/>
    <id name="id" type="long">
        <column name="id" sql-type="integer"/>
        <generator class="native"/>
    </id>
    <property name="name" column="name" type="string" length="50" not-null="true" />
    <property name="description" column="description" type="text" />
    <many-to-one name="memberAccountType" class="nl.strohalm.cyclos.entities.accounts.MemberAccountType">
        <column name="member_account_id" index="fk_member_account" not-null="true" sql-type="integer"/>
    </many-to-one>
</hibernate-mapping>

The entity mapping also should be declared on the persistence.xml file, like this:

<bean id="sessionFactory" class="nl.strohalm.cyclos.spring.CustomSessionFactoryBean">
    <property name="mappingResources">
        <list>
            ...
            <value>/nl/strohalm/cyclos/entities/accounts/external/ExternalAccount.hbm.xml</value>
            ...
        </list>
    </property>
</bean>

Entity changes almost always lead to database changes. These have to be applied also. Don't forget to enter any changes in the changelog.xml file as follows:

<version label="3.6_dev2">
    <statements database="mysql">
        <item>alter table account_status add column d_rate decimal(15,6), [continued sql]</item>
        <item>[more valid sql statements]</item>
    </statements>
</version>

A good tip for entering or changing entities in *.hbm.xml file and in the database is the following recipe:

  • Make changes in the *.hbm.xml file
  • Run nl.strohalm.cyclos.setup.Setup.java with the "-s filename" attribute. This causes the database script to be written in the specified filename
  • Check the exact syntax of the sql statements in the generated file, and use this to enter the changes in changelog.xml. If your statements change an existing table, remember to modify the statements from create table statements to alter table statements. Cyclos will run the statements for you if they are flagged with a version which is newer that the existing database's version.

Before entering them into the changelog.xml, you may want to check your sql statements on syntax errors first against a test database in the mysql console.

DAOs

The database access layer is implemented on Cyclos using DAO's. There is always the separation between the interface and the implementation, but both are on nl.strohalm.cyclos.dao.** package. Other classes that will use the DAO should always reference the interface. The implementation will only be referenced on the dao.xml configuration file.

All DAO interfaces should extend BaseDAO<EntityType> and since not all entities allow all CRUD operations (for example, a remark can only be inserted, others may not be modified, others can't be removed), individual operations are declared on separate interfaces. Here is an example:

public interface ExternalAccountDAO extends BaseDAO<ExternalAccount>, InsertableDAO<ExternalAccount>, 
UpdatableDAO<ExternalAccount>, DeletableDAO<ExternalAccount> {

    /*Lists all external accounts, ordering results by name*/
    List<ExternalAccount> listAll();

    /*External Account Details, ordering results by name*/
    List<ExternalAccountDetailsVO> listExternalAccountOverview();
}

The DAO implementations should extend BaseDAOImpl<EntityType> and implement the specific DAO interface. Another requirement is that the constructor should invoke the super constructor with the specific entity class. Here is an implementation example:

public class ExternalAccountDAOImpl extends BaseDAOImpl<ExternalAccount> implements ExternalAccountDAO {

    public ExternalAccountDAOImpl() {
        super(ExternalAccount.class);
    }

    public List<ExternalAccount> listAll() {
        return list("from ExternalAccount ea order by ea.name", null);
    }

    public List<ExternalAccountDetailsVO> listExternalAccountOverview() {
        final StringBuilder hql = new StringBuilder();
        hql.append(" select new " + ExternalAccountDetailsVO.class.getName() + "(ea.id, ea.name, sum(t.amount))");
        hql.append(" from ExternalAccount ea left join ea.types et left join et.transfers t ");
        hql.append(" group by ea.id, ea.name");
        hql.append(" order by ea.name");
        final List<ExternalAccountDetailsVO> result = list(hql.toString(), null);
        return result;
    }
}

Finally, the DAO implementation should be declared on the dao.xml file as a bean. For historical reasons, the bean is suffixed with Dao and not DAO, so, services that will reference the DAO should have a setter that ends with Dao. Here is the bean definition for the externalAccountDao bean:

<bean id="externalAccountDao" class="nl.strohalm.cyclos.dao.accounts.external.ExternalAccountDAOImpl" />

Services

The service layer is the responsible for executing the business logic and security checks. There are 2 interfaces: the "remote", which contains methods used by the web layer and the "local" interface, used by other services. Also, there are 2 implementations: the security layer and the service implementation. The security layer has a reference to the local interface (service implementation), and is responsible to enforce the permission for the logged user to invoke the current method, then delegates the method execution to the actual service implementation.

The web layer will always (indirectly) reference the security layer. The direct implementations will be hidden from it. So, after correctly enforcing the security, a real service implementation is invoked. Then, subsequent flow will no longer check permissions, as services must reference other services using the local interface, which no longer checks for security. This is important, because, for example, a regular member is not allowed to view transfer types. However, he needs a list of transfer types in order to do a payment. So, the payment service should have a method to list transfer types, checking for the payment permission.

Each class of the security layer must extend BaseServiceSecurity. However, there is no base class for the business implementation.

Permissions

As said before, permissions are declared on the Permissions class. New modules / operations should be added there.

Also, the translation keys for the new permissions should be created on the translation file. For modules, they are called permission.<module name>, and for operations, permission.<module name>.<operation name>.

Struts actions

The actions are, together with the forms, the MVC controls used in Cyclos. They are used to prepare data to be displayed and to pass data from the JSP pages to the service layer.

A normal CRUD will be composed of 3 actions:

  • Search, extending BaseQueryAction when there are parameters or directly BaseAction otherwise;
  • Edit, extending BaseFormAction;
  • Remove, extending directly BaseAction.

There is a struts-config-*.xml file for each functionality group. These files are declared on the web.xml file. Actions are mapped on different paths according to the user type. Administrator actions are mapped on <root>/do/admin/<action>, member actions on <root>/do/member/<action> and operator actions on <root>/do/operator/<action>.

Also, since Struts actions are not within the Spring context (which would require all actions to be also mapped on a beans.xml file), a mechanism is necessary to retrieve beans, such as services. In order to do this, the actions should have a setter for a property with the same name as the Spring bean, and it should be annotated with the @Inject annotation. Since Cyclos extends the default Struts request processor, it sets all dependencies on the action creation.

Actions often use data binders to read data from the user input.

Here is an example for listing external accounts:

public class ListExternalTransferTypesAction extends BaseAction {

    private ExternalTransferTypeService externalTransferTypeService;

    @Inject
    public void setExternalTransferTypeService(final ExternalTransferTypeService externalTransferTypeService) {
        this.externalTransferTypeService = externalTransferTypeService;
    }

    @Override
    protected ActionForward executeAction(final ActionContext context) throws Exception {
        final HttpServletRequest request = context.getRequest();
        final ExternalAccount externalAccount = (ExternalAccount) request.getAttribute("externalAccount");
        final List<ExternalTransferType> externalTransferTypes = externalTransferTypeService.listByAccount(externalAccount);
        request.setAttribute("externalTransferTypes", externalTransferTypes);
        request.setAttribute("editable", getPermissionService().checkPermission("systemExternalAccounts", "manage"));
        return context.getInputForward();
    }
}

The struts-config_externalAccounts.xml should also be mapped on the web.xml file, like this:

<!-- Standard Action Servlet Configuration -->
<servlet>
    <servlet-name>action</servlet-name>
    <servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
    <init-param>
        <param-name>config</param-name>
        <param-value>
            /WEB-INF/struts-configs/struts-config.xml,
            ...
            /WEB-INF/struts-configs/struts-config_externalAccounts.xml,
            ...
        </param-value>
    </init-param>
    ...
</servlet>

Here is the mapping for the given action:

<action-mappings type="org.apache.struts.config.SecureActionConfig">
    <action 
	path="/admin/listExternalAccounts" 
	type="nl.strohalm.cyclos.controls.accounts.external.ListExternalAccountsAction"
        input="admin/_listExternalAccounts">
	<set-property property="secure" value="true" />
    </action>
    ...
</action-mappings>

As a general case, actions with a form should have a scope="request", unless some search actions, whose form may keep the value between calls, where the scope should be session (the Struts default). Also note the input attribute, which points to a path with an underscore. It is a Tiles definition. In the Cyclos standards, all tiles definitions have an underscore before the name.

Forms

You should create a form for actions that require an user input, or even parameters that are passed as URL parameters. The form is declared on the struts-config_*.xml file and it's class is placed on the same package as the action.

JSPs

Cyclos JSP's use several tag libraries: the Java Standard Tag Library (JSTL), the Struts tags, the toggle taglib and a custom Cyclos taglib.

As a normal directive, the page will have as less javascript code as possible, and the JSTL tags are used to control the page generation.

Here is an example for a JSP:

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://jakarta.apache.org/struts/tags/struts-bean" prefix="bean" %>
<%@ taglib uri="http://jakarta.apache.org/struts/tags/struts-html" prefix="html" %>
<%@ taglib uri="http://devel.cyclos.org/tlibs/cyclos-core" prefix="cyclos" %>
<%@ taglib uri="http://www.servletsuite.com/servlets/toggletag" prefix="t" %> 
<%@ taglib uri="http://sslext.sf.net/tags/sslext" prefix="ssl" %>

<script src="<c:url value="/pages/groups/editGroup.js"/>"></script>
<c:set var="titleKey" value="group.title.new"/>
<c:set var="helpPage" value="${isMember ? 'insert_operator_group' : 'insert_group'}"/>

<ssl:form action="${formAction}" method="post">
<html:hidden property="groupId" />
<c:if test="${isOperatorGroup}">
	<html:hidden property="group(nature)" value="OPERATOR"/>
</c:if>
<table class="defaultTableContent" cellspacing="0" cellpadding="0">
    <tr>
        <td class="tdHeaderTable"><bean:message key="${titleKey}"/></td>
        <cyclos:help page="${helpPage}" />
    </tr>
    <tr></tr>
</table>

Pages are mapped on the tiles-def_*.xml file, which have the same suffix as the corresponding struts-config_*.xml file. The tiles-def is declared on the global struts-config.xml:

<plug-in className="org.apache.struts.tiles.TilesPlugin">
    <set-property property="definitions-config" value="
        ...
        /WEB-INF/tiles-defs/tiles-defs_externalPayment.xml
    " />
    ...
</plug-in>

Here is a definition:

<definition name="admin/_listConnectedUsers" extends=".adminLayout">
  <put name="body" value="/pages/access/listConnectedUsers.jsp"/>
</definition

As well as actions, there is a separate tiles definition for each user type, extending a specific layout. They are: .adminLayout, .memberLayout, .brokerLayout and .operatorLayout.

Javascripts

Each page includes by default several javascript libraries. They also should include an specific javascript for that page, which should apply the behavior for the page elements, using the Behaviour library.

Translation files

All text on Cyclos is written on external (translation files, which enables internationalization through properties files.

The keys are organized hierarchically using dots, like accountFee.groups, menu.member.account and so on.

We use an eclipse plugin called Jinto to edit the translation files.

Change Log

The changelog.xml file is used to read the Cyclos versions and to apply automatic database schema upgrade. If a new version contains new database tables or columns, then one or more statements should be added to a new version. Here is an example:

<?xml version="1.0"?>
...
<version label="3.1_dev9">
    <statements database="mysql">
        <item>alter table transfers add external_transfer_id integer</item>
        <item>create index fk_external_transfer on transfers (external_transfer_id)</item>
        <item>alter table transfers add index FK3EBE45E8617A8174 (external_transfer_id), 
add constraint FK3EBE45E8617A8174 foreign key (external_transfer_id) references external_transfers (id)</item>
        <item>alter table loan_payments add external_transfer_id integer</item>
        <item>create index fk_external_transfer on loan_payments (external_transfer_id)</item>
        <item>alter table loan_payments add index FKAF53099C617A8174 (external_transfer_id), 
add constraint FKAF53099C617A8174 foreign key (external_transfer_id) references external_transfers (id)</item>
</statements>
</version>

<version label="3.1_dev8">
    ....
</version>
...

The next time the Cyclos application is started, automatically the given statements will be executed, upgrading the database schema.

The changelog.xml file is also used to generate a textual changelog, allowing other elements than statements to be declared, such as description, bug-fixes, enhancements and so on.