--
PedroRio - 21 Dec 2010
XEO Object Model Behavior- XEO Library
Custom Behavior in XEO Models can be achieved through various way (each with a specific purpose):
- Model Events
- Model Methods
- Attribute Events
- Attribute Properties
To define custom behavior there are two methods: BOL and the Java API. BOL is an acronym for
Business
Object
Language which is essentially a small expression language specific to the XEO platform to help declare behavior. Everything that can be done using BOL can be done using Java (BOL expressions are ultimately converted to Java like any XEO Model is converted to a java class).
In this chapter you'll create the custom behavior required for the XEO Models in the Library application and you'll learn the basics of XEO's Java API (and BOL).
XEO Java API (Basic Introduction)
Each XEO Model is ultimatelly converted to a Java class to support operations (database tables are also created to persist data) and abstract from the relational data model. The classes created from XEO Models extend from a java class included in the framework (the
boObject class), which has a set of base methods to deal with attributes and other Model properties.
A user can login to a XEO Application via a login form or using the Java API to create a session within the XEO application. Having access to a
boSession instance, the Java API can be used to create new instances of XEO Models, load existing instances, change attribute values in existing instances as well as persist any changes made to those instances. One can also query XEO Model instances and iterate through the results. You can also check a XEO Model definition to perform operations based on that information.
In order to load an instance object, to create a new instance or to make a query XEO needs a "context". A context is an instance of the
EboContext class which can be created anytime (given there's access to a
boSession) and the operations to load an object, create an object and query objects all require a parameter which is the context. The
EboContext keeps in memory the loaded objects (and changes to their attribute values), acts as cache and has a connection to the database (meaning that creating many
EboContext instances can consume all available connections to the database). It's possible to create two different contexts and load the same instance into both of them, in this scenario changes made to each instance are not visible from the other context and if both objects are changed and then saved, the last save will crush the first one. The hierarchy of these classes is depicted in figure OB.1.
EboContext hierarchy" src="http://wiki.itds.pt/pub/WebXEO/XeoPrimerObjectBehavior/JavaAPIboSession.png" title="boSession and
EboContext hierarchy" height="720" />
Figure OB.1 - boSession and EboContext hierarchy (with two instances with BOUI = 111, to depict the possibility to have different values)
In most situations, there's no need to explicitly create instances of
EboContext, because an
EboContext is always created in each request (
FIXME: Vericar se isto é correcto) and that context can be used inside your custom code. Each
boObject instance has access to an
EboContext instance (the context in which it's loaded) so even if a method receives as parameter a
boObject, you still have access to the
EboContext.
The following tables represent the most important classes in the XEO API and list some of the methods that are used to create behavior for XEO Models. More detailed information can be found on the
XEO Java API Page.
boObject - Represents an instance of an XEO Model
Method Name |
Description |
Arguments |
Return type |
update |
Saves the object instance |
- |
void |
getAttribute |
Retrieves the handler for an attribute (non-collection) |
(String) attributeName |
AttributeHandler |
exists |
Checks whether the instance is saved in the database |
- |
boolean |
getBridge |
Retrieves the handler for a collection attribute |
(String) collectionName |
BridgeHandler |
AttributeHandler - Represents a handler to deal with an attribute (non-collection) of an Object Model
Method Name |
Description |
Arguments |
Return Type |
getName |
Returns the name of the attribute (as defined in the Object Model) |
- |
String |
getValueString |
Retrieves the value of the attribute as a String* |
- |
String |
getValueLong |
Retrieves the value of the attribute as a Long* |
- |
Long |
getValueDate |
Retrieves the value of the attribute as a Data* |
- |
java.util.Date |
getValueiFile |
Retrieves the value of the attribute as a binary data Wrapper (iFile)* |
- |
netgest.io.iFile |
getValueBoolean |
Retrieves the value of the attribute as a Boolean* |
- |
Boolean |
setValueString |
Sets the value of the attribute with a String* |
(String) newVal |
void |
setValueLong |
Sets the value of the attribute with a Long* |
(Long) newVal |
void |
setValueDate |
Sets the value of the attribute with a Date* |
(Date) newVal |
void |
setValueiFile |
Sets the value of the attribute with a IFile* |
(iFile) newVal |
void |
setValueBoolean |
Sets the value of the attribute with a Boolean* |
(Boolean) newVal |
void |
* - If an attribute is of a given type (String, Number) and you try to retrieve/set the value of the attribute of a different type it will throw an exception (unless the value can be converted to that type - i.e. you can get the String value of a Numeric attribute, but you cannot get the Date value of a Boolean attribute).
bridgeHandler - Represents a handler to deal with a collection attribute of an Object Model (similar to an java.util.Iterator)
Method Name |
Description |
Arguments |
Return Type |
getName |
Returns the name of the collection attribute |
- |
String |
beforeFirst |
Resets the handler to the first position of the collection |
- |
Boolean |
next |
Advances the handler to next position of the collection |
- |
Boolean |
getObject |
Retrieves the object in the current position of the handler |
- |
boObject |
add |
Add a new object to the end of the collection |
(Long) BOUI |
boolean |
remove |
Removes the boObject in the current position of the handler |
- |
boolean |
boManagerBean - Manages the load/create operations for boObjects
This set of classes will be almost enough (querying was purposely left out of the list) to define the behavior for the Object Models in the XEO Library application. Next, you'll create the first XEO Model Method, the
removeCategories method, for the LIB_Book Model.
Creating the first XEO Method - removeCategories (LIB_Book)
The LIB_Book XEO Model has a collection of categories (representing Book categories). As a convenience, if a Librarian wrongly categorizes the book it'll have a method that will clean all categories for that book (instead of forcing the librarian to individually remove each category). Open the LIB_Book Model and go to the "Methods" section and press the add (plus) button to add a new method. Type in "removeAllCategories" for the name of the method and "Remove all categories" for the Label, go to the "Body" section of the method, choose "JAVA" as the language and press the "edit" button (see figure OB.1 for details), which will open a "LIB_BookHandler" Class in Eclipse's Java Editor (see figure OB.1, again, for details).
Figure OB.1 - Creating a new XEO Model Method in XEO Studio
Whenever Java code needs to be created to perform custom-logic, XEO Studio creates a Java class for each XEO Model and names the methods appropriatelly with the correct parameters. In the case of XEO Model Methods the method declaration is always the name of the method given in the Object Model definition with the boObject representing the current instance as parameter.
These auto-generated classes are placed in a different source-code folder (as depicted in figure OB.1a) within the xeo.code.java.XEOPackageName (in this case
XEOPackageName = LIB$1.0 = LIB1_0). There's one java package for each XEO Package in your project (that uses custom java code, of course)
Figure OB.1a - Different source code folder for XEO Studio's generated classes.
Remove All Book Categories - The Java Code
Each (and every) XEO Model Method will be generated by XEO Studio using the method's name property and passing a boObject instance (representing the current instance at runtime) as a parameter. The flow of java code for "remove all book categories" ill be like the following:
- Check if the instance is saved (if it exists, it does not make sense to remove book categories from a book which does not yet exist in the library).
- Load the collection attribute and position the handler in the first position of the collection.
- Iterate through all elements and remove them.
- Save the instance.
The code to remove all book categories is depicted in figure OB.1b.
public void removeAllCategories(boObject obj) throws boRuntimeException {
//Check if the object is saved in the database, new objects cannot remove categories
if(obj.exists()) {
//Get the collection handler (bridgeHandler) for the categories attribute
bridgeHandler categories = obj.getBridge("categories");
//Iterate through them all and remove
while (categories.next())
{ categories.remove(); }
//Save the object
obj.update();
}
}
Figure OM.1b - Remove all book categories method souce code.
The
removeAllCategories methods could be optimized, because the
bridgeHandler class has a convenience method,
truncate, which will remove all elements in the bridge. The result is depicted in figure OM.1c.
package xeo.code.java.lib1_0;
import netgest.bo.runtime.*;
public class LIB_BookHandler {
public void removeAllCategories(boObject obj) throws boRuntimeException {
//Check if the object is saved in the database, new objects cannot remove categories
if(obj.exists()) {
//Get the collection handler (bridgeHandler) for the categories attribute
bridgeHandler categories = obj.getBridge("categories");
//Truncate the collection, removing all elements
categories.truncate();
//Save the object
obj.update();
}
}
}
Figure OM.1c - removeAllCategoires method source code optimized
Next you'll add behavior to validate the e-mail attribute in the LIB_Librarian and LIB_User
LIB_Librarian & LIB_User Behavior - Validate the e-mail attribute
The LIB_Librarian XEO Model has an
email attribute, representing the Librarian's e-mail. E-mail addresses follow a specific pattern and can be validated before saving a Librarian instance. Open the LIB_Librarian Model, select the
email attribute, in the details panel scroll to the section with "Valid" as title and select JAVA from the dropdown menu and click "edit". XEO Studio will generate a handler class with the following method:
public boolean email_Valid(boObject obj,AttributeHandler attribute) throws boRuntimeException {
return true;
}
The objective of this document is not to teach Java Programming, as such the code for the email_Valid method, could be the following:
public boolean email_Valid(boObject obj,AttributeHandler attribute) throws boRuntimeException {
//Keep the value to return
boolean res = false;
//Get the attribute value
String email = attribute.getValueString();
//Create a pattern to check for e-mails
Pattern p = Pattern.compile(".+@.+\\.[a-z]+");
Matcher m = p.matcher(email);
//If the pattern matches, e-mail is ok
if(m.matches() || email.length() == 0)
res = true;
else
obj.addErrorMessage("Invalid e-mail " + email); //Else, add error
//Return the result
return res;
}
For the LIB_User Model add the same valid behavior to the email attribute (since it's the same validation, you can create a class with the code and invoke it from both places.
LIB_Librarian & LIB_User Behavior - Username formula
When creating a new Libriarian or a new User a username must be chosen for both of them. Assume the library has a default "rule" for creating usernames which is the concatenation of their name, a separator (a dot) and the last name.
A formula is a behavior that allows to have an attribute whose value can be calculated based on the values of other attributes (i.e. its dependencies). The formula is triggered whenever one of the depedencies (value of the attribute from which this depends) is changed.
Open the LIB_Librarian Object Model and locate the
username attribute, find the
Formula section and press "Edit" to let XEO Studio generate the Java class for the handler and then add two attributes as "depends" (see red bordered zone in figure OB.2), the
name and
lastname attributes (name is inherited from
iXEOUser interface; this means the formula behavior will be invoked whenever the value of name/lastname are changed.
Figure OB.2 - Editing dependents in a Formula for the username attribute in LIB_Librarian
Go to the Java handler class created by XEO Studio and fill in the following code:
public String username_Formula(boObject obj,AttributeHandler attribute) throws boRuntimeException {
//Only use the rule for a new librarian
if(!obj.exists()) {
//Get the name value
String firstName = obj.getAttribute("name").getValueString().toLowerCase().toString();
//Get the last name value
String lastName = obj.getAttribute("lastname").getValueString().toLowerCase().toString();
//If both values are not null
if(firstName.length() == 0 || lastName.length() == 0)
return firstName + "." + "librarian";
else
return firstName + "." + lastName;
} //In an existing object, return the existing value
else
return obj.getAttribute("username").getValueString().toString();
}
The code for the formula will attempt to concatenate the name and lastname, if both have a value, or the name and the string "librarian" (all strings are separated by a dot), in case the object instance does not exist. If the instance object already exists, it returns the current value of the username attribute.
The (almost) same code is valid for the formula of the username in the LIB_User Object Model (instead of concatenating with ".librarian" in one of the situations it concatenates with ".user".
public String username_Formula(boObject obj,AttributeHandler attribute) throws boRuntimeException {
//Only use the rule for a new librarian
if(!obj.exists()) {
//Get the name value
String firstName = obj.getAttribute("name").getValueString().toLowerCase().toString();
//Get the last name value
String lastName = obj.getAttribute("lastname").getValueString().toLowerCase().toString();
//If both values are not null
if(firstName.length() == 0 || lastName.length() == 0)
return firstName + "." + "user";
else
return firstName + "." + lastName;
} //In an existing object, return the existing value
else
return obj.getAttribute("username").getValueString().toString();
}
Code for Formula of the username attribute (in the LIB_User Object Model)
_
LIB_Message Behavior - Date Default Value and DisabledWhen
The Message XEO Model has a
date attribute which represents the date the message was sent. Although a system SYS_DTCREATE attribute exists in all instances, this is added so that you could see some behaviors such as the default value and the disabled condition. The
date attribute is a value that will not be user-editable (it would not make sense to change the date a certain message was sent), as such it's a value that needs to be displayed but not changed and XEO provides that ability by allowing a "disabled when" condition for each attribute.
Go the
date attribute in LIB_Message and scroll to the "Default Value" section, choosing Java from the drop down and press edit (which will trigger the generation of a Java class handler for LIB_Message. The code to return the current date is the following:
public String date_DefaultValue(boObject obj,AttributeHandler attribute) throws boRuntimeException {
Calendar dueDate = Calendar.getInstance();
return new Date(dueDate.getTimeInMillis()).toString();
}
Default value code for the data attribute
One could use BOL, instead of JAVA to set this value, as the
Business Object Language has an expression,
NOW , which returns the current date.
For the disabled condition, in the same attribute scroll to the "Disabled When" section and choose "BOL" from the drop down and type
true as the value (Java code could also be used with a "return true" statement inside the function,
this dual usage of BOL and Java is to highlight that it can be done and each language should be used whenever they make sense to you as a developer.
LIB_Movement - Events, default value and collection attribute restrictions
The Movement entity represents a book loan from a Library User. A Movement instance can be in one of two states. The "Open" state (where some, or all, of the books are still in possession of the Library User and the "Closed" state when the User has returned all books of that movement. Books that are currently loaned to a user have their state as "Unavailable" which is something done in the act of creating the Movement. Also, movements cannot be changed (i.e. if a user wants to add another book to a movement he can't, he must create a new movement)
Updating all Books in the creation of a movement (onBeforeSave)
To update all books in the creation of a movement you'll add an
OnBeforeSave event to the LIB_Movement XEO Model, in which you'll change the state of the all of the books of the movement to "unavailable". To add an event, open the LIB_Movement Object Model and go the
Events section, press the
add button and choose "OnBeforeSave" from the drop down, then press "Edit" button to let XEO Studio generate the handler class for the LIB_Movement XEO Model, see figure OB.3 for an illustration.
Figure OB.3 - Adding the onBeforeSave event to the LIB_Movement Object Model
The condition for the onBeforeEvent is to change all instances of LIB_Book in the
books collection attribute to a state of "Unavailable", but only if the movement itself is a new movement (it doesn't exist and its state attribute has value 0 [more on this in the following paragraph]). The java code to assure this condition is the following:
/**
*
* Before a new Movement is saved, change the state of all books
*
* @param obj The Movement instance
* @param event The triggered event
* @return True if the movement can be saved
*
* @throws boRuntimeException If an error occurs while updating
* a book
*/
public boolean onBeforeSave(boObject obj,boEvent event) throws boRuntimeException {
//Only execute this action
if (!obj.exists()
&& obj.getAttribute("state").getValueString().equals("0")) {
//Retrieve the collection attribute and put the handler in
//the first position
bridgeHandler books = obj.getBridge("books");
books.beforeFirst();
//Iterate through all books and change state to 1 (see Lov values from LIB_Lov)
while (books.next()) {
boObject currentBook = books.getObject();
currentBook.getAttribute("state").setValueString("1");
}
}
//Return true so that the
return true;
}
Note: There's no call to the update() method on each boObject representing a book. Whenever the owner of a collection attribute is saved, all elements in the collection are saved as well, and since this code is executed right before the save of the movement instance, when save actually occurs, all books will be updated.
Default State for the movement
The
state attribute will have a default value of "Open", which means when a movement is created it's always because the user has loaned some books from the library. The "Open" state is the label corresponding (in the
movement_type lov) to the "0" value, so in order to have that as a default value, go to the
state attribute, find the "Default Value" section, choose "BOL" and type in "0" (without quotes).
Locking all attributes of an existing movement - onAfterLoad
Whenever an existing LIB_Movement instance is opened you want to lock all attributes so that they cannot be changed. The only thing that can be done in a movement is return books (which will be done later in the documentation due to the dependency on book selection from the visual layer). Add the
onAfterLoad event to the LIB_Movement Object Model and choose Java as the language and press edit to let XEO Studio create another method within the existing Java class handler. The code for the event is the following:
/**
*
* Disables all attributes of an existing movement
*
* @param obj The instance movement
* @param event The event
*
* @throws boRuntimeException If a problem occurs while disabling the attributes
*/
public void onAfterLoad(boObject obj,boEvent event) throws boRuntimeException {
//Only apply the condition if the object is saved
if (obj.exists()) {
//Get an enumeration of all attributes
Enumeration atts = obj.getAllAttributes().elements();
//Iterate through all of them and disable them
while (atts.hasMoreElements()) {
AttributeHandler currAtt = (AttributeHandler) atts.nextElement();
currAtt.setDisabled();
}
}
}
Collection Attribute restrictions (Filtering)
When adding a new collection attribute to an Object Model one has to choose the type of XEO Model instances which are allowed in the collection. You can choose a single type of instance (from one Object Model) or a set of different types. Business Logic, however, can dictate that not all instances from a given type are applicable and XEO provides a feature to allow filtering of instances.
Please note that this feature only has an effect when using XEO's Web Component Layer, i.e. if you create a filter on a collection and use the API to add an instance that is of a valid Object Model for that collection but does not match the rules of the filter, nothing is going to prevent it to be added. Filters are applied when listing instances in a lookup viewer. Most of the times, this is all that's needed as all interaction with the application is done through the Web layer but XEO provides mechanisms to allow you verify (programatically) that an instance added to a collection matches your filter (more on that in the following paragraphs)
In the
books attribute of the LIB_Movement Object Model the only books that should be added are ones which are not currently loaned to a user (i.e. the state of the book is "Available", whose value is "0" as defined in
book_state lov). In order to create a filter you'll need to know a bit about XEOQL (also known as BOQL,
not BOL).
XEO QL Basic Introduction
XEO Query Language (or BOQL - Business Object Query Language) is a small SQL-based querying language in order to select a set of XEO Model instances. XEOQL allows to select a set of instances while being able to set conditions on that search (just like SQL), let's see an example:
select LIB_Book
The previous query selects all instances of the LIB_Book Object Model, let's see another example with some conditions.
select LIB_Book where title = 'Angels and Demons'
The previous query selects all LIB_Book instances which have a title equal to "Angels and Demons", see the next example for a demonstration of querying using relations between
select LIB_Book.categories where LIB_Book.BOUI = 1111
The previous query selects all Book Categories for the Book instance with BOUI equal to 1111
To know more about XEOQL read the
XEOQL chapter. For now let's continue with the filter.
Going back to the LIB_Movement filter (and using the knowledge from XEO QL), the filter you want to apply can be described as "all book instances whose attribute state is available", translating to XEOQL would be "
select LIB_Book where state = '0' ", to add an Object filter, select the "books" attribute in XEO Studio, scroll to the "Object Filters" section and press "add". A new window will open, in the XEOQL filed type the expression " select LIB_Book where state = '0' " (without quotes), see figure OB.4 for details.
Figure OB.4 - Adding an Object Filter to the books attribute collection
Default Value for DueDate
The due date for a return of the books is seven days from the initial loan, as such you're going to add a default value for the
dueDate attribute in the form of Java code. Go to the "Default Value" section of the
dueDate attribute and select Java and press edit. In the Java editor, type the following code:
/**
*
* Default value for dueDate attribute, which is seven days from the current
* day
*
* @param obj The instance movement
* @param attribute The attribute handler for dueDate
*
* @return A string with the date seven days from today
*
* @throws boRuntimeException
*/
public String dueDate_DefaultValue(boObject obj,AttributeHandler attribute) throws boRuntimeException {
//Retrieve a calendar for current time
Calendar dueDate = Calendar.getInstance();
//Add seven days
dueDate.add(Calendar.DATE, 7);
//Return the date
return new Date(dueDate.getTimeInMillis()).toString();
}
This finishes our tour in the XEO Library Behavior. Next we'll go to the Web Components Layer (in the mean time we invite you to read more on the Java API, BOQL or BOL)
In the next chapter you'll
start creating the XEO Web Viewers for the Library Application.
____