openxava / documentation
/ Lesson 7: Refining the standard behavior
Course:
1.
Getting started |
2.
Modeling with Java |
3.
Automated testing |
4.
Inheritance |
5.
Basic business logic |
6.
Advanced validation |
7. Refining the standard behavior
|
8. Behavior
& business logic |
9.
References & collections |
A.
Architecture & philosophy |
B.
Java Persistence API |
C.
Annotations
I hope you are very happy with the current code of your
Invoicing
application. It's really simple, you basically have entities, simple
classes that model your problem. All the business logic is attached to
those entities, and OpenXava generates an application with a decent
behavior around them.
However, man shall not live by business logic alone. A good behavior is
desirable too. Inevitably, you will face that either you, or your user,
crave behavior deviating from, or adding to, the standard OpenXava
behavior for certain parts of your application. To make sure your
application is comfortable and intuitive for the end user refinement of
the standard behavior is needed.
Application behavior is defined by the controllers. A controller is simply
a collection of actions, and an action is what executes when a user clicks
a link or button. You can define your own controllers and actions and
associate them to modules or entities, and in that way refine how OpenXava
behaves.
In this lesson we'll refine the standard controllers and actions in order
to customize the behavior of your
Invoicing application.
Custom
actions
By default, an OpenXava module allows you to manage your entity in a way
powerful and adequate enough to perform the most common tasks: adding,
modifying, removing, searching, generating PDF reports, exporting to Excel
(CSV), and import data to entities. These default actions are contained in
the
Typical controller. You can refine or enhance the behavior
of your module by defining your own controller. This section will teach
you how to define your own controller and write custom actions.
Typical
controller
By default the
Invoice module uses the actions from
Typical
controller. This controller is defined in the
default-controllers.xml
located in the
OpenXava/xava folder of your workspace. A
controller definition is an XML fragment with a relation of actions.
OpenXava applies the
Typical controller by default to all
modules. You can see its definition:
<controller name="Typical"> <!-- 'Typical' inherits all actions -->
<extends controller="Navigation"/> <!-- from 'Navigation', -->
<extends controller="CRUD"/> <!-- from 'CRUD' -->
<extends controller="Print"/> <!-- from 'Print' -->
<extends controller="ImportData"/> <!-- and from 'ImportData' controllers -->
</controller>
Here you can see how a controller can be defined from other
controllers. This is controllers inheritance. In this case the
Typical
controller gets all the actions from the
Navigation,
Print
and
CRUD controllers.
Navigation contains the action
to navigate for object in detail mode.
Print contains the
actions for printing PDF reports as well as exporting to Excel.
CRUD
is composed of the actions for creating, reading, updating and deleting.
ImportData
has the action that allows loading a file, with table format (csv, xls,
xlsx), to import records into the module. An extract of the
CRUD
controller:
<controller name="CRUD">
<action name="new"
class="org.openxava.actions.NewAction"
image="new.gif"
icon="library-plus"
keystroke="Control N"
loses-changed-data="true">
<!--
name="new": Name to reference the action from other parts of the application
class="org.openxava.actions.NewAction" : Class with the action logic
image="images/new.gif": Image to show for this action,
in case "useIconsInsteadOfImages = false" of "xava.properties"
icon="library-plus": Icon to show for this action, this is by default
keystroke="Control N": Keys that the user can press to execute this action
loses-changed-data="true": If the user clicks on this action without saving first
the current data will be lose
-->
<set property="restoreModel" value="true"/> <!-- The restoreModel property of the action class will
be set to true before it's executed -->
</action>
<action name="save" mode="detail"
by-default="if-possible"
class="org.openxava.actions.SaveAction"
image="save.gif"
icon="content-save"
keystroke="Control S"/>
<!--
mode="detail": This action will be shown only in detail mode
by-default=”if-possible”: This action will be executed when the user press ENTER
-->
<action name="delete" mode="detail"
confirm="true"
class="org.openxava.actions.DeleteAction"
image="delete.gif"
icon="delete"
available-on-new="false"
keystroke="Control D"/>
<!--
confirm="true" : Ask the user for confirmation before executing the action
available-on-new="false" : action is not available when the user is creating a new entity
-->
<!-- More actions... -->
</controller>
Here you see how actions are defined. Basically it amounts to
linking a name with a class holding the logic to execute. Moreover, it
defines an image and a keystroke, and using
<set /> you
can configure your action class.
The actions are shown by default in both list and detail mode, although
you can, by means of
mode attribute, specify that the action
should be shown only in “list” or “detail” mode.
Refining
the controller for a module
To start of, we will refine the
delete action of the
Invoice
module. Our objective is to modify the delete procedure so that when a
user clicks on the
delete button the invoice will not be removed but instead simply marked as
removed. This way, we can recover the deleted invoices if needed.

The previous figure shows the actions from
Typical. We want all
of these actions in our
Invoice module, except that we're going
to write our own logic for the delete action.
You have to define your own controller for
Invoice in
controllers.xml
file of the
xava folder of your project, leaving it in this way:
<?xml version = "1.0" encoding = "ISO-8859-1"?>
<!DOCTYPE controllers SYSTEM "dtds/controllers.dtd">
<controllers>
<controller name="Invoice"> <!-- The same name as the entity -->
<extends controller="Typical"/> <!-- It has all the actions from Typical -->
<!-- Typical already has a 'delete' action, by using the same name we override it -->
<action name="delete"
mode="detail" confirm="true"
class="com.yourcompany.invoicing.actions.DeleteInvoiceAction"
icon="delete"
available-on-new="false"
keystroke="Control D"/>
</controller>
</controllers>
In order to define a controller for your entity, you have to
create a controller with the same name as the entity, i.e., if a
controller named “Invoice” exists, this controller will be used instead of
Typical when you run the
Invoice module. The
Invoice
controller extends
Typical, thus all actions from
Typical
are available for your
Invoice module. Any action you define in
your
Invoice controller will be a button available for the user
to click. But since we named our action “delete”, which is shared with an
action of the
Typical controller, we override the action defined
in
Typical. In other words, only our new
delete action
will be present in the user interface.
Writing
your own action
First, create a new package called
com.yourcompany.invoicing.actions.
Then create a
DeleteInvoiceAction class inside it, with this code:
package com.yourcompany.invoicing.actions; // In 'actions' package
import org.openxava.jpa.*;
import org.openxava.actions.*;
import com.yourcompany.invoicing.model.*;
public class DeleteInvoiceAction
extends ViewBaseAction { // ViewBaseAction has getView(), addMessage(), etc
public void execute() throws Exception { // The logic of the action
addMessage( // Add a message to show to the user
"Don't worry! I only have cleared the screen");
getView().clear(); // getView() returns the xava_view object
// clear() clears the data in user interface
}
}
An action is a simple class. It has an
execute() method
with the logic to perform when users click on the corresponding button or
link. An action must implement
org.openxava.actions.IAction
interface, though usually it is more practical to extend
BaseAction,
ViewBaseAction or any other base action from the
org.openxava.actions
package.
ViewBaseAction has a
view property that you can use from
inside
execute() with
getView(). This object of type
org.openxava.view.View
allows you manage the user interface. Above we cleared the displayed data
using
getView().clear().
We also used
addMessage(). All the messages added by
addMessage()
will be shown to the user at the end of action execution. You can either
add the message to show or an id of an entry in
i18n/Invoicing-messages_en.properties.
The following figure demonstrates the behavior of the
Invoice
module after adding your custom delete action:

This is of course mock behavior, so let us now add the real functionality.
In order to mark the current invoice as deleted without actually deleting
it, we need to add a new property to
Invoice. Let's call it
deleted.
You can see it:
@Hidden // It will not be shown by default in views and tabs
@Column(columnDefinition="BOOLEAN DEFAULT FALSE") // To populate with falses instead of nulls
private boolean deleted;
public boolean isDeleted() {
return deleted;
}
public void setDeleted(boolean deleted) {
this.deleted = deleted;
}
As you see, it's a plain boolean property. The only new detail is
the use of the
@Hidden annotation. It means that when a default
view or a tabular list is generated the deleted property will not be
shown; although if you explicitly put it in
@View(members=) or
@Tab(properties=)
it will be shown. Use this annotation for properties intended for internal
use of the developer but makes no sense to the end user.
We use
@Column(columnDefinition=) to populate the column with
falses instead of nulls. Here you put the SQL column definition to send to
the database. It's faster that do an update over the database but the code
is more database dependent.
We are now ready to write the real code for your action:
public void execute() throws Exception {
Invoice invoice = XPersistence.getManager().find(
Invoice.class,
getView().getValue("oid")); // We read the id from the view
invoice.setDeleted(true); // We modify the entity state
addMessage("object_deleted", "Invoice"); // The "deleted" message
getView().clear(); // The view is cleared
}
The visual effect is the same, a message is shown and the view
cleared, but in this case we actually do some work. We find the
Invoice
entity associated with the current view and modify its
deleted
property. That is all you have to do, because OpenXava automatically
commits the JPA transaction. This means that you can read any object and
modify its state from an action, and when the action finishes the modified
values will be stored in the database.
But we left a loose end here. The "delete" button remains in the view
after deleting the entity, that is, when there is no selected object, in
addition if the user clicks the button the search instruction will fail
and a somewhat technical and unintelligible message will be shown to our
helpless user. We can refine this case by not showing the button, such as
when we click the "new" button. Note the small modification of the
execute():
public void execute() throws Exception {
// ...
getView().clear();
getView().setKeyEditable(true); // A new entity
}
With
getView().setKeyEditable(true) we indicate that we
create a new entity and since our delete action has the attribute
available-in-new
= "false", then the delete button will not be displayed.
Now that you know how to write your own custom actions, it's time to learn
how to write generic code.
Generic
actions
The above code for your
DeleteInvoiceAction reflects the typical
way of writing actions. It is concrete code directly accessing and
manipulating your entities.
But sometimes you have action logic potentially to be used and reused
across your application, even across all your applications. In this case,
you can use techniques to produce reusable code, converting your custom
actions into generic actions.
Let's learn these techniques to write generic action code.
MapFacade
for generic code
Imagine that you want to use your
DeleteInvoiceAction for
Order
entities too. Moreover, imagine that you want to use it for all the
entities of the application having a
deleted property. That is,
you want an action to mark as deleted instead of actually removing from
the database any entity, not just invoices. In this case, the current code
for your action is not enough. You need a more generic code.
You can develop more generic actions using an OpenXava class named
MapFacade.
MapFacade (from
org.openxava.model package) allows you to
manage the state of your entities using maps, which is very practical
since
View works with maps. Furthermore, maps are more dynamic
than objects, and hence more suitable for generic code.
Let's rewrite our delete action. First, we'll rename it from
DeleteInvoiceAction
(an action to delete
Invoice objects) to
InvoicingDeleteAction
(the delete action of the
Invoicing application). This implies
that you must alter the class name entry for the action in
controllers.xml:
<action name="delete"
mode="detail" confirm="true"
class="com.yourcompany.invoicing.actions.InvoicingDeleteAction"
icon="delete"
available-on-new="false"
keystroke="Control D"/>
Now, rename your
DeleteInvoiceAction to
InvoicingDeleteAction
and rewrite its code:
package com.yourcompany.invoicing.actions;
import java.util.*;
import org.openxava.actions.*;
import org.openxava.model.*;
public class InvoicingDeleteAction extends ViewBaseAction {
public void execute() throws Exception {
Map<String, Object> values =
new HashMap<>(); // The values to modify in the entity
values.put("deleted", true); // We set true to 'deleted' property
MapFacade.setValues( // Modifies the values of the indicated entity
getModelName(), // A method from ViewBaseAction
getView().getKeyValues(), // The key of the entity to modify
values); // The values to change
resetDescriptionsCache(); // Clears the caches for combos
addMessage("object_deleted", getModelName());
getView().clear();
getView().setKeyEditable(true);
getView().setEditable(false); // The view is left as not editable
}
}
This action executes the same logic as the previous one, but it
contains no reference to the
Invoice entity. This action is
generic and you can use it with
Order,
Author or any
other entity as long they have a
deleted property. The trick
lies in
MapFacade. It allows you to modify an entity from maps.
These maps can be obtained directly from the view (using
getView().getKeyValues()
for example) or created generically as in the case with values above.
Additionally you can see two small improvements over the old version.
First, we call
resetDescriptionsCache(), a method from
BaseAction.
This method clears the cache used for combos. When you modify an entity,
this is needed if you want the combos to reflect the change
instantaneously. Second, we call
getView().setEditable(false).
This disables the editors of the view, to prevent the user from filling in
data in the view. In order to create a new entity the user must click the
“new” button.
Now your action is ready to use with any other entity. We could copy and
paste the
Invoice controller as
Order controller in
controllers.xml.
In this way, our new generic delete logic would be used for
Order
too. Wait a minute! Did I say “copy and paste”? We don't want to rot in
hell, right? So we'll use a more automatic way to apply our new action to
all modules. Let us find out how in the next section.
Changing
the default controller for all modules
If you use the
InvoicingDeleteAction just for
Invoice
then defining it in the
Invoice controller in
controllers.xml
is a good way to go. But, we improved this action with the sole purpose
making it reusable, so let's reuse it. For this we will assign a
controller to all modules at once.
The first step is to change the name of the controller from
Invoice
to
Invoicing in
controllers.xml:
<controller name="Invoicing">
<extends controller="Typical"/>
<action name="delete"
mode="detail" confirm="true"
class="com.yourcompany.invoicing.actions.InvoicingDeleteAction"
icon="delete"
available-on-new="false"
keystroke="Control D"/>
</controller>
As you already know, when you use the name of an entity, like
Invoice,
as controller name, that controller will be used by default for the module
of that entity. Therefore, if we change the name of the controller, this
controller will not be used for the entity. Indeed, the
Invoicing
controller is not used by any module since there is no entity called
Invoicing.
We want to make the
Invoicing controller the default controller
for all the modules in our application. To achieve this we need to modify
the
application.xml file located in the
xava folder of
your application. Leave it as:
<?xml version = "1.0" encoding = "ISO-8859-1"?>
<!DOCTYPE application SYSTEM "dtds/application.dtd">
<application name="Invoicing">
<!--
A default module for each entity is assumed with the
controllers on <default-module/>
-->
<default-module>
<controller name="Invoicing"/>
</default-module>
</application>
In this simple way all modules of your application will use
Invoicing
instead of
Typical as the default controller. Try to execute
your
Invoice module and see how the
InvoicingDeleteAction
is executed on deletion of an element.
You can try the
Order module too, but it will not work because
it has no
deleted property. We could add the
deleted
property to
Order and it would work with our new controller, but
instead of “copying and pasting” the
deleted property to all our
entities we are going to use a better technique. Let's see it in the next
section.
Come
back to the model a little
Now your task is to add the
deleted property to all the entities
of your application, in order to make
InvoicingDeleteAction
work. This is a good occasion to use inheritance to put this shared code
in the same place, instead of using the infamous “copy & paste”.
First remove the
deleted property from
Invoice:
public class Invoice extends CommercialDocument {
//@Hidden // It will not be shown by default in views and tabs
//@Column(columnDefinition="BOOLEAN DEFAULT FALSE")
//private boolean deleted;
//public boolean isDeleted() {
// return deleted;
//}
//public void setDeleted(boolean deleted) {
// this.deleted = deleted;
//}
// ...
}
And now create a new mapped superclass named
Deletable in
com.yourcompany.invoicing.model package:
package com.yourcompany.invoicing.model;
import javax.persistence.*;
import org.openxava.annotations.*;
@MappedSuperclass
public class Deletable extends Identifiable {
@Hidden
@Column(columnDefinition="BOOLEAN DEFAULT FALSE")
private boolean deleted;
public boolean isDeleted() {
return deleted;
}
public void setDeleted(boolean deleted) {
this.deleted = deleted;
}
}
Deletable is a mapped superclass. Remember, a mapped
superclass is not an entity, but a class with properties, methods and
mapping annotations to be used as a superclass for entities.
Deletable
extends from
Identifiable, so any entity that extends
Deletable
will have the
oid and
deleted properties.
Now you can convert any of your current entities to deletable, you only
have to change
Identifiable to
Deletable as
superclass. This is done for
CommercialDocument:
// abstract public class CommercialDocument extends Identifiable {
abstract public class CommercialDocument extends Deletable {
...
Given that
Invoice and
Order are subclasses of
ComercialDocument, now you can use the
Invocing
controller with the
InvoicingDeleteAction against them.
A subtle detail remains. The
Order entity has a
@PreRemove
method to do a validation on remove. This validation can prevent the
removal. We can maintain this validation for our custom deletion by just
overwriting the
setDeleted() method for
Order:
public class Order extends CommercialDocument {
// ...
@PreRemove
private void validateOnRemove() { // Now this method is not executed automatically
if (invoice != null) { // since a real deleletion is not done
throw new javax.validation.ValidationException(
XavaResources.getString(
"cannot_delete_order_with_invoice"));
}
}
public void setDeleted(boolean deleted) {
if (deleted) validateOnRemove(); // We call the validation explicitly
super.setDeleted(deleted);
}
}
With this change the validation works just as in the real deletion
case, thus we preserve untouched the original behavior.
Metadata
for more generic code
With your current code
Invoice and
Order work fine.
Though if you try to remove an entity in any other module, you'll get an
ugly error message. The following figure shows what happens when you try
to delete a
Customer.

If your entity has no
deleted property, the delete action fails.
Using
Deletable as base class you can add the
deleted
property to all your entities. However, you might want to have entities
marked as deleted (so
Deletable) and entities to be actually
removed from the database. We want the action to work in all cases.
OpenXava stores metadata for all entities, and you can access this
metadata from your code. This allows you, among other things, to figure
out if an entity has a
deleted property.
The following code shows how to modify the action to find out if the
entity has a
deleted property, if not, the remove process is not
performed:
public void execute() throws Exception {
if (!getView().getMetaModel() // Metadata about the current entity
.containsMetaProperty("deleted")) // Is there a 'deleted' property?
{
addMessage( // For now, only shows a message if 'deleted' is not present
"Not deleted, it has no deleted property");
return;
}
// ...
}
The key thing here is
getView().getMetaModel() that
returns a
MetaModel object from
org.openxava.model.meta
package. This object contains metadata about the entity currently
displayed in the view. You can ask for the properties, references,
collections, methods and other entity metadata. Consult the
MetaModel
API to learn more. In this case we ask if the
deleted
property exists.
For now we only show a message. Let's improve on this to actually delete
the entity.
Call
another action from an action
Our objective is to remove the entity in the usual way if it lacks the
deleted
property. Our first option is to write the removal logic ourselves, a
rather simple task. Nonetheless, it is a much better idea to use the
standard removal logic of OpenXava, since we don't need to write any
deletion logic and we use a more refined and tested piece of code.
For this OpenXava allows to call an action from inside an action, just
call to
executeAction() indicating the qualified name of your
action, that is the name of the controller and the name of the action. In
our case for calling the standard OpenXava delete action we should use
executeAction("CRUD.delete").
The following code shows
InvoicingDeleteAction modified to call
to the standard OpenXava delete action:
package com.yourcompany.invoicing.actions;
import java.util.*;
import org.openxava.actions.*;
import org.openxava.model.*;
public class InvoicingDeleteAction extends ViewBaseAction {
public void execute() throws Exception {
if (!getView().getMetaModel().containsMetaProperty("deleted")) {
executeAction("CRUD.delete"); // We call the standard OpenXava
return; // action for deleting
}
// When "delete" exists we use our own deletion logic
Map<String, Object> values = new HashMap<>();
values.put("deleted", true);
MapFacade.setValues(getModelName(), getView().getKeyValues(), values);
resetDescriptionsCache();
addMessage("object_deleted", getModelName());
getView().clear();
getView().setKeyEditable(true);
getView().setEditable(false);
}
}
Simple. We call to
executeAction(“CRUD.delete”)
if we want the default delete action of OpenXava to be executed. Thus, we
write our custom removal logic (in this case marking a property as true)
for some cases, and we “by-pass” to the standard logic for all other
cases.
Now you can use your
InvoicingDeleteAction with any entity. If
the entity has a
deleted property it will be marked as deleted,
otherwise it will be actually deleted from the database.
This example shows you how to use
executeAction() to refine the
standard OpenXava logic. Another way to do so is by means of inheritance.
Let's see how in the next section.
Refining
the default search action
Now, your
InvoicingDeleteAction works fine, though it is
useless. It is useless to mark objects as deleted if the rest of the
application is not aware of that. In other words, we have to modify other
parts of the application to treat the marking-as-deleted object as if they
do not exist.
The most obvious place to start is the search action. If you delete an
invoice and then search for it, it should not be found. The following
figure shows how searching works in OpenXava.

The first thing to observe in the previous figure is that searching in
detail mode is more flexible than it seems. The user can enter any value
in any field, or set of fields, and then click the refresh button. The
first matching object will get loaded in the view.
Now you might think: “Well, I can refine the
CRUD.refresh action
just in the same was as I refined
CRUD.delete “. Yes, you can,
and it will work; when the user clicks on the refresh action of detail
mode your code will be executed. However, there is a subtle detail here.
The search logic is not called only from the detail view, but from several
other places in an OpenXava module. For example, when the user chooses a
detail from list mode, the
List.viewDetail action takes the key
from the row, puts it in the detail view, and then executes the search
action.
We put the search logic for a module in one action, and all actions that
need to search will chain to this action. Just as shown in the previous
figure.
This gets clearer when you see the code for the standard
CRUD.refresh
action,
org.openxava.actions.SearchAction whose code is shown
below:
public class SearchAction extends BaseAction
implements IChainAction { // It chains to another action
public void execute() throws Exception { // Do nothing
}
public String getNextAction() throws Exception { // Of IChainAction
return getEnvironment() // To access an environment variables
.getValue("XAVA_SEARCH_ACTION");
}
}
As you can see, the standard search action for detail mode does
nothing except redirect to another action. This other action is defined in
an environment variable called
XAVA_SEARCH_ACTION, and can be
read using
getEnvironment(). Therefore, if you want to refine the
OpenXava search logic, the best way to do it is to define your action as
XAVA_SEARCH_ACTION.
So, let's do it this way.
To set the environment variable, edit the
controllers.xml file
in the
xava folder of your project, and add the
<env-var
/> line at the beginning:
...
<controllers>
<!-- To define the global value for an environment variable -->
<env-var
name="XAVA_SEARCH_ACTION"
value="Invoicing.searchExcludingDeleted" />
<controller name="Invoicing">
...
This way the value of the
XAVA_SEARCH_ACTION environment
variable in any module will be “Invoicing.searchExcludingDeleted”, and
hence, the search logic for all modules will be in this action.
Next, we define this action in
controllers.xml:
<controller name="Invoicing">
...
<action name="searchExcludingDeleted"
hidden="true"
class="com.yourcompany.invoicing.actions.SearchExcludingDeletedAction"/>
<!-- hidden="true": Thus the action will not be shown in button bar -->
</controller>
And now it's time to write the implementation class. In this case
we only want to refine the search logic so that the search is done the
regular way with the exception that it is leaving out entities where
deleted
is set to true. To do this refinement we are going to use inheritance.
package com.yourcompany.invoicing.actions;
import java.util.*;
import javax.ejb.*;
import org.openxava.actions.*;
public class SearchExcludingDeletedAction
extends SearchByViewKeyAction { // The standard OpenXava action to search
private boolean isDeletable() { // To see if this entity has a deleted property
return getView().getMetaModel()
.containsMetaProperty("deleted");
}
protected Map getValuesFromView() // Gets the values displayed in this view
throws Exception // These values are used as keys for the search
{
if (!isDeletable()) { // If not deletable we run the standard logic
return super.getValuesFromView();
}
Map<String, Object> values = super.getValuesFromView();
values.put("deleted", false); // We populate deleted property with false
return values;
}
protected Map getMemberNames() // The members to be read from the entity
throws Exception
{
if (!isDeletable()) { // If not deletable we run the standard logic
return super.getMemberNames();
}
Map<String, Object> members = super.getMemberNames();
members.put("deleted", null); // We want to get the deleted property from entity,
return members; // though it might not be in the view
}
protected void setValuesToView(Map values) // Assigns the values from the
throws Exception // entity to the view
{
if (isDeletable() && // If it has an deleted property and
(Boolean) values.get("deleted")) { // it is true
throw new ObjectNotFoundException(); // The same exception OpenXava
} // throws when the object is not found
else {
super.setValuesToView(values); // Otherwise we run the standard logic
}
}
}
The standard logic to search resides in the
SearchByViewKeyAction
class. Basically, what this action does is that it reads the values from
the view and searches for an object. If the id property is present this is
used, otherwise all values are used, and the first object that matches the
condition is returned. We want to use this same algorithm changing only
some details about the
deleted property. So instead of
overwriting the
execute() method, that contains the search logic,
we overwrite three protected methods, that are called from
execute()
and contain some parts suitable to be refined.
After these changes you can try your application, and you will find that
when you search, a removed invoice or order will never be shown. Even if
you choose a deleted invoice or order from list mode an error will be
produced and you will not see the data in detail mode.
You have seen how defining a
XAVA_SEARCH_ACTION environment
variable in
controllers.xml you establish the search logic in a
global way for all your modules at once. If you want to define a specific
search action for a particular module just define the environment variable
in the module definition in
application.xml, as we show below:
<module name="Product">
<!-- To give a local value to the environment variable for this module -->
<env-var
name="XAVA_SEARCH_ACTION"
value="Product.searchByNumber"/>
<model name="Product"/>
<controller name="Product"/>
<controller name="Invoicing"/>
</module>
In this way for the module
Product the
XAVA_SEARCH_ACTION
environment variable will be “Product.searchByNumber”. This way, the
environment variables are local to the modules. You can define a default
value in
controllers.xml, but you always have the option to
overwrite the value for particular modules. The environment variables are
a powerful way to configure your application in an declarative way.
We don't want a special way to search for
Product, so don't add
this module definition to your
application.xml. The code here is
only to illustrate how to use
<env-var /> in modules.
List
mode
We have almost completed the job. When the user removes an entity with a
deleted
property the entity is marked as deleted instead of actually being removed
from the database. And if the user attempts to search for a
marked-as-deleted entity he cannot view it in detail mode. However, the
user can still see such entities in list mode though, and even worse, if
he deletes entities when in list mode, they are actually removed from the
database. Let's fix these remaining details.
Filtering
tabular data
Only entities where the deleted property is false should be shown in list
mode. This is very easy to achieve using the
@Tab annotation. This
annotation allows you to define the way the tabular data (the data shown
in list mode) is displayed, moreover it allows you to define a condition.
So, adding this annotation to the entities with
deleted property
is sufficient to achieve our goal, as we show below:
@Tab(baseCondition = "deleted = false")
public class Invoice extends CommercialDocument { ... }
@Tab(baseCondition = "deleted = false")
public class Order extends CommercialDocument { ... }
And in this simple way the list mode will not show the
marked-as-deleted entities.
List
actions
The only remaining detail is that when removing entities from list mode,
they should be marked as deleted if applicable. We are going to refine the
standard
CRUD.deleteSelected and
CRUD.deleteRow actions
in the same way we used for
CRUD.delete.
First, we overwrite the
deleteSelected and
deleteRow
actions for our application. Add the following action definition to your
Invoicing
controller defined in
controllers.xml:
<controller name="Invoicing">
<extends controller="Typical"/>
...
<action name="deleteSelected" mode="list" confirm="true"
process-selected-items="true"
icon="delete"
class="com.yourcompany.invoicing.actions.InvoicingDeleteSelectedAction"
keystroke="Control D"/>
<action name="deleteRow" mode="NONE" confirm="true"
class="com.yourcompany.invoicing.actions.InvoicingDeleteSelectedAction"
icon="delete"
in-each-row="true"/>
</controller>
The standard actions to remove entities from list mode are
deleteSelected
(to delete the checked rows) and
deleteRow (the action displayed
in each row). These actions are defined in the
CRUD controller.
Typical extends
CRUD, and
Invoicing extends
Typical;
so
Invoicing controller includes these actions by default. Given
we have defined these actions with the same name, our actions overwrite
the standard ones. That is, from now on the logic for removing selected
rows in list mode is in the
InvoicingDeleteSelectedAction class.
Note how the logic for both actions is in the same Java class:
package com.yourcompany.invoicing.actions;
import java.util.*;
import javax.validation.*;
import org.openxava.actions.*;
import org.openxava.model.*;
import org.openxava.model.meta.*;
public class InvoicingDeleteSelectedAction
extends TabBaseAction // To work with tabular data (list) by means of getTab()
implements IChainActionWithArgv { // It chains to another action, returned by getNextAction() method
private String nextAction = null; // To store the next action to execute
public void execute() throws Exception {
if (!getMetaModel().containsMetaProperty("deleted")) {
nextAction="CRUD.deleteSelected"; // 'CRUD.deleteSelected' will be
return; // executed after this action is finisreturn;
}
markSelectedEntitiesAsDeleted(); // The logic to mark the selected rows
// as deleted objects
}
private MetaModel getMetaModel() {
return MetaModel.get(getTab().getModelName());
}
public String getNextAction() // Required because of IChainAction
throws Exception
{
return nextAction; // If null no action will be chained
}
public String getNextActionArgv() throws Exception {
return "row=" + getRow(); // Argument to send to chainged action
}
private void markSelectedEntitiesAsDeleted() throws Exception {
// ...
}
}
You can see that this action is very similar to
InvoicingDeleteAction.
If the selected entities don't have the deleted property it chains to the
standard action, otherwise it executes its own logic to delete the
entities. Although in this case we use
IChainActionWithArgv
instead of a simpler
executeAction() because we need to send an
argument to the chained action. Usually the actions for list mode extend
TabBaseAction,
thus you can use
getTab() to obtain the
Tab object
associated to the list. A
Tab (from
org.openxava.tab)
allows you to manage the tabular data. For example in the
getMetaModel()
method we retrieve the model name from the
Tab in order to obtain
the corresponding
MetaModel.
If the entity has a
deleted property then our custom delete
logic is executed. This logic is in the
markSelectedEntitiesAsDeleted()
method:
private void markSelectedEntitiesAsDeleted() throws Exception {
Map<String, Object> values = new HashMap<>(); // Values to assign to each entity to be marked
values.put("deleted", true); // Just set deleted to true
Map<String, Object>[] selectedOnes = getSelectedKeys(); // We get the selected rows
if (selectedOnes != null) {
for (int i = 0; i < selectedOnes.length; i++) { // Loop over all selected rows
Map<String, Object> key = selectedOnes[i]; // We obtain the key of each entity
try {
MapFacade.setValues( // Each entity is modified
getTab().getModelName(),
key,
values);
}
catch (ValidationException ex) { // If there is a ValidationException...
addError("no_delete_row", i, key);
addError("remove_error", getTab().getModelName(), ex.getMessage()); // ...we show the message
}
catch (Exception ex) { // If any other exception is thrown, a generic
addError("no_delete_row", i, key); // message is added
}
}
}
getTab().deselectAll(); // After removing we deselect the rows
resetDescriptionsCache(); // And reset the cache for combos for this user
}
As you see the logic is a plain loop over all selected rows, in
each iteration we set the
deleted property to true using
MapFacade.setValues().
Exceptions are caught inside the loop, so if there is a problem deleting
one of the entities, this does not affect the removal of the other
entities. Furthermore, we have added a small refinement for the
ValidationException
case, adding the validation message
(ex.getMessage()) to the
errors to be shown to the user.
Here we also deselect all rows by means of
getTab().deselectAll(),
because we are removing rows. If we don't remove the selection, it would
be moved after the action execution.
We call
resetDescriptionsCache() to update all combos in the
current user session and remove all deleted entities. The combos
(references marked with
@DescriptionsList) use the
@Tab of
the referenced entity to filter the data. In our case all the combos for
invoices and orders use the “deleted = false” condition of the
@Tab.
So, it's needed to reset the combos cache.
Now you have thoroughly improved the way your application deletes the
entities. Nevertheless, there still remains a few interesting things we
can do with this.
Reusing
actions code
Fantastic! Now your application marks the invoices and orders as deleted
instead of actually removing them. One advantage of this approach is that
in any given moment, the user can restore an invoice or order deleted by
mistake. For this feature to be useful, you have to provide a tool for
restoring the deleted entities. Therefore, we are going to create an
Invoice
trash and an
Order trash to bring deleted documents back
to life.
Using
properties to create reusable actions
The trash we want is exhibited in the next figure. It is a list of
invoices or orders where the user can choose several of them and click the
Restore button, or alternatively, for a single entity, click the
Restore
link on the row of the item to restore.

The logic of this restore action is to set the
deleted property
of the selected entities to false. Obviously, this is the same logic we
used for deleting, but setting deleted to false instead of true. Since our
conscience does not allow us to copy and paste, we're going to reuse the
present code. Just adding a
restore property to
InvoicingDeleteSelectedAction
is enough to enable recovery of deleted entities.
public class InvoicingDeleteSelectedAction ... {
...
private boolean restore; // A new restore property...
public boolean isRestore() { // ...with its getter
return restore;
}
public void setRestore(boolean restore) { // ...and setter
this.restore = restore;
}
private void markSelectedEntitiesAsDeleted()
throws Exception
{
Map<String, Object> values = new HashMap<String, Object>();
// values.put("deleted", true); // Instead of a hardcoded true, we use
values.put("deleted", !isRestore()); // the restore property value
// ...
}
// ...
}
As you can see, we only added a
restore property, and
then use its complement as the new value for the
deleted
property in the entity. If restore is false, (the default),
deleted
is set to
true, meaning that we perform a deletion. But if
restore
is true the action will save the
deleted property as
false,
making the invoice, order or whatever entity available again in the
application.
To make this action available you have to define it in
controllers.xml:
<controller name="Trash">
<action name="restore" mode="list"
class="com.yourcompany.invoicing.actions.InvoicingDeleteSelectedAction">
<set property="restore" value="true"/> <!-- Initialize the restore property to -->
<!-- true before calling the execute() method of the action -->
</action>
</controller>
From now on you can utilize the
Trash.restore action
when you need an action for restoring. You're reusing the same code to
delete and to restore, thanks to
<set /> element of
<action
/> that allows you to configure the properties of your action.
Let's use this new restore action in the new trash modules.
Custom
modules
As you already know, OpenXava generates a default module for each entity
in your application. Still, you always have the option of defining the
modules by hand, either by refining the behavior of a certain entity
module, or by defining completely new functionality associated to the
entity. In this case we're going to create two new modules,
InvoiceTrash
and
OrderTrash, to restore deleted documents. In these modules
we will use the
Trash controller. See how we define the module
in the
application.xml file:
<application name="Invoicing">
<default-module>
<controller name="Invoicing"/>
</default-module>
<module name="InvoiceTrash">
<env-var name="XAVA_LIST_ACTION"
value="Trash.restore"/> <!-- The action to be shown in each row -->
<model name="Invoice"/>
<tab name="Deleted"/> <!-- To show only the deleted entities -->
<controller name="Trash"/> <!-- With only one action: restore -->
</module>
<module name="OrderTrash">
<env-var name="XAVA_LIST_ACTION" value="Trash.restore"/>
<model name="Order"/>
<tab name="Deleted"/>
<controller name="Trash"/>
</module>
</application>
These modules work as
Invoice and
Order, but
they define a specific action as a row action using the
XAVA_LIST_ACTION
environment variable. The next figure shows the
InvoiceTrash
module in action.
Several
tabular data definitions by entity
Another important detail is that only deleted entities are shown in the
list. This is possible because we define a specific
@Tab by name
for the module.
<module ... >
...
<tab name="Deleted"/> <!-- "Deleted" is a @Tab defined in the entity -->
...
</module>
Of course you must have a
@Tab named “Deleted” in your
Order
and
Invoice entities.
@Tab(baseCondition = "deleted = false") // No name, so the default tab
@Tab(name="Deleted", baseCondition = "deleted = true") // A named tab
public class Invoice extends CommercialDocument { ... }
@Tab(baseCondition = "deleted = false")
@Tab(name="Deleted", baseCondition = "deleted = true")
public class Order extends CommercialDocument { ... }
You use the
@Tab with no name as default list for
Invoice
and
Order, but you have a
@Tab named "Deleted" you can
use for generating a list with only the deleted rows. In our context you
use it for the trash modules. Now you can try your new trash modules, if
you don't see it in the menu, sign out and sign in again.
Reusable
obsession
We have done a good job! The
InvoicingDeleteSelectedAction code
can delete and restore entities, and we added the restore capacity with a
very small amount of code and without copy & paste.
Now a lot of pernicious thoughts are swarming around your head. Surely you
are thinking: “This action is no longer a mere delete action, but rather a
delete and restore action”, and then: “Wait a minute, it is actually an
action that updates the deleted property of the current entity”, followed
by your next thought: “With very little improvement we can update any
property of the entity”.
Yes, you're right. You can easily create a more generic action, an
UpdatePropertyAction
for example; and use it to declare your
deleteSelected and
restore actions, thereby:
<action name="deleteSelected" mode="list" confirm="true"
class="com.yourcompany.invoicing.actions.UpdatePropertyAction"
keystroke="Control D">
<set property="property" value="deleted"/>
<set property="value" value="true"/>
</action>
<action name="restore" mode="list"
class="com.yourcompany.invoicing.actions.UpdatePropertyAction">
<set property="property" value="deleted"/>
<set property="value" value="false"/>
</action>
Although it seems like a good idea, we're not going to create this
flexible
UpdatePropertyAction. Because the more flexible your
code is, the more sophisticated it is. And we don't want sophisticated
code. We want simple code, and though simple code is impossible to
achieve, we must make an effort to have the simplest possible code. The
advice is: create reusable code only when it simplifies your application
right now.
JUnit
tests
We have refined the way your application deletes entities, moreover we
added two custom modules, the trash modules. Before we move ahead, we must
write the tests for the new features.
Testing
the customized delete behavior
We do not have to write new tests for the deletion, because the current
testing code already tests the delete functionality. Usually, when you
change the implementation of some functionality but not their use from the
user viewpoint, as in our current case, you do not need to add new tests.
Just run all the tests for your application, and adjust the needed details
in order for them to work properly. In fact you only need to change
“CRUD.delete” to “Invoicing.delete” and “CRUD.deleteSelected” to
“Invoicing.deleteSelected” in a few tests. The next code summarizes the
changes you need to apply to your current test code:
// In CustomerTest.java file
public class CustomerTest extends ModuleTestBase {
...
public void testCreateReadUpdateDelete() throws Exception {
...
// Delete
// execute("CRUD.delete");
execute("Invoicing.delete");
assertMessage("Customer deleted successfully");
}
...
}
// In CommercialDocumentTest.java file
abstract public class CommercialDocumentTest extends ModuleTestBase {
...
private void remove() throws Exception {
// execute("CRUD.delete");
execute("Invoicing.delete");
assertNoErrors();
}
...
}
// In ProductTest.java file
public class ProductTest extends ModuleTestBase {
...
public void testRemoveFromList() throws Exception {
...
// execute("CRUD.deleteSelected");
execute("Invoicing.deleteSelected");
...
}
...
}
// In OrderTest.java file
public class OrderTest extends CommercialDocumentTest {
...
public void testSetInvoice() throws Exception {
...
//execute("CRUD.deleteRow", "row=0");
execute("Invoicing.deleteRow", "row=0");
...
}
}
After these changes all your tests will work properly, and this
verifies that your refined delete actions preserve the original semantics.
Only the implementation has changed.
Testing
several modules in the same test method
You also have to test the new custom modules,
OrderTrash and
InvoiceTrash.
Additionally, we will also verify that the removal logic works fine, and
that the entities are only marked as deleted and not actually removed.
To test the InvoiceTrash module we will follow the steps below:
- Start in the Invoice module.
- Delete an invoice from detail mode and verify that it has been
deleted.
- Delete an invoice from list mode and verify that it has been
deleted.
- Go to the InvoiceTrash module.
- Verify that it contains the two deleted invoices.
- Restore them and verify that they disappear from the trash list.
- Come back to the Invoice module.
- Verify that the two restored invoices are in the list.
You can see that we start in the
Invoice module. Moreover, you
surely have realized that the logic to test the
Order module is
exactly the same. So instead of creating the new test classes,
OrderTrashTest
and
InvoiceTrashTest, we'll just add the test in the already
existing
CommercialDocumentTest. Thus we reuse the same code for
testing
OrderTrash,
InvoiceTrash, and the custom
delete logic. This code is in the
testTrash() method:
public void testTrash() throws Exception {
login("admin", "admin");
assertListOnlyOnePage(); // Only one page in the list, that is less than 10 rows
execute("List.orderBy", "property=number"); // We order the list by number
// Deleting from detail mode
int initialRowCount = getListRowCount();
String year1 = getValueInList(0, 0);
String number1 = getValueInList(0, 1);
execute("List.viewDetail", "row=0");
execute("Invoicing.delete");
execute("Mode.list");
assertListRowCount(initialRowCount - 1); // There is one row less
assertDocumentNotInList(year1, number1); // The deleted entity is not in the list
// Deleting from list mode
String year2 = getValueInList(0, 0);
String number2 = getValueInList(0, 1);
checkRow(0);
execute("Invoicing.deleteSelected");
assertListRowCount(initialRowCount - 2); // There are 2 fewer rows
assertDocumentNotInList(year2, number2); // The other deleted entity is not in the list now
// Verifying deleted entities are in the trash module
changeModule(model + "Trash"); // model can be 'Invoice' or 'Order'
assertListOnlyOnePage();
int initialTrashRowCount = getListRowCount();
assertDocumentInList(year1, number1); // We verify that the deleted entities
assertDocumentInList(year2, number2); // are in the trash module list
// Restoring using row action
int row1 = getDocumentRowInList(year1, number1);
execute("Trash.restore", "row=" + row1);
assertListRowCount(initialTrashRowCount - 1); // 1 row less after restoring
assertDocumentNotInList(year1, number1); // The restored entity is no longer shown
// in the trash module list
// Restoring by checking a row and using the bottom button
int row2 = getDocumentRowInList(year2, number2);
checkRow(row2);
execute("Trash.restore");
assertListRowCount(initialTrashRowCount - 2); // 2 fewer rows
assertDocumentNotInList(year2, number2); // The restored entity is no longer shown
// in the trash module list
// Verifying entities are restored
changeModule(model);
assertListRowCount(initialRowCount); // After restoring we have the original
assertDocumentInList(year1, number1); // rows again
assertDocumentInList(year2, number2);
}
As you see the
testTrash() follows the aforesaid steps.
Note how using the
changeModule() method from
ModuleTestBase
your test can change to another module. We use it to switch to the trash
module, and then come back.
In our tests we're using a few auxiliary methods you have to add to
CommercialDocumentTest.
The first is
assertListOnlyOnePage() to assert that the list is
adequate for running this test.
private void assertListOnlyOnePage() throws Exception {
assertListNotEmpty(); // This is from ModuleTestBase
assertTrue("Must be less than 10 rows to run this test",
getListRowCount() < 10);
}
We need to have less than 10 rows, because the
getListRowCount()
method only informs about rows that are displayed. If you have more than
10 rows (10 is the default row count per page) you cannot take advantage
of
getListRowCount(), it would always return 10.
The remaining methods are for verifying if some order or invoice is (or is
not) in the list. See them:
private void assertDocumentNotInList(String year, String number)
throws Exception
{
assertTrue(
"Document " + year + "/" + number +" must not be in list",
getDocumentRowInList(year, number) < 0);
}
private void assertDocumentInList(String year, String number)
throws Exception
{
assertTrue(
"Document " + year + "/" + number + " must be in list",
getDocumentRowInList(year, number) >= 0);
}
private int getDocumentRowInList(String year, String number)
throws Exception
{
int c = getListRowCount();
for (int i=0; i<c; i++) {
if ( year.equals(getValueInList(i, 0)) &&
number.equals(getValueInList(i, 1)))
{
return i;
}
}
return -1;
}
You can see in
getDocumentRowInList() how to create a
loop for searching after a specific value in a list.
Now you can run all the tests for your
Invoicing application.
Everything should be green.
Summary
The standard OpenXava behavior is only a starting point. Using the delete
actions as excuse we have explored a couple of ways to refine the details
of the application behavior. With the techniques in this lesson you can
not only refine the delete logic, but fully alter the way your OpenXava
application works. Thus, you have the possibility of tailoring the
behavior of your application to fit your user expectations.
The default OpenXava behavior is pretty limited, only CRUD and reporting.
If you want a valuable application for your user you need to add specific
functionality to help your user solve his problems. We will do more of
that in the next lesson.
Download source code of this lesson
Any problem with this lesson? Ask in the forum Everything fine?
Go to Lesson
8