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.
<action name="action-name" label="actions.action-name.label" endpoint="/business/logic"/>
netuiactions("action-name"):
grant netuiPerform to actionrole;
@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
inActionResultBeanBuilder
is ignored as this navigate from the detail to the listselection cannot be cleared after action (
clearSelection
inActionResultBeanBuilder
)entity detail cannot be reloaded after action (
reloadDetail
inActionResultBeanBuilder
)
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"/>
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 directlytitle(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 directlyforceDefaultTitle()
: 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
andfilename
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 entitiesaddOutputJob(Entity outputJob)
: The output job(s) will be offered to open or download in the success toasterclearSelection()
: If this method is called the selection is cleared after the action in the clientselectionDeleted()
: If this method is called and the action was triggered from a detail, it navigates back to the list like the delete actionreloadDetail()
: 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 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.