/* * 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; }