org.openvpms.component.business.dao.hibernate.im.common.DOState.java Source code

Java tutorial

Introduction

Here is the source code for org.openvpms.component.business.dao.hibernate.im.common.DOState.java

Source

/*
 * Version: 1.0
 *
 * The contents of this file are subject to the OpenVPMS License Version
 * 1.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.openvpms.org/license/
 *
 * Software distributed under the License is distributed on an 'AS IS' basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * Copyright 2018 (C) OpenVPMS Ltd. All Rights Reserved.
 */

package org.openvpms.component.business.dao.hibernate.im.common;

import org.hibernate.StaleObjectStateException;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;
import org.openvpms.component.business.domain.im.common.IMObject;
import org.openvpms.component.business.domain.im.common.IMObjectReference;
import org.openvpms.component.model.object.Reference;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Manages the state of an {@link IMObjectDO} as it is being assembled by an {@link Assembler}.
 *
 * @author Tim Anderson
 */
public class DOState {

    /**
     * The object.
     */
    private final IMObjectDO object;

    /**
     * Determines if the object was new when the state was first created.
     */
    private final boolean isNew;

    /**
     * The source object. May be {@code null}.
     */
    private IMObject source;

    /**
     * The source version.
     */
    private long version;

    /**
     * The deferred assemblers.
     */
    private List<DeferredAssembler> deferred;

    /**
     * The reference updaters.
     */
    private List<ReferenceUpdater> updaters;

    /**
     * The reference update reverters, used to revert reference updates on transaction rollback.
     */
    private Map<Reference, ReferenceUpdater> reverters;

    /**
     * Child states of this state.
     */
    private Map<String, DOState> states;

    /**
     * Constructs a {@link DOState} for an object retrieved from the database.
     *
     * @param object the object
     */
    public DOState(IMObjectDO object) {
        this(object, null);
    }

    /**
     * Constructs a {@link DOState} for an object being assembled from an {@link IMObject}.
     *
     * @param object the object
     * @param source the source object
     */
    public DOState(IMObjectDO object, IMObject source) {
        this.object = object;
        this.source = source;
        isNew = (source != null) && source.isNew();
        version = (source != null) ? source.getVersion() : 0;
        if (!isNew && source != null) {
            if (source.getVersion() != object.getVersion()) {
                throw new StaleObjectStateException(object.getClass().getName(), object.getId());
            }
        }
    }

    /**
     * Returns the object.
     *
     * @return the object
     */
    public IMObjectDO getObject() {
        return object;
    }

    /**
     * Returns the source object.
     *
     * @return the source object, or {@code null} if the object is not
     * being assembled from an {@link IMObject}.
     */
    public IMObject getSource() {
        return source;
    }

    /**
     * Indicates whether some other object is "equal to" this one.
     * <p/>
     * Note that this uses the underlying {@link #getObject() object} to perform
     * equality. If the object has been lazily loaded, this will force it to
     * load.
     *
     * @param other the reference object with which to compare.
     * @return {@code true} if this object is the same as the obj
     * argument; {@code false} otherwise.
     */
    @Override
    public boolean equals(Object other) {
        return other == this || other instanceof DOState && object.equals(((DOState) other).object);
    }

    /**
     * Returns a hash code value for the object.
     * <p/>
     * Note that this uses the underlying {@link #getObject() object} to
     * calculate the hash code. If the object has been lazily loaded, this will
     * force it to load.
     *
     * @return a hash code value for this object
     */
    @Override
    public int hashCode() {
        return object.hashCode();
    }

    /**
     * Adds a state that is related to this state.
     *
     * @param state the related state
     */
    public void addState(DOState state) {
        if (states == null) {
            states = new LinkedHashMap<>();
        } else if (!state.isUninitialised()) {
            // remove any existing mapping for the state. Only relevant if
            // the object has been loaded subsequent to the state being added
            removeState(state.getObject());
        }
        states.put(state.getKey(), state);
    }

    /**
     * Removes a related state.
     *
     * @param object the object associated with the state
     */
    public void removeState(IMObjectDO object) {
        if (states != null) {
            if (states.remove(getKey(object)) == null) {
                if (!HibernateHelper.isUnintialised(object) && object.getId() != -1) {
                    // the object may have been loaded subsequent to the state
                    // being added, in which case its key has changed
                    object = (IMObjectDO) HibernateHelper.deproxy(object);
                    Class impl = object.getClass();
                    while (impl != IMObjectDOImpl.class && impl != Object.class) {
                        String key = impl.getName() + "#" + object.getId();
                        if (states.remove(key) != null) {
                            break;
                        }
                        impl = impl.getSuperclass();
                    }
                }
            }
        }
    }

    /**
     * Adds a deferred assembler.
     *
     * @param assembler the assembler to add
     */
    public void addDeferred(DeferredAssembler assembler) {
        if (deferred == null) {
            deferred = new ArrayList<>();
        }
        deferred.add(assembler);
    }

    /**
     * Removes a deferred assembler.
     *
     * @param assembler the assembler to remove
     */
    public void removeDeferred(DeferredAssembler assembler) {
        deferred.remove(assembler);
    }

    /**
     * Returns the deferred assemblers.
     *
     * @return the deferred assemblers
     */
    public Set<DeferredAssembler> getDeferred() {
        DeferredCollector collector = new DeferredCollector();
        collector.visit(this);
        return collector.getAssemblers();
    }

    /**
     * Adds a reference updater.
     *
     * @param updater the reference updater to add
     */
    public void addReferenceUpdater(ReferenceUpdater updater) {
        if (updaters == null) {
            updaters = new ArrayList<>();
        }
        updaters.add(updater);
    }

    /**
     * Determines if the object is complete. It is considered complete if
     * it or any of its related states have no deferred updaters registers.
     *
     * @return {@code true} if the object is complete; otherwise {@code false}
     */
    public boolean isComplete() {
        return new CompleteVistor().visit(this);
    }

    /**
     * Updates the identifiers and versions of the {@link IMObject}s associated
     * with this object.
     * <p/>
     * This is used to propagate identifier and version changes after a
     * transaction commits.
     *
     * @param context the context
     */
    public void updateIds(Context context) {
        new IdUpdater(context).visit(this);
    }

    /**
     * Returns the object, and any related objects added via {@link #addState}.
     *
     * @return the objects associated with the state
     */
    public Collection<IMObjectDO> getObjects() {
        ObjectCollector collector = new ObjectCollector();
        collector.visit(this);
        return collector.getObjects();
    }

    /**
     * Updates the state with a new instance of the source.
     *
     * @param source the new source instance
     * @throws StaleObjectStateException if the old and new versions aren't
     *                                   the same
     */
    public void update(IMObject source) {
        if (source.getVersion() != object.getVersion()) {
            throw new StaleObjectStateException(object.getClass().getName(), object.getId());
        }
        this.source = source;
        if (deferred != null) {
            deferred.clear();
        }
        if (updaters != null) {
            if (reverters == null) {
                reverters = new HashMap<>();
            }
            for (ReferenceUpdater updater : updaters) {
                if (!reverters.containsKey(updater.getReference())) {
                    reverters.put(updater.getReference(), updater);
                }
            }
            updaters.clear();
        }
    }

    /**
     * Destroys the state.
     */
    public void destroy() {
        new Cleaner().visit(this);
    }

    /**
     * Determines if the object associated with the state is an object which
     * is yet to be loaded from the database.
     *
     * @return {@code true} if the object is yet to be loaded from the database
     */
    public boolean isUninitialised() {
        return HibernateHelper.isUnintialised(object);
    }

    /**
     * Collects the {@link DeferredAssembler}s for each state reachable from the set of supplied states.
     *
     * @param states the states
     * @return all states, and their corresponding deferred assemblers
     */
    public static Map<DOState, Set<DeferredAssembler>> getDeferred(Collection<DOState> states) {
        Map<DOState, Set<DeferredAssembler>> result = new HashMap<>();
        DeferredCollector collector = new DeferredCollector();
        for (DOState state : states) {
            collector.visit(state);
            result.put(state, collector.getAssemblers());
        }
        return result;
    }

    /**
     * Returns a string representation of this.
     *
     * @return a string representation of this
     */
    public String toString() {
        StringBuilder builder = new StringBuilder();
        Visitor visitor = new Visitor() {
            boolean first = true;

            @Override
            protected boolean doVisit(DOState state) {
                if (!first) {
                    builder.append("\n -> ");
                } else {
                    first = false;
                }
                if (state.getSource() != null) {
                    builder.append(state.getSource().getObjectReference());
                } else if (state.isUninitialised()) {
                    builder.append("uninitialised=").append(state.getKey());
                } else {
                    builder.append(state.getObject().getObjectReference());
                }
                return true;
            }
        };
        visitor.visit(this);
        return builder.toString();
    }

    /**
     * Returns all objects from the specified states and their related states.
     * <p/>
     * This orders objects so that the top-level states are returned first, followed by those at lower levels.
     *
     * @param states the states
     * @return the objects
     */
    public static Collection<IMObjectDO> getObjects(Collection<DOState> states) {
        ObjectCollector collector = new ObjectCollector();
        for (DOState state : states) {
            collector.visitFirst(state);
        }
        for (DOState state : states) {
            collector.visit(state);
        }
        return collector.getObjects();
    }

    /**
     * Invoked after successful commit.
     * <p/>
     * This propagates identifier and version changes from the committed {@code IMObjectDO}s to their corresponding
     * {@code IMObject}s.
     *
     * @param states  the states to update
     * @param context the context
     */
    public static void updateIds(Collection<DOState> states, Context context) {
        IdUpdater updater = new IdUpdater(context);
        for (DOState state : states) {
            updater.visit(state);
        }
    }

    /**
     * Invoked on transaction rollback.
     * <p/>
     * This reverts identifier and version changes.
     *
     * @param states the states to update
     */
    public static void rollbackIds(Collection<DOState> states) {
        DOState.IdReverter reverter = new DOState.IdReverter();
        for (DOState state : states) {
            reverter.visit(state);
        }
    }

    /**
     * Returns a key for this state, to be used in sets and maps, to avoid
     * loading the underlying object from the database.
     *
     * @return a unique identifier for the state
     */
    protected String getKey() {
        return getKey(object);
    }

    /**
     * Returns a key for an object, to be used in sets and maps, to avoid
     * loading objects from the database.
     * <p/>
     * If the object is loaded, its link identifier will be used, otherwise
     * the concatenation of its persistent class name and id will be used.
     *
     * @param object the object
     * @return a unique identifier for the object
     */
    private String getKey(IMObjectDO object) {
        String id = null;
        if (object instanceof HibernateProxy) {
            HibernateProxy proxy = (HibernateProxy) object;
            LazyInitializer init = proxy.getHibernateLazyInitializer();
            if (init.isUninitialized()) {
                id = init.getPersistentClass().getName() + "#" + init.getIdentifier().toString();
            }
        }
        if (id == null) {
            id = object.getLinkId();
        }
        return id;
    }

    /**
     * Helper class to visit each reachable state once and only once.
     */
    private static abstract class Visitor {

        /**
         * The visited states, used to avoid visiting a state more than once.
         */
        private Set<String> visited = new HashSet<>();

        /**
         * Visits each state reachable from the specified state, invoking {@link #doVisit(DOState)}.
         * If the method returns {@code false}, iteration terminates.
         *
         * @param state the starting state
         * @return the result of {@link #doVisit(DOState)}.
         */
        public boolean visit(DOState state) {
            addVisited(state);
            boolean result = doVisit(state);
            if (result) {
                result = visitChildren(state);
            }
            return result;
        }

        /**
         * Visits each state reachable from the specified states, invoking {@link #doVisit(DOState)}.
         * If the method returns {@code false}, iteration terminates.
         *
         * @param states the states
         * @return the result of {@link #doVisit(DOState)}.
         */
        public boolean visit(List<DOState> states) {
            boolean result = true;
            for (DOState state : states) {
                result = visit(state);
                if (!result) {
                    break;
                }
            }
            return result;
        }

        /**
         * Visits the specified state.
         *
         * @param state the state to visit
         * @return {@code true} to visit any other state, {@code false} to
         * terminate
         */
        protected abstract boolean doVisit(DOState state);

        /**
         * Invokes {@link #visit(DOState)} for each child of the specified
         * state. If the method returns {@code false}, iteration terminates.
         *
         * @param state the state
         * @return the result of {@link #visit(DOState)}
         */
        protected boolean visitChildren(DOState state) {
            boolean result = true;
            Map<String, DOState> states = state.states;
            if (states != null) {
                for (DOState child : states.values()) {
                    if (!visited(child)) {
                        if (!visit(child)) {
                            result = false;
                            break;
                        }
                    }
                }
            }
            return result;
        }

        /**
         * Marks a state visited.
         *
         * @param state the visited state
         * @return {@code true} if the state hadn't already been visited
         */
        protected boolean addVisited(DOState state) {
            return visited.add(state.getKey());
        }

        /**
         * Determines if a state has been visited.
         *
         * @param state the state
         * @return {@code true} if the state has been visited
         */
        private boolean visited(DOState state) {
            return visited.contains(state.getKey());
        }
    }

    /**
     * Visitor that determines if a state is complete. A state is complete if
     * the are no deferred assemblers associated with it or its associated
     * states.
     */
    private static class CompleteVistor extends Visitor {

        /**
         * Visits the specified state.
         *
         * @param state the state to visit
         * @return {@code true} to visit any other state, {@code false} to
         * terminate
         */
        protected boolean doVisit(DOState state) {
            List<DeferredAssembler> deferred = state.deferred;
            return !(deferred != null && !deferred.isEmpty());
        }
    }

    /**
     * Visitor that updates the {@link IMObject} and the objects they reference
     * with the ids and versions of their corresponding {@link IMObjectDO}s.
     */
    private static class IdUpdater extends Visitor {

        /**
         * The context.
         */
        private final Context context;

        /**
         * Creates a new {@code IdUpdater}.
         *
         * @param context the context
         */
        public IdUpdater(Context context) {
            this.context = context;
        }

        /**
         * Visits the specified state.
         *
         * @param state the state to visit
         * @return {@code true}
         */
        protected boolean doVisit(DOState state) {
            IMObjectDO object = state.getObject();
            IMObject source = state.getSource();
            if (source != null) {
                source.setId(object.getId());
                source.setVersion(object.getVersion());
            }
            List<ReferenceUpdater> updaters = state.updaters;
            if (updaters != null) {
                for (ReferenceUpdater updater : updaters) {
                    DOState target = context.getCached(updater.getReference());
                    if (target != null) {
                        IMObjectReference ref = target.getObject().getObjectReference();
                        updater.doUpdate(ref);
                    }
                }
            }
            return true;
        }
    }

    /**
     * Visitor that reverts the ids and versions of {@link IMObject}s and the
     * objects they reference.
     */
    private static class IdReverter extends Visitor {

        /**
         * Visits the specified state.
         *
         * @param state the state to visit
         * @return {@code true}
         */
        protected boolean doVisit(DOState state) {
            if (state.isNew) {
                IMObject source = state.source;
                if (source != null) {
                    source.setId(-1);
                    source.setVersion(state.version);
                }
            }
            if (state.reverters != null) {
                for (ReferenceUpdater reverter : state.reverters.values()) {
                    reverter.revert();
                }
            }
            return true;
        }
    }

    /**
     * Visitor that collects {@link DeferredAssembler}s.
     */
    private static class DeferredCollector extends Visitor {

        /**
         * The collected assemblers.
         */
        private Set<DeferredAssembler> assemblers;

        /**
         * Empty helper.
         */
        private static final Set<DeferredAssembler> EMPTY = Collections.emptySet();

        /**
         * Returns the assemblers from the last visited state(s).
         * <p/>
         * This list is reset.
         *
         * @return the assemblers
         */
        public Set<DeferredAssembler> getAssemblers() {
            Set<DeferredAssembler> result = assemblers;
            assemblers = null;
            return (result != null) ? result : EMPTY;
        }

        /**
         * Visits the specified state.
         *
         * @param state the state to visit
         * @return {@code true}
         */
        protected boolean doVisit(DOState state) {
            List<DeferredAssembler> deferred = state.deferred;
            if (deferred != null && !state.deferred.isEmpty()) {
                if (assemblers == null) {
                    assemblers = new LinkedHashSet<>();
                }
                assemblers.addAll(deferred);
            }
            return true;
        }
    }

    /**
     * Visitor that destroys states.
     */
    private static class Cleaner extends Visitor {

        /**
         * Visits each state reachable from the specified state, invoking
         * {@link #doVisit(DOState)}.
         *
         * @param state the starting state
         * @return {@code true}
         */
        @Override
        public boolean visit(DOState state) {
            addVisited(state);
            visitChildren(state);
            doVisit(state);
            return true;
        }

        /**
         * Visits the specified state.
         *
         * @param state the state to visit
         * @return {@code true}
         */
        protected boolean doVisit(DOState state) {
            List<DeferredAssembler> deferred = state.deferred;
            if (deferred != null) {
                deferred.clear();
            }
            List<ReferenceUpdater> updaters = state.updaters;
            if (updaters != null) {
                updaters.clear();
            }
            Map<Reference, ReferenceUpdater> reverters = state.reverters;
            if (reverters != null) {
                reverters.clear();
            }
            Map<String, DOState> states = state.states;
            if (states != null) {
                states.clear();
            }
            return true;
        }
    }

    /**
     * Visitor that collects {@link IMObjectDO}s.
     */
    private static class ObjectCollector extends Visitor {

        /**
         * The collected objects.
         */
        private final Map<String, IMObjectDO> objects = new LinkedHashMap<>();

        /**
         * Returns the collected objects.
         *
         * @return the collected objects
         */
        public Collection<IMObjectDO> getObjects() {
            return objects.values();
        }

        /**
         * Visits the first state.
         *
         * @param state the state
         */
        public void visitFirst(DOState state) {
            addVisited(state);
            doVisit(state);
        }

        /**
         * Visits the specified state.
         *
         * @param state the state to visit
         * @return {@code true}
         */
        protected boolean doVisit(DOState state) {
            IMObjectDO object = state.getObject();
            objects.put(state.getKey(), object);
            return true;
        }
    }

}