/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.isis.applib.services.iactn;
import java.sql.Timestamp;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicInteger;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.apache.isis.applib.annotation.Programmatic;
import org.apache.isis.applib.annotation.Value;
import org.apache.isis.applib.services.HasTransactionId;
import org.apache.isis.applib.services.clock.ClockService;
import org.apache.isis.applib.services.command.Command;
import org.apache.isis.applib.services.eventbus.AbstractDomainEvent;
import org.apache.isis.applib.services.eventbus.ActionDomainEvent;
import org.apache.isis.applib.services.eventbus.EventBusService;
import org.apache.isis.applib.services.eventbus.PropertyDomainEvent;
import org.apache.isis.applib.services.metrics.MetricsService;
import org.apache.isis.applib.services.wrapper.WrapperFactory;
import org.apache.isis.applib.services.xactn.Transaction;
import org.apache.isis.schema.common.v1.DifferenceDto;
import org.apache.isis.schema.common.v1.InteractionType;
import org.apache.isis.schema.common.v1.PeriodDto;
import org.apache.isis.schema.ixn.v1.ActionInvocationDto;
import org.apache.isis.schema.ixn.v1.MemberExecutionDto;
import org.apache.isis.schema.ixn.v1.MetricsDto;
import org.apache.isis.schema.ixn.v1.ObjectCountsDto;
import org.apache.isis.schema.ixn.v1.PropertyEditDto;
import org.apache.isis.schema.utils.MemberExecutionDtoUtils;
import org.apache.isis.schema.utils.jaxbadapters.JavaSqlTimestampXmlGregorianCalendarAdapter;
/**
* Represents an action invocation or property modification, resulting in some state change of the system. It captures
* not only the target object and arguments passed, but also builds up the call-graph, and captures metrics, eg
* for profiling.
*
* <p>
* The distinction between {@link Command} and this object is perhaps subtle: the former represents the
* intention to invoke an action/edit a property, whereas this represents the actual invocation/edit itself.
* </p>
*
* <p>
* To confuse matters slightly, historically the {@link Command} interface defines members (specifically:
* {@link Command#getStartedAt()}, {@link Command#getCompletedAt()}, {@link Command#getResult()},
* {@link Command#getException()}) which logically belong to this class instead; they remain in {@link Command}
* for backward compatibility only (and have been deprecated).
* </p>
*
* <p>
* NOTE: you could also think of this interface as being analogous to the (database) transaction. The name
* "Transaction" has not been used for the interface not chosen however because there is also the
* system-level transaction that manages the persistence of
* the {@link Command} object itself.
* </p>
*
*/
@Value
public class Interaction implements HasTransactionId {
//region > transactionId (property)
private UUID transactionId;
@Programmatic
@Override
public UUID getTransactionId() {
return transactionId;
}
@Programmatic
@Override
public void setTransactionId(final UUID transactionId) {
this.transactionId = transactionId;
}
//endregion
//region > push/pop/current/get/clear Execution(s)
private final List<Execution> executionGraphs = Lists.newArrayList();
private Execution currentExecution;
private Execution priorExecution;
/**
* The execution that preceded the current one.
*/
@Programmatic
public Execution getPriorExecution() {
return priorExecution;
}
/**
* <b>NOT API</b>: intended only to be implemented by the framework.
*
* <p>
* (Modelled after {@link Callable}), is the implementation
* by which the framework actually performs the interaction.
*/
public interface MemberExecutor<T extends Execution> {
@Programmatic
Object execute(final T currentExecution);
}
/**
* <b>NOT API</b>: intended to be called only by the framework.
*
* <p>
* Use the provided {@link MemberExecutor} to invoke an action, with the provided
* {@link ActionInvocation} capturing the details of said action.
* </p>
*/
@Programmatic
public Object execute(
final MemberExecutor<ActionInvocation> memberExecutor,
final ActionInvocation actionInvocation) {
push(actionInvocation);
return executeInternal(memberExecutor, actionInvocation);
}
/**
* <b>NOT API</b>: intended to be called only by the framework.
*
* <p>
* Use the provided {@link MemberExecutor} to edit a property, with the provided
* {@link PropertyEdit} capturing the details of said property edit.
* </p>
*/
@Programmatic
public Object execute(
final MemberExecutor<PropertyEdit> memberExecutor,
final PropertyEdit propertyEdit) {
push(propertyEdit);
return executeInternal(memberExecutor, propertyEdit);
}
private <T extends Execution> Object executeInternal(
final MemberExecutor<T> memberExecutor,
final T execution) {
// as a convenience, since in all cases we want the command to start when the first interaction executes,
// we populate the command here.
try {
try {
Object result = memberExecutor.execute(execution);
execution.setReturned(result);
return result;
} catch (Exception ex) {
// just because an exception has thrown, does not mean it is that significant; it could be that
// it is recognized by an ExceptionRecognizer and is not severe, eg unique index violation in the DB.
currentExecution.setThrew(ex);
// propagate (as in previous design); caller will need to trap and decide
throw ex;
}
} finally {
final Timestamp completedAt = clockService.nowAsJavaSqlTimestamp();
pop(completedAt);
}
}
/**
* The current (most recently pushed) {@link Execution}.
*/
@Programmatic
public Execution getCurrentExecution() {
return currentExecution;
}
/**
* <b>NOT API</b>: intended to be called only by the framework.
*
* <p>
* Push a new {@link org.apache.isis.applib.services.eventbus.AbstractDomainEvent}
* onto the stack of events held by the command.
* </p>
*/
@Programmatic
private Execution push(final Execution execution) {
if(currentExecution == null) {
// new top-level execution
executionGraphs.add(execution);
} else {
// adds to graph of parent
execution.setParent(currentExecution);
}
// update this.currentExecution and this.previousExecution
moveCurrentTo(execution);
return execution;
}
/**
* <b>NOT API</b>: intended to be called only by the framework.
*
* <p>
* Pops the top-most {@link org.apache.isis.applib.services.eventbus.ActionDomainEvent}
* from the stack of events held by the command.
* </p>
*/
@Programmatic
private Execution pop(final Timestamp completedAt) {
if(currentExecution == null) {
throw new IllegalStateException("No current execution to pop");
}
final Execution popped = currentExecution;
popped.setCompletedAt(completedAt);
moveCurrentTo(currentExecution.getParent());
return popped;
}
private void moveCurrentTo(final Execution newExecution) {
priorExecution = currentExecution;
currentExecution = newExecution;
}
/**
* Returns a (list of) {@link Execution}s in the order that they were pushed. Generally there will be just one entry in this list, but additional entries may arise from the use of mixins/contributions when re-rendering a modified object.
*
* <p>
* Each {@link Execution} represents a call stack of domain events (action invocations or property edits),
* that may in turn cause other domain events to be fired (by virtue of the {@link WrapperFactory}).
* The reason that a list is returned is to support bulk command/actions (against multiple targets). A non-bulk
* action will return a list of just one element.
* </p>
*/
@Programmatic
public List<Execution> getExecutions() {
return Collections.unmodifiableList(executionGraphs);
}
/**
* <b>NOT API</b>: intended to be called only by the framework.
*
* Clears the set of {@link Execution}s that may have been {@link #push(Execution)}ed.
*/
@Programmatic
public void clear() {
executionGraphs.clear();
}
//endregion
//region > next (programmatic)
/**
* Enumerates the different reasons why multiple occurrences of a certain type might occur within a single
* (top-level) interaction.
*/
public enum Sequence {
/**
* Each interaction is either an action invocation or a property edit. There could be multiple of these,
* typically as the result of a nested calls using the {@link WrapperFactory}. Another reason is
* support for bulk action invocations within a single transaction.
*/
INTERACTION,
/**
* For objects: multiple such could be dirtied and thus published as separate events. For actions
* invocations/property edits : multiple sub-invocations could occur if sub-invocations are made through the
* {@link WrapperFactory}.
*/
PUBLISHED_EVENT,
/**
* There may be multiple transactions within a given interaction, as per {@link Transaction#getSequence()}.
*/
TRANSACTION,
;
@Programmatic
public String id() {
return Interaction.Sequence.class.getName() + "#" + name();
}
}
private final Map<String, AtomicInteger> maxBySequence = Maps.newHashMap();
/**
* Generates numbers in a named sequence. The name of the sequence can be arbitrary, though note that the
* framework also uses this capability to generate sequence numbers corresponding to the sequences enumerated by
* the {@link Sequence} enum.
*/
@Programmatic
public int next(final String sequenceId) {
AtomicInteger next = maxBySequence.get(sequenceId);
if(next == null) {
next = new AtomicInteger(0);
maxBySequence.put(sequenceId, next);
} else {
next.incrementAndGet();
}
return next.get();
}
//endregion
/**
* Represents an action invocation/property edit as a node in a call-stack execution graph, with sub-interactions
* being made by way of the {@link WrapperFactory}).
*/
public static abstract class Execution<T extends MemberExecutionDto, E extends AbstractDomainEvent<?>> {
//region > fields, constructor
private final String memberIdentifier;
private final Object target;
private final String targetMember;
private final String targetClass;
private final Interaction interaction;
private final InteractionType interactionType;
protected Execution(
final Interaction interaction,
final InteractionType interactionType,
final String memberIdentifier,
final Object target,
final String targetMember,
final String targetClass) {
this.interaction = interaction;
this.interactionType = interactionType;
this.memberIdentifier = memberIdentifier;
this.target = target;
this.targetMember = targetMember;
this.targetClass = targetClass;
}
//endregion
//region > via constructor: interaction, interactionType, memberId, target, targetMember, targetClass
@Programmatic
public Interaction getInteraction() {
return interaction;
}
@Programmatic
public InteractionType getInteractionType() {
return interactionType;
}
@Programmatic
public String getMemberIdentifier() {
return memberIdentifier;
}
/**
* The target of the action invocation. If this interaction is for a mixin action, then will be the
* mixed-in target (not the transient mixin itself).
*/
@Programmatic
public Object getTarget() {
return target;
}
/**
* A human-friendly description of the class of the target object.
*/
@Programmatic
public String getTargetClass() {
return targetClass;
}
/**
* The human-friendly name of the action invoked/property edited on the target object.
*/
@Programmatic
public String getTargetMember() {
return targetMember;
}
//endregion
//region > parent, children
private final List<Execution<?,?>> children = Lists.newArrayList();
private Execution<?,?> parent;
/**
* The action/property that invoked this action/property edit (if any).
*/
@Programmatic
public Execution<?,?> getParent() {
return parent;
}
/**
* <b>NOT API</b>: intended to be called only by the framework.
*/
@Programmatic
public void setParent(final Execution<?,?> parent) {
this.parent = parent;
if(parent != null) {
parent.children.add(this);
}
}
/**
* The actions/property edits made in turn via the {@link WrapperFactory}.
*/
@Programmatic
public List<Execution<?,?>> getChildren() {
return Collections.unmodifiableList(children);
}
//endregion
//region > event
private E event;
/**
* The domain event fired on the {@link EventBusService event bus} representing the execution of
* this action invocation/property edit.
*
* <p>
* This event field is set by the framework before the action invocation/property edit itself;
* if read by the executing action/property edit method it will be in the
* {@link AbstractDomainEvent.Phase#EXECUTING executing} phase.
* </p>
*/
@Programmatic
public E getEvent() {
return event;
}
/**
* <b>NOT API</b>: intended to be called only by the framework.
*/
@Programmatic
public void setEvent(final E event) {
this.event = event;
}
//endregion
//region > startedAt, completedAt
private Timestamp startedAt;
private Timestamp completedAt;
/**
* The date/time at which this execution started.
*/
@Programmatic
public Timestamp getStartedAt() {
return startedAt;
}
@Programmatic
public void setStartedAt(final Timestamp startedAt) {
syncMetrics(When.BEFORE, startedAt);
}
/**
* The date/time at which this execution completed.
*/
@Programmatic
public Timestamp getCompletedAt() {
return completedAt;
}
/**
* <b>NOT API</b>: intended to be called only by the framework.
*/
void setCompletedAt(final Timestamp completedAt) {
syncMetrics(When.AFTER, completedAt);
}
//endregion
//region > returned, threw (properties)
private Object returned;
/**
* The object returned by the action invocation/property edit.
*
* <p>
* If the action returned either a domain entity or a simple value (and did not throw an
* exception) then this object is provided here.
*
* <p>
* For <tt>void</tt> methods and for actions returning collections, the value
* will be <tt>null</tt>.
*/
@Programmatic
public Object getReturned() {
return returned;
}
/**
* <b>NOT API</b>: intended to be called only by the framework.
*/
@Programmatic
public void setReturned(Object returned) {
this.returned = returned;
}
private Exception threw;
@Programmatic
public Exception getThrew() {
return threw;
}
/**
* <b>NOT API</b>: intended to be called only by the framework.
*/
@Programmatic
public void setThrew(Exception threw) {
this.threw = threw;
}
//endregion
//region > dto (property)
private T dto;
/**
* A serializable representation of this action invocation/property edit.
*
* <p>
* This <i>will</i> be populated (by the framework) during the method call itself (representing the
* action invocation/property edit), though some fields ({@link Execution#getCompletedAt()},
* {@link Execution#getReturned()}) will (obviously) still be null.
* </p>
*/
@Programmatic
public T getDto() {
return dto;
}
/**
* Set by framework (implementation of {@link MemberExecutor})
*/
@Programmatic
public void setDto(final T executionDto) {
this.dto = executionDto;
}
//endregion
//region > helpers (syncMetrics)
enum When {
BEFORE {
@Override
void syncMetrics(
final Execution<?, ?> execution,
final Timestamp timestamp,
final int numberObjectsLoaded,
final int numberObjectsDirtied) {
execution.startedAt = timestamp;
final MetricsDto metricsDto = metricsFor(execution);
final PeriodDto periodDto = timingsFor(metricsDto);
periodDto.setStartedAt(JavaSqlTimestampXmlGregorianCalendarAdapter.print(timestamp));
final ObjectCountsDto objectCountsDto = objectCountsFor(metricsDto);
numberObjectsLoadedFor(objectCountsDto).setBefore(numberObjectsLoaded);
numberObjectsDirtiedFor(objectCountsDto).setBefore(numberObjectsDirtied);
}
},
AFTER {
@Override void syncMetrics(
final Execution<?, ?> execution,
final Timestamp timestamp,
final int numberObjectsLoaded,
final int numberObjectsDirtied) {
execution.completedAt = timestamp;
final MetricsDto metricsDto = metricsFor(execution);
final PeriodDto periodDto = timingsFor(metricsDto);
periodDto.setCompletedAt(JavaSqlTimestampXmlGregorianCalendarAdapter.print(timestamp));
final ObjectCountsDto objectCountsDto = objectCountsFor(metricsDto);
numberObjectsLoadedFor(objectCountsDto).setAfter(numberObjectsLoaded);
numberObjectsDirtiedFor(objectCountsDto).setAfter(numberObjectsDirtied);
}
};
//region > helpers
private static DifferenceDto numberObjectsDirtiedFor(final ObjectCountsDto objectCountsDto) {
return MemberExecutionDtoUtils.numberObjectsDirtiedFor(objectCountsDto);
}
private static DifferenceDto numberObjectsLoadedFor(final ObjectCountsDto objectCountsDto) {
return MemberExecutionDtoUtils.numberObjectsLoadedFor(objectCountsDto);
}
private static ObjectCountsDto objectCountsFor(final MetricsDto metricsDto) {
return MemberExecutionDtoUtils.objectCountsFor(metricsDto);
}
private static MetricsDto metricsFor(final Execution<?, ?> execution) {
return MemberExecutionDtoUtils.metricsFor(execution.dto);
}
private static PeriodDto timingsFor(final MetricsDto metricsDto) {
return MemberExecutionDtoUtils.timingsFor(metricsDto);
}
//endregion
abstract void syncMetrics(
final Execution<?, ?> teExecution,
final Timestamp timestamp,
final int numberObjectsLoaded,
final int numberObjectsDirtied);
}
private void syncMetrics(final When when, final Timestamp timestamp) {
final MetricsService metricsService = interaction.metricsService;
final int numberObjectsLoaded = metricsService.numberObjectsLoaded();
final int numberObjectsDirtied = metricsService.numberObjectsDirtied();
when.syncMetrics(this, timestamp, numberObjectsLoaded, numberObjectsDirtied);
}
//endregion
}
public static class ActionInvocation extends Execution<ActionInvocationDto, ActionDomainEvent<?>> {
private final List<Object> args;
public ActionInvocation(
final Interaction interaction,
final String memberId,
final Object target,
final List<Object> args,
final String targetMember,
final String targetClass) {
super(interaction, InteractionType.ACTION_INVOCATION, memberId, target, targetMember, targetClass);
this.args = args;
}
@Programmatic
public List<Object> getArgs() {
return args;
}
}
public static class PropertyEdit extends Execution<PropertyEditDto, PropertyDomainEvent<?,?>> {
private final Object newValue;
public PropertyEdit(
final Interaction interaction,
final String memberId,
final Object target,
final Object newValue,
final String targetMember,
final String targetClass) {
super(interaction, InteractionType.PROPERTY_EDIT, memberId, target, targetMember, targetClass);
this.newValue = newValue;
}
@Programmatic
public Object getNewValue() {
return newValue;
}
}
@javax.inject.Inject
MetricsService metricsService;
@javax.inject.Inject
ClockService clockService;
}