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. Advanced validation |
19. Refining the standard
behavior |
20. Behavior & business logic |
21. References
& collections |
A.
Architecture & philosophy |
B.
Java Persistence API |
C.
Annotations |
D.
Automated testing
OpenXava is not just a CRUD framework, but a framework for developing
full-fledged business applications. Until now we have learned how to
create and enhance a data management application. We will now improve the
application further by giving the user the possibility to perform specific
business logic.
In this lesson we'll see how to add business logic to a model and call
this logic from custom actions. In this way you can transform a database
management application into a useful tool for the everyday work of your
user.
Business logic in detail
mode
We'll start with the simplest case: a button in the detail mode that
executes some concrete logic. In this case we'll add a button for creating
an invoice from an order:

This shows how this new action takes the current order and creates an
invoice from it. It just copies all the order data to the new invoice,
including the detail lines. A message is shown and the INVOICE tab of the
order will display the recently created invoice. Let's see how to
implement this.
Creating an action
for custom logic
As you already know the first step towards having a custom action in your
module is defining a controller for that action. Let's edit
controllers.xml, to add such a controller. Here you have the
Order
controller definition:
<controller name="Order">
<extends controller="Invoicing"/> <!-- In order to have the standard actions -->
<action name="createInvoice" mode="detail"
class="com.yourcompany.invoicing.actions.CreateInvoiceFromOrderAction"/>
<!-- mode="detail" : Only in detail mode -->
</controller>
Since we follow the convention of giving the controller the same
name as the entity and the module, you automatically have this new action
available for
Order.
Order controller extends
Invoicing
controller. Remember that we created
Invoicing controller in the
previous lesson. It is a refinement of the
Typical controller.
Now we have to write the Java code for the action:
package com.yourcompany.invoicing.actions; // In 'actions' package
import org.openxava.actions.*;
import com.yourcompany.invoicing.model.*;
public class CreateInvoiceFromOrderAction
extends ViewBaseAction { // To use getView()
public void execute() throws Exception {
Order order = (Order) getView().getEntity(); // Order entity displayed in the view (1)
order.createInvoice(); // The real work is delegated to the entity (2)
getView().refresh(); // In order to see the created invoice in 'Invoice' tab (3)
addMessage("invoice_created_from_order", // Confirmation message (4)
order.getInvoice());
}
}
Really simple. We get the
Order entity (1), call the
createInvoice()
method (2), refresh the view (3) and display a message (4). Note how the
action is a mere intermediary between the view (the user interface) and
the model (the business logic).
Remember to add the message text to the
Invoicing-messages_en.properties
file in
i18n folder, as following:
invoice_created_from_order=Invoice {0} created from current order
However, just "as is" the message is not shown nicely, because we
pass an
Invoice object as argument. We need a
toString()
for
Invoice and
Order useful to the user. We'll overwrite
the
toString() of
CommercialDocument (the parent of
Invoice
and
Order) to achieve this. You can see this
toString()
method here:
abstract public class CommercialDocument extends Deletable {
...
public String toString() {
return year + "/" + number;
}
}
Year and number are perfect to identify an invoice or order from
the user perspective.
That's all for the action. Let's see the missing piece, the
createInvoice()
method of the
Order entity.
Writing
the real business logic in the entity
The business logic for creating the new
Invoice is defined in the
Order entity, not in the action. This is just the natural way to
go. This is the natural way to go in accordance with the essential
principle behind Object-Orientation where the objects are not just data,
but data and logic. The most beautiful code is that whose objects contain
the logic for managing their own data. If your entities are mere data
containers (simple wrappers around database tables), and your actions
contain all the logic for manipulating them, your code is a perversion of
the original goal of Object-Orientation.
Apart from the spiritual reason, to put the logic for creating an
Invoice
inside the
Order entity is a very pragmatic approach, because in
this way we can use this logic from other actions, batch processes, web
services, etc.
Let's see the code of the
createInvoice() method of the
Order
class:
public class Order extends CommercialDocument {
...
public void createInvoice() throws Exception { // throws Exception is just
// to get simpler code for now
Invoice invoice = new Invoice(); // Instantiates an Invoice (1)
BeanUtils.copyProperties(invoice, this); // and copies the state (2)
// from the current Order
invoice.setOid(null); // To let JPA know this entity does not exist yet
invoice.setDate(LocalDate.now()); // The date for the new invoice is today
invoice.setDetails(new ArrayList<>(getDetails())); // Clones the details collection
XPersistence.getManager().persist(invoice);
this.invoice = invoice; // Always after persist() (3)
}
}
The logic consists of creating a new
Invoice object (1),
copying the data from the current
Order to it (2) and assigning
the resulting entity to the invoice reference in the current
Order
(3).
There are three subtle details here. First, you have to write
invoice.setOid(null),
otherwise the new
Invoice will get the same identity as the source
Order. Moreover, JPA does not like to persist objects with the
autogenerated id pre-filled. Second, you have to assign the new
Invoice
to the current
Order (
this.invoice = invoice) after your
call to
persist(invoice), if not you get a error from JPA
(something like "object references an unsaved transient instance". Third,
we have to wrap the
details collection with a
new ArrayList(),
so it is a new collection but with the same elements, because JPA don't
want the same collection assigned to two entities.
Write
less code using Apache Commons BeanUtils
Note how we have used
BeanUtils.copyProperties() to copy all
properties from the current
Order to the new
Invoice. This
method copies all properties with the same name from one object to
another, even if the objects belong to different classes. This utility is
from the Commons BeanUtils project from Apache. The jar for this utility,
commons-beanutils.jar, is already included in your project.
The next snippet shows how using BeanUtils you actually write less code:
BeanUtils.copyProperties(invoice, this);
// Is the same as
invoice.setOid(getOid());
invoice.setYear(getYear());
invoice.setNumber(getNumber());
invoice.setDate(getDate());
invoice.setDeleted(isDeleted());
invoice.setCustomer(getCustomer());
invoice.setVatPercentage(getVatPercentage());
invoice.setVat(getVat());
invoice.setTotalAmount(getTotalAmount());
invoice.setRemarks(getRemarks());
invoice.setDetails(getDetails());
However, the main advantage of using BeanUtils is not to save some
typing, but that you have code more resilient to changes. Because, if you
add, remove or rename some property of
ComercialDocument (the
parent of
Invoice and
Order) you don't need to change your
code, while if you're copying the properties manually you must change the
code manually.
Application
exceptions
Remember the phrase: "The exception that proves the rule". Rules, life and
software are full of exceptions. And our
createInvoice() method is
not an exception. We have written the code to work in the most common
cases. But, what happens if the order is not ready to be invoiced, or if
there is some problem accessing the database? Obviously, in these cases we
need to take different paths.
This is to say that the simple
throws Exception we have written
for
createInvoice() method is not enough to ensure a robust
behavior. Instead we should use our own exception. Let's create it:
package com.yourcompany.invoicing.model; // In model package
import org.openxava.util.*;
public class CreateInvoiceException extends Exception { // Not RuntimeException
public CreateInvoiceException(String message) {
// The XavaResources is to translate the message from the i18n entry id
super(XavaResources.getString(message));
}
}
Now we can use our
CreateInvoiceException instead of
Exception
in the
createInvoice() method of
Order:
public void createInvoice()
throws CreateInvoiceException // An application exception (1)
{
if (this.invoice != null) { // If an invoice is already present we cannot create one
throw new CreateInvoiceException(
"order_already_has_invoice"); // Allows an i18n id as argument
}
if (!isDelivered()) { // If the order is not delivered we cannot create the invoice
throw new CreateInvoiceException("order_is_not_delivered");
}
try {
Invoice invoice = new Invoice();
BeanUtils.copyProperties(invoice, this);
invoice.setOid(null);
invoice.setDate(LocalDate.now());
invoice.setDetails(new ArrayList<>(getDetails()));
XPersistence.getManager().persist(invoice);
this.invoice = invoice;
}
catch (Exception ex) { // Any unexpected exception (2)
throw new SystemException( // A runtime exception is thrown (3)
"impossible_create_invoice", ex);
}
}
Now we declare explicitly which application exceptions this method
throws (1). An application exception is a checked exception that indicates
a special but expected behavior of the method. An application exception is
related to the method's business logic. You could create an application
exception for every possible case, such as an
OrderAlreadyHasInvoiceException
and an
OrderNotDeliveredException. This enables you to handle each
case differently in the calling code. This is not needed in our case, so
we simply use our
CreateInvoiceException, a generic application
exception for this method.
Additionally, we have to deal with unexpected problems (2). Unexpected
problems can be system errors (database access, net or hardware problems)
or programmer errors (
NullPointerException,
IndexOutOfBoundsException,
etc). When we find any unexpected problem we throw a runtime exception
(3). In this instance we have thrown
SystemException, a runtime
exception included in OpenXava for convenience, but you can throw any
runtime exception you want.
You do not need to modify the action code. If your action does not catch
the exceptions, OpenXava does it automatically. It displays the messages
from the application exceptions to the user, and, for the runtime
exceptions, shows a generic error message, and rolls back the transaction.
In order to be complete, we have to add the messages used for the
exceptions in the i18n files. Edit the
Invoicing-messages_en.properties
file from
Invoicing/i18n folder adding the next entries:
order_already_has_invoice=The order already has an invoice
order_is_not_delivered=The order is not delivered yet
impossible_create_invoice=Impossible to create invoice
There is some debate in the developer community regarding the
correct way of using exceptions in Java. The approach in this section is
the classic way to work with exceptions in the Java Enterprise world.
Validation from action
Usually the best place for validations is the model, i.e., the entities.
However, sometimes it's necessary to put validation logic in the actions.
For example, if you want to obtain the current state of the user
interface, the validation must be done from the action.
In our case, if the user clicks on CREATE INVOICE when creating a new
order, and this order is not yet saved, it will fail. It fails because
it's impossible to create an invoice from an non-existent order. The user
must first save the order.
For that add an condition at the begin of the
execute() method of
CreateInvoiceFromOrderAction to validate that the currently
displayed order is saved:
public void execute() throws Exception {
// Add the next condition
if (getView().getValue("oid") == null) {
// If oid is null the current order is not saved yet (1)
addError("impossible_create_invoice_order_not_exist");
return;
}
...
}
The validation consists of verifying if the
oid is null
(1), in which case the user is entering a new order, but he did not save
it yet. In this case a message is shown, and the creation of the invoice
is aborted.
Here we also have a message to add to the i18n file. Edit the
Invoicing-messages_en.properties
file in the
Invoicing/i18n folder adding the next entry:
impossible_create_invoice_order_not_exist=Impossible to create invoice: The order does not exist yet
Validations tell the user that he has done something wrong. This
is needed, of course, but better still is to create an application that
helps the user to avoid any wrong doings. Let's see one way to do so in
the next section.
On
change event to hide/show an action programmatically
Our current code is robust enough to prevent user slips from breaking
data. We will go one step further, preventing the user to slip at all.
We're going to hide the action for creating a new invoice, if the order is
not valid to be invoiced.
OpenXava allows to hide and show actions programmatically. It also allows
the execution of an action when some property is changed by the user on
the screen. We can use these two techniques to show the button only when
the action is ready to be used.
Remember that an invoice can be generated from an order only if the order
has been delivered and it does not yet have an invoice. So, we have to
monitor the changes in the
invoice reference and
delivered
property of the
Order entity. First, we'll create an action to
show or hide the action for creating an invoice from order,
ShowHideCreateInvoiceAction,
with this code:
package com.yourcompany.invoicing.actions; // In the 'actions' package
import org.openxava.actions.*; // Needed to use OnChangePropertyAction,
public class ShowHideCreateInvoiceAction
extends OnChangePropertyBaseAction { // Needed for @OnChange actions (1)
public void execute() throws Exception {
if (isOrderCreated() && isDelivered() && !hasInvoice()) { // (2)
addActions("Order.createInvoice");
}
else {
removeActions("Order.createInvoice");
}
}
private boolean isOrderCreated() {
return getView().getValue("oid") != null; // We read the value from the view
}
private boolean isDelivered() {
Boolean delivered = (Boolean)
getView().getValue("delivered"); // We read the value from the view
return delivered == null?false:delivered;
}
private boolean hasInvoice() {
return getView().getValue("invoice.oid") != null; // We read the value from the view
}
}
Then we annotate
invoice and
delivered in
Order
with
@OnChange so when the user changes the value of
delivered
or
invoice in the screen, the
ShowHideCreateInvoiceAction
will be executed:
public class Order extends CommercialDocument {
...
@OnChange(ShowHideCreateInvoiceAction.class) // Add this
Invoice invoice;
...
@OnChange(ShowHideCreateInvoiceAction.class) // Add this
boolean delivered;
...
}
ShowHideCreateInvoiceAction is a conventional action with
an
execute() method, moreover it extends
OnChangePropertyBaseAction
(1). All the actions annotated with
@OnChange must implement
IOnChangePropertyAction,
however it's easier to extend
OnChangePropertyBaseAction which
implements it. From this action you can use the
getNewValue() and
getChangedProperty(), although in this specific case we don't need
them.
The
execute() method asks if the displayed order is saved,
delivered, and does not already have an invoice (2), in that case it shows
the action with
addActions("Order.createInvoice"), otherwise it
hides the action with
removeActions("Order.createInvoice"). Thus
we hide or show the
Order.createInvoice action, only showing it
when it is applicable.The
add/removeActions() methods allow to
specify several actions to show or hide separated by commas.
Now when the user checks the delivered checkbox, or chooses an invoice,
the action button is shown or hidden. Accordingly, when the user clicks on
New button to create a new order the button for creating the
invoice is hidden. However, if you choose to modify an already existing
order, the button is always present, regardless if the prerequisites are
fulfilled. This is because when an object is searched and displayed the
@OnChange
actions are not executed by default. We can change this with a little
modification in
SearchExcludingDeleteAction:
public class SearchExcludingDeletedAction
// extends SearchByViewKeyAction {
extends SearchExecutingOnChangeAction { // Use this as base class
The default search action, i.e.,
SearchByViewKeyAction
does not execute the
@OnChange actions, so we change our search
action to extend from
SearchExecutingOnChangeAction.
SearchExecutingOnChangeAction
behaves like
SearchByViewKeyAction but executes the on-change
events. This way, when the user selects an order, the
ShowHideCreateInvoiceAction
is executed.
A tiny detail remains to make all this perfect: when the user click on
CREATE INVOICE, after the invoice has been created, the button should be
hidden. It should not be possible to create the same invoice twice. We can
implement this functionality by refining the
CreateInvoiceFromOrderAction:
public void execute() throws Exception {
...
// Everything worked fine, so we'll hide the action
removeActions("Order.createInvoice");
}
As you can see we just add the
removeActions("Order.createInvoice")
at the end of the
execute() method.
Showing and hiding actions is not a substitute for validation in the
model. Validations are still necessary since the entities can be used from
any other part of the application, not just from the CRUD module. However,
the trick of hiding and showing actions improves the user experience.
Business logic from list
mode
In the previous lesson you learned
how
to create list actions. List actions are very useful tools that
provides the user with the ability to perform some specific logic on
multiple objects at the same time. In our case, we can add an action in
list mode to create a new invoice automatically from several selected
orders in the list. We want this action to work this way:

This list action takes the selected orders and creates an invoice from
them. It just copies the order data into the new invoice, adding the
detail lines of all the orders in one unique invoice. Also a message is
shown. Let's see how to code this behavior.
List action with custom
logic
As you already know, the first step towards having a new custom action in
your module is to add that action to a controller. So, let's edit
controllers.xml adding a new action to the
Order controller:
<controller name="Order">
...
<!-- The new action -->
<action name="createInvoiceFromSelectedOrders"
mode="list"
class="com.yourcompany.invoicing.actions.CreateInvoiceFromSelectedOrdersAction"/>
<!-- mode="list": Only shown in list mode -->
</controller>
This is all that is needed to have this new action available for
Order in list mode.
Now we have to write the Java code for the action:
package com.yourcompany.invoicing.actions;
import java.util.*;
import javax.ejb.*;
import org.openxava.actions.*;
import org.openxava.model.*;
import com.yourcompany.invoicing.model.*;
public class CreateInvoiceFromSelectedOrdersAction
extends TabBaseAction { // Typical for list actions. It allows you to use getTab() (1)
public void execute() throws Exception {
Collection<Order> orders = getSelectedOrders(); // (2)
Invoice invoice = Invoice.createFromOrders(orders); // (3)
addMessage("invoice_created_from_orders", invoice, orders); // (4)
}
private Collection<Order> getSelectedOrders() // (5)
throws FinderException
{
Collection<Order> result = new ArrayList<>();
for (Map key: getTab().getSelectedKeys()) { // (6)
Order order = (Order) MapFacade.findEntity("Order", key); // (7)
result.add(order);
}
return result;
}
}
Really simple. We obtain the list of the checked orders in the
list (2), call
createFromOrders() static method (3) of
Invoice
and show a message (4). In this case we also put the real logic in the
model class, not in the action. Since the logic applies to several orders
and creates a new invoice the natural place to put it is a static method
of
Invoice class.
The
getSelectedOrders() method (5) returns a collection containing
the
Order entities checked by the user in the list. This is
easily achieved using
getTab() (6), available from
TabBaseAction
(1), that returns an
org.openxava.tab.Tab object. The
Tab
object allows you to manage the tabular data of the list. In this case we
use
getSelectedKeys() (6) that returns a collection with the keys
of the selected rows. Since these keys are in
Map format we use
MapFacade.findEntity()
(7) to convert them to
Order entities.
As always, add the message text to the
Invoicing-messages_en.properties
file in i18n folder:
invoice_created_from_orders=Invoice {0} created from orders: {1}
That's all for the action. Let's see the missing piece, the
createFromOrders() method of the
Invoice class.
Business
logic in the model over several entities
The business logic for creating a new
Invoice from several
Order
entities is in the model layer, i.e., the entities, not in the action. We
cannot put the method in
Order class, because the process is done
from several orders, not just one. We cannot use an instance method in
Invoice
because the invoice does not exist yet, in fact we want to create it.
Therefore, we are going to create a static factory method in the
Invoice
class for creating a new invoice from several orders.
You can see this method here:
public class Invoice extends CommercialDocument {
...
public static Invoice createFromOrders(Collection<Order> orders)
throws CreateInvoiceException
{
Invoice invoice = null;
for (Order order: orders) {
if (invoice == null) { // The first order
order.createInvoice(); // We reuse the logic for creating an invoice
// from an order
invoice = order.getInvoice(); // and use the created invoice
}
else { // For the remaining orders the invoice is already created
order.setInvoice(invoice); // Assign the invoice
order.copyDetailsToInvoice(); // A method of Order to copy the lines
}
}
if (invoice == null) { // If there are no orders
throw new CreateInvoiceException(
"orders_not_specified");
}
return invoice;
}
}
We use the first
Order to create the new
Invoice
using the already existing
createInvoice() method from
Order.
Then we call to the
copyDetailsToInvoice() method of
Order
to copy the lines from the remaining orders to the new
Invoice
and accumulates on it the
vat and
totalAmount. Moreover,
we set the new
Invoice as the invoice for the orders of the
collection.
If
invoice is null at the end of the process it's because the
orders
collection is empty. In this case we throw a
CreateInvoiceException.
Since the action does not catch the exceptions, OpenXava shows the
exception message to the user. This is fine. If the user does not check
any orders and he clicks on the button for creating an invoice, then this
error message will be shown to him.
As you can see, It copies the
details of the current order to the invoice and accumulates the vat
and totalAmount.