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 development6. 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

Table of contents

Lesson 24: Refining the standard behavior
Custom actions
Typical controller
Refining the controller for a module
Writing your own action
Generic actions
MapFacade for generic code
Changing the default controller for all modules
Come back to the model a little
Metadata for more generic code
Call another action from an action actions
Refining the default search action
List mode
Filtering tabular data
List actions
Reusing actions code
Using properties to create reusable actions
Custom modules
Several tabular data definitions by entity
Reusable obsession
Summary
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.
refining_behavior_en010.png
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:
refining_behavior_en020.png
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.
refining_behavior_en030.png
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.
refining_behavior_en040.png
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.
refining_behavior_en050.png
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.
refining_behavior_en060.png

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.

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 25