Command Log
The Command Log module provides an implementation of CommandSubscriber SPI that persists Commands using the JPA/EclipseLink object store.
One use case is to combine with the Audit Trail extension. The Command Log module logs the action invocations or property edits that the end-user makes, while the audit trail logs the resultant changes in state to domain objects. The two logs are correlated using the interactionId of the owning interaction.
Another use case is to support (persisted) background commands, whereby actions are not invoked immediately but instead persisted and invoked by a background thread; this is described in the background commands section below.
Sometimes the Execution Log extension is also configured with or instead of this extension; see the notes below to compare and contrast.
Setup
Dependency Management
Add an entry for the Command Log module’s own BOM:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.causeway.extensions</groupId>
<artifactId>causeway-extensions-commandlog</artifactId>
<scope>import</scope>
<type>pom</type>
<version>3.6.0</version>
</dependency>
</dependencies>
</dependencyManagement>
Dependencies / Imports
In the webapp module of your application, add the following dependency for the JPA (EclipseLink) object store:
<dependencies>
<dependency>
<groupId>org.apache.causeway.extensions</groupId>
<artifactId>causeway-extensions-commandlog-persistence-jpa</artifactId>
</dependency>
</dependencies>
In your application’s App Manifest, import the CommandLog modules. For the JPA/Eclipselink persistence mechanism, use:
@Configuration
@Import({
CausewayModuleExtCommandLogPersistenceJpa.class,
// ...
})
public class AppManifest {
}
Configuration Properties
Add the database schema used by the Command Log entities to the configuration file:
causeway:
persistence:
schema:
auto-create-schemas: causewayExtCommandLog
Optionally, modify the configuration properties for the Command Log module itself:
causeway:
extensions:
command-log:
publish-policy: "always" (1)
| 1 | the alternative is "only-if-system-changed", which suppresses the persisting of `CommandLogEntry`s for commands where no other system state was changed (for example a finder action with safe semantics).
See causeway.extensions.command-log.publish-policy configuration property for more details. |
Persistent Replay-result Mappings
Command replay can observe that an object recorded with one bookmark on the source system was created with another bookmark locally. By default these replay-result mappings are held in memory for the lifetime of the application instance. To retain mappings across restarts, select persistent storage:
causeway:
extensions:
command-log:
replay-result-mapping:
storage-strategy: PERSISTENT
on-conflict-policy: THROW_EXCEPTION
Persistent mapping requires the causeway-extensions-commandlog-persistence-jpa dependency and its additive CommandReplayResultMapping database table.
Causeway 4’s commandlog persistence adapter is JPA-only; the JDO adapter removed for Causeway 4 is not restored by this feature.
The first observed mapping for a recorded bookmark is authoritative.
Repeated observations of the same actual bookmark are idempotent and retain the original command interaction id.
A different later result either fails and rolls back replay (THROW_EXCEPTION) or is logged and ignored (LOG_AND_CONTINUE).
Identity mappings are stored as well as changed mappings.
When the repository is available, the commandlog menu provides prototyping actions to list all or changed mappings, find mappings by recorded or actual bookmark, and delete all mappings.
Each mapping can also be inspected and deleted individually.
An application-defined CommandReplayMappingListener bean remains authoritative and causes both built-in listeners to back off.
Replayable Command Projections
The export and replay managers wrap command-log entries as ReplayableCommand view models.
Each command reports whether it has a recorded result and derives a Participants collection from its command data:
-
TARGETrows represent recorded target bookmarks; -
PARAMETERrows represent reference-valued action parameters and include the parameter name (scalar parameters are omitted); and -
RESULTrepresents the recorded result bookmark, when present.
Participants show the recorded bookmark even when the referenced object is unavailable locally.
Their actual bookmark comes from the configured replay-mapping service.
For target and parameter rows an explicit mapping is visible in any replay state; after a successful (OK) replay, an unmapped bookmark falls back to the recorded value.
A result row exposes its actual bookmark only after successful replay, using an explicit mapping when available and otherwise the recorded value.
The optional target, argument, and result object links resolve only the actual bookmark and remain empty if that object is not available locally; bookmark audit data remains visible.
Participant view-model mementos contain identity rather than bookmark state: the owning interaction id followed by --target, --parameter--<name>, or --result.
Opening such a memento re-derives the participant from the current command and current replay mappings.
General command projections include state-changing commands, safe commands with a recorded result, and commands whose semantics cannot be resolved. They omit resultless safe actions without deleting or changing the underlying command-log entries. The pending-or-failed replay collection is intentionally exempt so that all imported replay work remains reviewable. The safe Previous and Next actions navigate foreground commands in command-log order, skipping entries that are ineligible under this general rule.
For now, export and replay continue to use the separate CommandExportManager and CommandReplayManager view models.
A future change may unify their sequence-management surface; the participant projection and eligibility policy do not depend on that redesign.
menubar.layout.xml
Once configured, the extension provides a number of menu actions.
You can use menubars.layout.xml to arrange these as you see fit.
To get you started, the following fragment adds all of the actions to an "Activity" secondary menu:
<mb:secondary>
...
<mb:menu>
<mb:named>Activity</mb:named>
...
<mb:section>
<mb:named>Commands</mb:named>
<mb:serviceAction id="activeCommands" objectType="causeway.ext.commandLog.CommandLogMenu"/>
<mb:serviceAction id="findMostRecent" objectType="causeway.ext.commandLog.CommandLogMenu"/>
<mb:serviceAction id="findCommands" objectType="causeway.ext.commandLog.CommandLogMenu"/>
<mb:serviceAction id="findAll" objectType="causeway.ext.commandLog.CommandLogMenu"/>
</mb:section>
...
</mb:menu>
</mb:secondary>
SecMan Security Roles
If SecMan extension is configured, then permissions must be granted to access the menu actions.
This can be done by granting the role set up by the CausewayExtCommandLogRoleAndPermissions seed fixture script (see its ROLE_NAME constant).
User Interface
The extension provides a number of menu actions and contributions.
The menu actions are as listed in menubar.layout.xml, above. They allow the administrator to query the persisted commands. Typically access to these actions would be restricted, see security roles above.
The extension also provides these mixins:
-
This contributes a
recentCommandscollection to each and every domain object.This can be explicit positioned through the domain class' own layout file, but this is generally not necessary: it will slot into the tab group in the layout file indicated for unreferenced collections using
<tabGroup unreferencedCollections="true">. -
HasUsername_recentCommandsByUser
This contributes the
recentCommandsByUsercollection to any domain object that implements the HasUsername interface.Most notably, this is SecMan extension’s ApplicationUser entity that represents a logged-on user. It is also supported by xref:security:sessionlog:about.adoc
-
HasInteractionId_commandLogEntry
This contributes the
commandLogEntryproperty to any object implementing HasInteractionId interface. Typically these are the entities persisted by the Execution Log or Audit Trail extensions, making it easy to traverse between these logs.
Background Commands
Sometimes we might want to execute an action not immediately in the current users’s thread of control, but instead to perform it in the background; for example any long-running process.
One way to accomplish this is to use WrapperFactory#asyncWrap(…), where the command is executed by another thread obtained from a thread pool (ForkJoinPool.commonPool()).
This works well, but has the slight risk that it is not transactionally safe - the async thread executes in its own interaction/transaction, and so might fail even though the initiating command succeeds; or vice versa.
An alternative approach is to use the BackgroundService.
This persists the command as an CommandLogEntry instance, indicating that it is to be executed in the background.
Then, a separate thread - eg scheduling using Quartz - can pick up the queued CommandLogEntry and execute it.
Submitting Actions
For example, suppose we have a long-running action to export all the invoices we have received from a supplier, perhaps to be sent to some other system.
Assuming that the exportInvoices() action is a regular action on the Supplier domain class, we would use:
BackgroundService to invoke a regular action@Action
public void exportInvoices(Supplier supplier) {
backgroundService.execute(supplier).exportInvoices();
}
If instead this functionality is implemented as a mixin, we would use something like:
BackgroundService to invoke a mixin action:@Action
public void exportInvoices(Supplier supplier) {
backgroundService.executeMixin(Supplier_exportInvoices.class, supplier).act();
}
The action being invoked must be part of the Causeway metamodel, in other words it cannot be marked uses @Programmatic or @Domain.Exclude.
By default all the usual hide/disable/validate rules will be checked, but there are also methods to allow these rules to be skipped.
Behind the scenes this service uses WrapperFactory#asyncWrap(…) using AsyncControl#with(ExecutorService) to pass an implementation of ExecutorService that persists the command as a CommandLogEntry instance.
If you require more fine-grained control, you can always just use the WrapperFactory async method yourself, specifying an implementation of the SyncControl.CommandListener.
Executing Actions using the Quartz scheduler
Once a command has been persisted as a CommandLogEntry, we require some other process to actually execute the command.
The Command Log module includes the RunBackgroundCommandsJob, a Quartz job that does exactly this.
Each time it is called it will query for any background commands that have not been started, and will execute each (using the CommandExecutorService).
The job is marked as non re-entrant, so it doesn’t matter how often it is called; we recommend a 10 second delay usually works fine.
To configure Quartz, add the following to your AppManifest:
public class AppManifest {
@Bean(name = "RunBackgroundCommandsJob") (1)
public JobDetailFactoryBean jobDetail() {
val jobDetailFactory = new JobDetailFactoryBean();
jobDetailFactory.setJobClass(RunBackgroundCommandsJob.class);
jobDetailFactory.setDurability(true);
return jobDetailFactory;
}
@Bean
public SimpleTriggerFactoryBean trigger(
final @Qualifier("RunBackgroundCommandsJob") JobDetail job) { (1)
val trigger = new SimpleTriggerFactoryBean();
trigger.setJobDetail(job);
trigger.setStartDelay(60_000); (2)
trigger.setRepeatInterval(10_000); (3)
trigger.setRepeatCount(REPEAT_INDEFINITELY);
return trigger;
}
// ...
}
| 1 | name and qualify the job (so will not interfere with any other Quartz jobs you may have defined) |
| 2 | 60 secs to wait for the app to be ready |
| 3 | check every 10 seconds |
Disabling Quartz
The Command Log module automatically references the Quartz library. If you don’t want to use this functionality and want to exclude quartz, then add an explicit dependency on the Command Log applib but exclude the quartz dependency within it:
<dependencies>
<dependency>
<groupId>org.apache.causeway.extensions</groupId>
<artifactId>causeway-extensions-commandlog-applib</artifactId>
<exclusions>
<exclusion>
<groupId>org.quartz-scheduler</groupId> (1)
<artifactId>quartz</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
| 1 | exclude reference to quartz |
Notes
Conceptually a command represents the intention to execute an action or to edit a property ("before" the change), while an interaction execution represents the actual execution itself ("after" the change).
The CommandSubscriber SPI and ExecutionSubscriber SPI allow either to be subscribed to. From an auditing perspective, their behaviour is quite similar:
-
even though a command represents the intention to invoke an action, its CommandSubscriber SPI is only called once the action/property edit has been completed.
-
the ExecutionSubscriber is called as soon as the action has completed. In most interactions there will only be a single action called within the interaction, hence these two subscribers will be called at almost the same time with very similar payloads.
However, there can be some subtle differences:
-
the WrapperFactory service allows actions to be invoked "as if" through the user interface. Therefore one action can execute another can execute another, creating a nested call graph of executions.
The ExecutionSubscriber is called after each and every execution as it completes, so will be called several times.
-
In contrast, the CommandSubscriber is called only once, for the top-level (outermost) action.
See also
-
refguide:applib:index/services/iactnlayer/InteractionService.adoc.
-
WrapperFactory service
-
BackgroundService service
-
Execution Log extension