openxava / documentation / Lesson 24: Refining the standard behavior
Course:
1. Getting started |
2. Basic domain model (1) |
3. Basic domain model (2) |
4. Refining the user interface |
5. Agile development |
6. Mapped superclass inheritance |
7. Entity inheritance |
8. View inheritance |
9. Java properties |
10. Calculated properties |
11. @DefaultValueCalculator in collections |
12. @Calculation and collections totals |
13. @DefaultValueCalculator from file |
14. Manual schema evolution |
15. Multi user default value calculation |
16. Synchronize persistent and computed propierties |
17. Logic from database |
18. Validating with @EntityValidator |
19. Validation alternatives |
20. Validation on remove |
21. Custom Bean Validation annotation |
22. REST service call from validation |
23. Attributes in annotations |
24. Refining the standard behavior |
25. Behavior & business logic |
26. References & collections |
A. Architecture & philosophy |
B. Java Persistence API |
C. Annotations |
D. Automated testing
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 and you can find it in
xava folder from github. 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
src/main/resources/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.actions.*;
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
src/main/resources/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
boolean 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
src/main/resources/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")
//boolean 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.*;
import lombok.*;
@MappedSuperclass @Getter @Setter
public class Deletable extends Identifiable {
@Hidden
@Column(columnDefinition="BOOLEAN DEFAULT FALSE")
boolean 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 "deleted" 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
src/main/resources/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 org.openxava.actions.*;
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 finished
}
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 (javax.validation.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 ... {
...
@Getter @Setter
boolean restore; // A new restore property
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.