This is an internal documentation. There is a good chance you’re looking for something else. See Disclaimer.

Simple Actions

Common use cases can be implemented through a bit of config and business logic only, without the need for a custom front end. This results in robust actions that are maintained during product development and should cause little conflicts in future versions. They can be added to list or detail forms.

Basic action example

Hint

We use the naming schema {moduleName}/actions/{actionName} for simple action endpoint. Some examples:

  • customer module: ihkschwaben/actions/assignInspector

  • core module: enterprisesearch/actions/indexBuild

  • optional module: event/actions/massRegistration

The most basic form of an action simply calls an endpoint with the current selection, executes some logic and signals success or failure without any further user interaction.

XML configuration
<action name="action-name" label="actions.action-name.label" endpoint="/business/logic"/>
ACL configuration
netuiactions("action-name"):
    grant netuiPerform to actionrole;
Endpoint
@Path("/business/logic") // defines the URL of your resource
@Secured(roles = "actionrole") // usually the same role as in ACL
@RestResource // needed for Spring to recognize your resource
public class BusinessLogicResource extends AbstractActionResource {
    public BusinessLogicResource() {
        // only a single entity is passed as selection
        super(SelectionType.SINGLE);

        // pass a list as selection
        super(SelectionType.MULTIPLE);

        // action is independent of selection
        super(SelectionType.NONE);
    }

    @Override
    protected ActionResultBean doPerformAction(ActionDataBean actionDataBean,
                                               ActionResourceBean actionResourceBean,
                                               TaskContext taskContext) {
        // used if SelectionType.SINGLE
        Entity entity = getSelectedEntity(actionDataBean.getSelection());

        // used if SelectionType.MULTIPLE
        PrimaryKeyList selectedKeys = getSelectedEntities(actionDataBean.getSelection());

        // insert business logic here

        return new ActionResultBeanBuilder(true).build();
    }

    @Override
    protected Class<? extends AbstractJob> getJobClass() {
        return BusinessLogicJob.class;
    }

    private class BusinessLogicJob extends AbstractActionJob {
        // class needed for task execution, no need to add anything here
    }
}

Form possibilities

The usual form properties like position or scopes are also applicable.

Adding an icon

Icons are not used when normally displaying an action, but they can be useful when using them inline in a list.

<action name="action-name" label="actions.action-name.label" endpoint="/business/logic" icon="icon-id"/>

A list of all icons can be found in the IconShowcase

You can set button-type="icon" that only the icon (without the label) is displayed.

Limit selection size

<action name="action-name" label="actions.action-name.label" endpoint="/business/logic" minSelection="1" maxSelection="10"/>

Warn when selection size reaches some threshold

By default the threshold is set to 100, but disabled.

<action name="action-name" label="actions.action-name.label" endpoint="/business/logic" showConfirmation="true" confirmationThreshold="1000"/>

Run in background

This will also require adjustments in your resource, see Run in background.

<action name="action-name" label="actions.action-name.label" endpoint="/business/logic" runInBackground="true"/>

Running actions in the background have some limitations:

  • is not working with entity docs (@EnableEntityDocAcl annotation)

  • does not reload list after action is finished (e.g. nice feature if field in list is edited via action or new entities are created)

  • selectionDeleted in ActionResultBeanBuilder is ignored as this navigate from the detail to the list

  • selection cannot be cleared after action (clearSelection in ActionResultBeanBuilder)

  • entity detail cannot be reloaded after action (reloadDetail in ActionResultBeanBuilder)

Action condition

If an action should only be visible if a certain condition is fullfilled, add an action condition.

First define an action condition contribution. An action condition has a unqiue name, an entity model to which it belongs and a TQL condition.

@Bean
public ActionConditionContribution myConditionActionConditionContribution() {
    String condition = "relUser_status.unique_id == \"active\"";
    return new ActionConditionContribution("my-condition", "User", condition);
}

Add the condition name attribute to the action tag. Action conditions are only supported on the entity detail and for actions in columns.

<action name="action-name" label="actions.action-name.label" endpoint="/business/logic" condition-name="my-condition"/>

Button styling

The background color of action button can be changed to the primary color of the theme. paper is the default value with the white background.

<action name="action-name" label="actions.action-name.label" endpoint="/business/logic" button-style="primary"/>

Endpoint possibilities

Endpoints are generally split into two parts, preAction and doPerformAction. preAction is used for anything that needs to be checked or made available before the actual business logic is ran.

The doPerformAction returns ActionResultBean which is the result of performing the action. The ActionResultBeanBuilder must be used to create ActionResultBean. The minimal setup is new ActionResultBeanBuilder(success).build();. A boolean is passed where true means the action was successful performed and false the action failed.

There are several optional parameters:

  • message(String textResourceKey): Pass a custom text message with a text resource key (e.g. message("message-textresource-key"))

  • message(TextMessage textMessage): Pass a custom text message with variable (e.g. message(new TextMessage("message-textresource-key").setVar("count", count))))

  • messageFormatted(String message): Pass a preformatted custom text message to be used directly

  • title(String textResourceKey): Pass a custom title with a text resource key (e.g. title("title-textresource-key"))

  • title(TextMessage textMessage): Pass a custom title with variables (e.g. title(new TextMessage("title-textresource-key").setVar("count", count))))

  • titleFormatted(String message): Pass a preformatted custom title to be used directly

  • forceDefaultTitle(): Usually, using a message without a custom title disables the default title and only shows the message. Using this, the default title is added even to custom messages.

  • addParam(String key, Object value): Add optional parameters used by the client (e.g. downloadUrl and filename for directly downloading files without a toaster)

  • addResultEntity(Entity entity): These entities will be mentioned in the success toaster. Just call the method multiple times to add more entities

  • addOutputJob(Entity outputJob): The output job(s) will be offered to open or download in the success toaster

  • clearSelection(): If this method is called the selection is cleared after the action in the client

  • selectionDeleted(): If this method is called and the action was triggered from a detail, it navigates back to the list like the delete action

  • reloadDetail(): If this method is called and the action was triggered from a subtable in a detail, the detail is also reloaded

Finally the build() method is called to create the ActionResultBean.

Any unchanged methods and annotations from the example are not listed again for the sake of brevity.

Run checks before logic and abort if not successful

public class BusinessLogicResource extends AbstractActionResource {
    @Override
    public PreActionResponseBean preAction(ActionResourceBean actionResourceBean) {
        boolean success = false; // insert check logic here
        if (success) {
            return new PreActionResponseBean(PreCheckResponseBean.success());
        } else {
            return new PreActionResponseBean(PreCheckResponseBean.failed("message"));
        }
    }
}

Make user confirm action

public class BusinessLogicResource extends AbstractActionResource {
    @Override
    public PreActionResponseBean preAction(ActionResourceBean actionResourceBean) {
        String message = "message"; // insert any logic to build message here
        return new PreActionResponseBean(PreCheckResponseBean.confirm("message"));
    }
}

Have user acknowledge some message without proceeding with action

public class BusinessLogicResource extends AbstractActionResource {
    @Override
    public PreActionResponseBean preAction(ActionResourceBean actionResourceBean) {
        String message = "message"; // insert any logic to build message here
        return new PreActionResponseBean(PreCheckResponseBean.acknowledge("message"));
    }
}

Change default action in confirm popup

public class BusinessLogicResource extends AbstractActionResource {
    @Override
    public PreActionResponseBean preAction(ActionResourceBean actionResourceBean) {
        return new PreActionResponseBean(PreCheckResponseBean.confirm("message")).withDefaultAction(PreCheckResponseBean.Action.CANCEL);
    }
}

Add entities to message

These show up grouped by entity model, displaying their name and count, with a link to the given entities where possible. These only show up when using PreCheckResponseBean#confirm and PreCheckResponseBean#acknowledge.

public class BusinessLogicResource extends AbstractActionResource {
    @Override
    public PreActionResponseBean preAction(ActionResourceBean actionResourceBean) {
        return new PreActionResponseBean(PreCheckResponseBean.confirm("message")).withEntities(someIterableOfEntities);
    }
}

Add TQL condition for select field

public class BusinessLogicResource extends AbstractActionResource {
    @Override
    public PreActionResponseBean preAction(ActionResourceBean actionResourceBean) {
        InitialFormValueResponseBean initialForm = loadInitialForm()
            .setCondition("relUser", "relUser_code1.unique_id == \"executive_board\"");
        return new PreActionResponseBean(initialForm);
    }
}

Validate user input

Show confirmation message:

public class BusinessLogicResource extends AbstractActionResource {
    @Override
    protected PreActionResponseBean doValidate(ActionResourceBean actionResourceBean, ActionDataBean actionDataBean) {
        return new PreActionResponseBean(PreCheckResponseBean.confirm("message"));
    }
}

Show failed toaster:

public class BusinessLogicResource extends AbstractActionResource {
    @Override
    protected PreActionResponseBean doValidate(ActionResourceBean actionResourceBean, ActionDataBean actionDataBean) {
        Entity formEntity = actionDataBean.getFormEntity();
        boolean configurationBoolean = formEntity.getBool("configuration_boolean");

        if (configurationBoolean) {
            return new PreActionResponseBean(PreCheckResponseBean.failed("message"));
        }

        return new PreActionResponseBean(PreCheckResponseBean.success());
    }
}

Let the user input data for the action

Create a session-only entity model that contains the inputs you need. This will then be available in the resource like a regular entity. Forms can also be used, but often times the automatically generated forms are sufficient.

XML of example entity called Configuration_entity
<?xml version="1.0" encoding="UTF-8"?>
<entity-model xmlns="http://nice2.tocco.ch/schema/entityModel.xsd" session-only="true">
    <field name="configuration_number" type="integer">
        <validations>
            <mandatory/>
        </validations>
    </field>
    <field name="configuration_boolean" type="boolean"/>
</entity-model>
public class BusinessLogicResource extends AbstractActionResource {

    public AddCandidateNumbersActionResource(PersistenceService persistenceService) {
        super(SelectionType.SINGLE, "Configuration_entity");
    }

    @Override
    public PreActionResponseBean preAction(ActionResourceBean actionResourceBean) {
        return new PreActionResponseBean(loadInitialForm());
    }

    @Override
    protected ActionResultBean doPerformAction(ActionDataBean actionDataBean,
                                               ActionResourceBean actionResourceBean,
                                               TaskContext taskContext) {
        Entity formEntity = actionDataBean.getFormEntity();
        int configurationNumber = formEntity.getInt("configuration_number");
        boolean configurationNumber = formEntity.getBool("configuration_boolean");

        // insert business logic here

        return new ActionResultBeanBuilder(true).build();
    }
}

Normally the form name must not be set and is determined by the initialFormEntityName. However the form can be explicitly set with initialFormName. This allows to define one form entity and multiple forms based on it.

Fill initial form with default values and custom texts

public class BusinessLogicResource extends AbstractActionResource {
    @Override
    public PreActionResponseBean preAction(ActionResourceBean actionResourceBean) {
        Entity relatedEntity;
        InitialFormValueResponseBean initialForm = loadInitialForm()
            .withTitle("custom-title")
            .withMessage("custom-message")
                .setSelectValue(
                        "relOther_entity",
                        relatedEntity.getString("label"),
                        relatedEntity.requireKey().stringify()
                )
                .setValue("fieldname", "some-value");
        return new PreActionResponseBean(initialForm);
    }
}

Run in background

Requires adjustement in form, see Run in background. Log information through TaskContext.getProgress() (always first check TaskContext.isProgressAvailable()).

public class BusinessLogicResource extends AbstractActionResource {
    @Override
    protected ActionResultBean doPerformAction(ActionDataBean actionDataBean,
                                               ActionResourceBean actionResourceBean,
                                               TaskContext taskContext) {
        int done = 0;
        int totalSize = 100;
        while (done < totalSize) {
            if (taskContext.isCancelled()) {
                if (taskContext.isProgressAvailable()) {
                    taskContext.getProgress().updateCancelled();
                }
                break;
            }


            if (taskContext.isProgressAvailable()) {
                taskContext.getProgress().updateAbsolute("actions.action-name.progress", totalSize, done);
            }

            // insert business logic here
        }

        if (taskContext.isProgressAvailable()) {
            taskContext.getProgress().updateCompleted("actions.action-name.success");
        }

        return new ActionResultBeanBuilder(true).build();
    }

    @Override
    protected Class<? extends AbstractJob> getJobClass() {
        return InterruptableBusinessLogicJob.class;
    }

    private class InterruptableBusinessLogicJob extends AbstractInterruptableActionJob {
        // class needed for task execution, no need to add anything here
    }
}

The progress logging can be simplified by using the CancelHandlingIterator#withIndexedCancelHandling utility when looping over something. CancelHandlingIterator#withCancelHandling can be used if you do not need the data with its index.

@Override
    protected ActionResultBean doPerformAction(ActionDataBean actionDataBean,
                                               ActionResourceBean actionResourceBean,
                                               TaskContext taskContext) {
        List<Entity> allData = List.of(); // example data, works with any class

        for (Indexed<Entity> currentData : withIndexedCancelHandling(allData, taskContext)) {
            Entity currentEntity = currentData.value();
            // business logic
            if (taskContext.isProgressAvailable()) {
                taskContext.getProgress().updateAbsolute("message-textresource-key", allData.size(), currentData.index() + 1);
            }
        }

        return new ActionResultBeanBuilder(true).build();
    }

Customize client messages for background actions

public class BusinessLogicResource extends AbstractActionResource {
    protected TextMessage taskSchedulingMessage() {
        return new TextMessage("rest.action.task.scheduled");
    }

    protected TextMessage taskStartedMessage() {
        return new TextMessage("rest.action.task.started");
    }

    protected TextMessage taskFinishedMessage() {
        return new TextMessage("rest.action.task.finished");
    }

    protected TextMessage taskFailedMessage() {
        return new TextMessage("rest.action.task.failed");
    }

    protected TextMessage taskCancelledMessage() {
        return new TextMessage("rest.action.task.cancelled");
    }
}

Get detail entity when running action from a embedded list

public class BusinessLogicResource extends AbstractActionResource {
    @Override
    protected ActionResultBean doPerformAction(ActionDataBean actionDataBean,
                                               ActionResourceBean actionResourceBean,
                                               TaskContext taskContext) {
        Entity parentEntity = actionDataBean.getParentEntity();

        // insert business logic here

        return new ActionResultBeanBuilder(true).build();
    }
}

Testing

Use the class BaseAbstractActionResourceTest as base class for your tests and just write normal REST tests (as described in How to test your resource). It is just an extension of a normal REST test where already some mocking is done and some helper methods exist such as createRequestBean, createEntityBean, doRequest, doRequestExpectSuccess and doRequestExpectFailure.

Actions are always run synchronously in tests. If you want to test async progress reporting, you can use AbstractActionResource#setTaskContextBuilder to use a custom mocked task context that expects progress calls even when the action is not actually asynchronous. NEVER use this in production code.