org.apache.isis.runtimes.dflt.runtime.system.transaction.IsisTransaction.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.isis.runtimes.dflt.runtime.system.transaction.IsisTransaction.java

Source

/*
 *  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.runtimes.dflt.runtime.system.transaction;

import static org.apache.isis.core.commons.ensure.Ensure.ensureThatArg;
import static org.apache.isis.core.commons.ensure.Ensure.ensureThatState;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.isis.core.commons.components.TransactionScopedComponent;
import org.apache.isis.core.commons.ensure.Ensure;
import org.apache.isis.core.commons.exceptions.IsisException;
import org.apache.isis.core.commons.lang.ToString;
import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
import org.apache.isis.core.metamodel.adapter.ResolveState;
import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager;
import org.apache.isis.core.metamodel.adapter.oid.OidMarshaller;
import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation;
import org.apache.isis.core.metamodel.spec.feature.ObjectAssociationFilters;
import org.apache.isis.runtimes.dflt.runtime.persistence.objectstore.transaction.CreateObjectCommand;
import org.apache.isis.runtimes.dflt.runtime.persistence.objectstore.transaction.DestroyObjectCommand;
import org.apache.isis.runtimes.dflt.runtime.persistence.objectstore.transaction.PersistenceCommand;
import org.apache.isis.runtimes.dflt.runtime.persistence.objectstore.transaction.SaveObjectCommand;
import org.apache.isis.runtimes.dflt.runtime.persistence.objectstore.transaction.TransactionalResource;
import org.apache.isis.runtimes.dflt.runtime.system.context.IsisContext;
import org.apache.log4j.Logger;

import com.google.common.base.Objects;
import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

/**
 * Used by the {@link IsisTransactionManager} to captures a set of changes to be
 * applied.
 * 
 * <p>
 * The protocol by which the {@link IsisTransactionManager} interacts and uses
 * the {@link IsisTransaction} is not API, because different approaches are
 * used. For the server-side <tt>ObjectStoreTransactionManager</tt>, each object
 * is wrapped in a command generated by the underlying <tt>ObjectStore</tt>. for
 * the client-side <tt>ClientSideTransactionManager</tt>, the transaction simply
 * holds a set of events.
 * 
 * <p>
 * Note that methods such as <tt>flush()</tt>, <tt>commit()</tt> and
 * <tt>abort()</tt> are not part of the API. The place to control transactions
 * is through the {@link IsisTransactionManager transaction manager}, because
 * some implementations may support nesting and such like. It is also the job of
 * the {@link IsisTransactionManager} to ensure that the underlying persistence
 * mechanism (for example, the <tt>ObjectAdapterStore</tt>) is also committed.
 */
public class IsisTransaction implements TransactionScopedComponent {

    public static enum State {
        /**
         * Started, still in progress.
         * 
         * <p>
         * May {@link IsisTransaction#flush() flush},
         * {@link IsisTransaction#commit() commit} or
         * {@link IsisTransaction#abort() abort}.
         */
        IN_PROGRESS(true, true, true, false),
        /**
         * Started, but has hit an exception.
         * 
         * <p>
         * May not {@link IsisTransaction#flush()} or
         * {@link IsisTransaction#commit() commit} (will throw an
         * {@link IllegalStateException}), but can only
         * {@link IsisTransaction#abort() abort}.
         * 
         * <p>
         * Similar to <tt>setRollbackOnly</tt> in EJBs.
         */
        MUST_ABORT(false, false, true, false),
        /**
         * Completed, having successfully committed.
         * 
         * <p>
         * May not {@link IsisTransaction#flush()} or
         * {@link IsisTransaction#abort() abort} or
         * {@link IsisTransaction#commit() commit} (will throw
         * {@link IllegalStateException}).
         */
        COMMITTED(false, false, false, true),
        /**
         * Completed, having aborted.
         * 
         * <p>
         * May not {@link IsisTransaction#flush()},
         * {@link IsisTransaction#commit() commit} or
         * {@link IsisTransaction#abort() abort} (will throw
         * {@link IllegalStateException}).
         */
        ABORTED(false, false, false, true);

        private final boolean canFlush;
        private final boolean canCommit;
        private final boolean canAbort;
        private final boolean isComplete;

        private State(final boolean canFlush, final boolean canCommit, final boolean canAbort,
                final boolean isComplete) {
            this.canFlush = canFlush;
            this.canCommit = canCommit;
            this.canAbort = canAbort;
            this.isComplete = isComplete;
        }

        /**
         * Whether it is valid to {@link IsisTransaction#flush() flush} this
         * {@link IsisTransaction transaction}.
         */
        public boolean canFlush() {
            return canFlush;
        }

        /**
         * Whether it is valid to {@link IsisTransaction#commit() commit} this
         * {@link IsisTransaction transaction}.
         */
        public boolean canCommit() {
            return canCommit;
        }

        /**
         * Whether it is valid to {@link IsisTransaction#abort() abort} this
         * {@link IsisTransaction transaction}.
         */
        public boolean canAbort() {
            return canAbort;
        }

        /**
         * Whether the {@link IsisTransaction transaction} is complete (and so a
         * new one can be started).
         */
        public boolean isComplete() {
            return isComplete;
        }

    }

    private static final Logger LOG = Logger.getLogger(IsisTransaction.class);

    private final TransactionalResource objectStore;
    private final List<PersistenceCommand> commands = Lists.newArrayList();
    private final IsisTransactionManager transactionManager;
    private final MessageBroker messageBroker;
    private final UpdateNotifier updateNotifier;
    private final List<IsisException> exceptions = Lists.newArrayList();

    private State state;

    private RuntimeException cause;

    public IsisTransaction(final IsisTransactionManager transactionManager, final MessageBroker messageBroker,
            final UpdateNotifier updateNotifier, final TransactionalResource objectStore) {

        ensureThatArg(transactionManager, is(not(nullValue())), "transaction manager is required");
        ensureThatArg(messageBroker, is(not(nullValue())), "message broker is required");
        ensureThatArg(updateNotifier, is(not(nullValue())), "update notifier is required");

        this.transactionManager = transactionManager;
        this.messageBroker = messageBroker;
        this.updateNotifier = updateNotifier;

        this.state = State.IN_PROGRESS;

        this.objectStore = objectStore;
        if (LOG.isDebugEnabled()) {
            LOG.debug("new transaction " + this);
        }
    }

    // ////////////////////////////////////////////////////////////////
    // State
    // ////////////////////////////////////////////////////////////////

    public State getState() {
        return state;
    }

    private void setState(final State state) {
        this.state = state;
    }

    // //////////////////////////////////////////////////////////
    // Commands
    // //////////////////////////////////////////////////////////

    /**
     * Add the non-null command to the list of commands to execute at the end of
     * the transaction.
     */
    public void addCommand(final PersistenceCommand command) {
        if (command == null) {
            return;
        }

        final ObjectAdapter onObject = command.onAdapter();

        // Saves are ignored when preceded by another save, or a delete
        if (command instanceof SaveObjectCommand) {
            if (alreadyHasCreate(onObject) || alreadyHasSave(onObject)) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("ignored command as object already created/saved" + command);
                }
                return;
            }

            if (alreadyHasDestroy(onObject)) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("ignored command " + command + " as object no longer exists");
                }
                return;
            }
        }

        // Destroys are ignored when preceded by a create, or another destroy
        if (command instanceof DestroyObjectCommand) {
            if (alreadyHasCreate(onObject)) {
                removeCreate(onObject);
                if (LOG.isDebugEnabled()) {
                    LOG.debug("ignored both create and destroy command " + command);
                }
                return;
            }

            if (alreadyHasSave(onObject)) {
                removeSave(onObject);
                if (LOG.isDebugEnabled()) {
                    LOG.debug("removed prior save command " + command);
                }
            }

            if (alreadyHasDestroy(onObject)) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("ignored command " + command + " as command already recorded");
                }
                return;
            }
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug("add command " + command);
        }
        commands.add(command);
    }

    /////////////////////////////////////////////////////////////////////////
    // for worm-hole handling of exceptions
    /////////////////////////////////////////////////////////////////////////

    public void ensureExceptionsListIsEmpty() {
        Ensure.ensureThatArg(exceptions.isEmpty(), is(true), "exceptions list is not empty");
    }

    public void addException(IsisException exception) {
        exceptions.add(exception);
    }

    public List<IsisException> getExceptionsIfAny() {
        return Collections.unmodifiableList(exceptions);
    }

    // ////////////////////////////////////////////////////////////////
    // flush
    // ////////////////////////////////////////////////////////////////

    public synchronized final void flush() {
        ensureThatState(getState().canFlush(), is(true), "state is: " + getState());
        if (LOG.isDebugEnabled()) {
            LOG.debug("flush transaction " + this);
        }

        try {
            doFlush();
        } catch (final RuntimeException ex) {
            setState(State.MUST_ABORT);
            setAbortCause(ex);
            throw ex;
        }
    }

    /**
     * Mandatory hook method for subclasses to persist all pending changes.
     * 
     * <p>
     * Called by both {@link #commit()} and by {@link #flush()}:
     * <table>
     * <tr>
     * <th>called from</th>
     * <th>next {@link #getState() state} if ok</th>
     * <th>next {@link #getState() state} if exception</th>
     * </tr>
     * <tr>
     * <td>{@link #commit()}</td>
     * <td>{@link State#COMMITTED}</td>
     * <td>{@link State#ABORTED}</td>
     * </tr>
     * <tr>
     * <td>{@link #flush()}</td>
     * <td>{@link State#IN_PROGRESS}</td>
     * <td>{@link State#MUST_ABORT}</td>
     * </tr>
     * </table>
     */
    private void doFlush() {

        try {
            doAudit(getAuditEntries());

            objectStore.execute(Collections.unmodifiableList(commands));

            for (final PersistenceCommand command : commands) {
                if (command instanceof DestroyObjectCommand) {
                    final ObjectAdapter adapter = command.onAdapter();
                    adapter.setVersion(null);
                    adapter.changeState(ResolveState.DESTROYED);
                }
            }
        } finally {
            // even if there's an exception, we want to clear the commands
            // this is because the Wicket viewer uses an implementation of IsisContext 
            // whereby there are several threads which could be sharing the same context
            // if the first fails, we don't want the others to pick up the same command list
            // and try again
            commands.clear();
        }
    }

    /**
     * Hook method for subtypes to audit as required.
     */
    protected void doAudit(Set<Entry<AdapterAndProperty, PreAndPostValues>> auditEntries) {
        for (Entry<AdapterAndProperty, PreAndPostValues> auditEntry : auditEntries) {
            LOG.info(auditEntry.getKey() + ": " + auditEntry.getValue());
        }
    }

    // ////////////////////////////////////////////////////////////////
    // commit
    // ////////////////////////////////////////////////////////////////

    public synchronized final void commit() {

        ensureThatState(getState().canCommit(), is(true), "state is: " + getState());
        ensureThatState(exceptions.isEmpty(), is(true),
                "cannot commit: " + exceptions.size() + " exceptions have been raised");

        if (LOG.isDebugEnabled()) {
            LOG.debug("commit transaction " + this);
        }

        if (getState() == State.COMMITTED) {
            if (LOG.isInfoEnabled()) {
                LOG.info("already committed; ignoring");
            }
            return;
        }

        try {
            doFlush();
            setState(State.COMMITTED);
        } catch (final RuntimeException ex) {
            setAbortCause(ex);
            throw ex;
        }
    }

    // ////////////////////////////////////////////////////////////////
    // abort
    // ////////////////////////////////////////////////////////////////

    public synchronized final void abort() {
        ensureThatState(getState().canAbort(), is(true), "state is: " + getState());
        if (LOG.isInfoEnabled()) {
            LOG.info("abort transaction " + this);
        }

        setState(State.ABORTED);
    }

    private void setAbortCause(final RuntimeException cause) {
        this.cause = cause;
    }

    /**
     * The cause (if any) for the transaction being aborted.
     * 
     * <p>
     * Will be set if an exception is thrown while {@link #flush() flush}ing,
     * {@link #commit() commit}ting or {@link #abort() abort}ing.
     */
    public RuntimeException getAbortCause() {
        return cause;
    }

    // //////////////////////////////////////////////////////////
    // Helpers
    // //////////////////////////////////////////////////////////

    private boolean alreadyHasCommand(final Class<?> commandClass, final ObjectAdapter onObject) {
        return getCommand(commandClass, onObject) != null;
    }

    private boolean alreadyHasCreate(final ObjectAdapter onObject) {
        return alreadyHasCommand(CreateObjectCommand.class, onObject);
    }

    private boolean alreadyHasDestroy(final ObjectAdapter onObject) {
        return alreadyHasCommand(DestroyObjectCommand.class, onObject);
    }

    private boolean alreadyHasSave(final ObjectAdapter onObject) {
        return alreadyHasCommand(SaveObjectCommand.class, onObject);
    }

    private PersistenceCommand getCommand(final Class<?> commandClass, final ObjectAdapter onObject) {
        for (final PersistenceCommand command : commands) {
            if (command.onAdapter().equals(onObject)) {
                if (commandClass.isAssignableFrom(command.getClass())) {
                    return command;
                }
            }
        }
        return null;
    }

    private void removeCommand(final Class<?> commandClass, final ObjectAdapter onObject) {
        final PersistenceCommand toDelete = getCommand(commandClass, onObject);
        commands.remove(toDelete);
    }

    private void removeCreate(final ObjectAdapter onObject) {
        removeCommand(CreateObjectCommand.class, onObject);
    }

    private void removeSave(final ObjectAdapter onObject) {
        removeCommand(SaveObjectCommand.class, onObject);
    }

    // ////////////////////////////////////////////////////////////////
    // toString
    // ////////////////////////////////////////////////////////////////

    @Override
    public String toString() {
        return appendTo(new ToString(this)).toString();
    }

    protected ToString appendTo(final ToString str) {
        str.append("state", state);
        str.append("commands", commands.size());
        return str;
    }

    // ////////////////////////////////////////////////////////////////
    // Depenendencies (from constructor)
    // ////////////////////////////////////////////////////////////////

    /**
     * The owning {@link IsisTransactionManager transaction manager}.
     * 
     * <p>
     * Injected in constructor
     */
    public IsisTransactionManager getTransactionManager() {
        return transactionManager;
    }

    /**
     * The {@link MessageBroker} for this transaction.
     * 
     * <p>
     * Injected in constructor
     */
    public MessageBroker getMessageBroker() {
        return messageBroker;
    }

    /**
     * The {@link UpdateNotifier} for this transaction.
     * 
     * <p>
     * Injected in constructor
     */
    public UpdateNotifier getUpdateNotifier() {
        return updateNotifier;
    }

    public static class AdapterAndProperty {
        private final ObjectAdapter objectAdapter;
        private final ObjectAssociation property;

        public static AdapterAndProperty of(ObjectAdapter adapter, ObjectAssociation property) {
            return new AdapterAndProperty(adapter, property);
        }

        private AdapterAndProperty(ObjectAdapter adapter, ObjectAssociation property) {
            this.objectAdapter = adapter;
            this.property = property;
        }

        public ObjectAdapter getAdapter() {
            return objectAdapter;
        }

        public ObjectAssociation getProperty() {
            return property;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((objectAdapter == null) ? 0 : objectAdapter.hashCode());
            result = prime * result + ((property == null) ? 0 : property.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            AdapterAndProperty other = (AdapterAndProperty) obj;
            if (objectAdapter == null) {
                if (other.objectAdapter != null)
                    return false;
            } else if (!objectAdapter.equals(other.objectAdapter))
                return false;
            if (property == null) {
                if (other.property != null)
                    return false;
            } else if (!property.equals(other.property))
                return false;
            return true;
        }

        @Override
        public String toString() {
            return getAdapter().getOid().enStringNoVersion(getMarshaller()) + " , " + getProperty().getId();
        }

        protected OidMarshaller getMarshaller() {
            return new OidMarshaller();
        }

        private Object getPropertyValue() {
            ObjectAdapter referencedAdapter = property.get(objectAdapter);
            return referencedAdapter == null ? null : referencedAdapter.getObject();
        }
    }

    ////////////////////////////////////////////////////////////////////////
    // Auditing
    ////////////////////////////////////////////////////////////////////////

    public static class PreAndPostValues {

        private final static Predicate<Entry<?, PreAndPostValues>> CHANGED = new Predicate<Entry<?, PreAndPostValues>>() {
            @Override
            public boolean apply(Entry<?, PreAndPostValues> input) {
                final PreAndPostValues papv = input.getValue();
                return papv.differ();
            }
        };

        private final Object pre;
        private Object post;

        public static PreAndPostValues pre(Object preValue) {
            return new PreAndPostValues(preValue, null);
        }

        private PreAndPostValues(Object pre, Object post) {
            this.pre = pre;
            this.post = post;
        }

        public Object getPre() {
            return pre;
        }

        public Object getPost() {
            return post;
        }

        public void setPost(Object post) {
            this.post = post;
        }

        @Override
        public String toString() {
            return getPre() + " -> " + getPost();
        }

        public boolean differ() {
            return !Objects.equal(getPre(), getPost());
        }
    }

    private final Map<AdapterAndProperty, PreAndPostValues> auditLog = Maps.newLinkedHashMap();

    public void auditDirty(ObjectAdapter adapter) {
        for (ObjectAssociation property : adapter.getSpecification()
                .getAssociations(ObjectAssociationFilters.PROPERTIES)) {
            audit(adapter, property);
        }
    }

    private void audit(ObjectAdapter adapter, ObjectAssociation property) {
        final AdapterAndProperty aap = AdapterAndProperty.of(adapter, property);
        PreAndPostValues papv = PreAndPostValues.pre(aap.getPropertyValue());
        auditLog.put(aap, papv);
    }

    public Set<Entry<AdapterAndProperty, PreAndPostValues>> getAuditEntries() {
        updatePostValues(auditLog.entrySet());

        return Collections.unmodifiableSet(Sets.filter(auditLog.entrySet(), PreAndPostValues.CHANGED));
    }

    private void updatePostValues(Set<Entry<AdapterAndProperty, PreAndPostValues>> entrySet) {
        for (Entry<AdapterAndProperty, PreAndPostValues> entry : entrySet) {
            final AdapterAndProperty aap = entry.getKey();
            final PreAndPostValues papv = entry.getValue();

            papv.setPost(aap.getPropertyValue());
        }
    }

    ////////////////////////////////////////////////////////////////////////
    // Dependencies (from context)
    ////////////////////////////////////////////////////////////////////////

    protected AdapterManager getAdapterManager() {
        return IsisContext.getPersistenceSession().getAdapterManager();
    }

}