ome.services.graphs.GraphState.java Source code

Java tutorial

Introduction

Here is the source code for ome.services.graphs.GraphState.java

Source

/*
 *   $Id$
 *
 *   Copyright 2010 Glencoe Software, Inc. All rights reserved.
 *   Use is subject to license terms supplied in LICENSE.txt
 */

package ome.services.graphs;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import ome.model.IObject;
import ome.security.basic.CurrentDetails;
import ome.services.messages.EventLogMessage;
import ome.system.EventContext;
import ome.system.OmeroContext;
import ome.tools.hibernate.ExtendedMetadata;
import ome.tools.hibernate.QueryBuilder;
import ome.util.SqlAction;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.exception.ConstraintViolationException;
import org.perf4j.StopWatch;
import org.perf4j.commonslog.CommonsLogStopWatch;

/**
 * Tree-structure containing all scheduled deletes which closely resembles the
 * tree structure of the {@link GraphSpec} itself. All ids of the intended
 * deletes will be collected in a preliminary phase. This is necessary since
 * intermediate deletes, may disconnect the graph, causing later deletes to fail
 * if they were solely based on the id of the root element.
 *
 * The {@link GraphState} instance can only be initialized with a graph of
 * initialized {@GraphSpec}s.
 *
 * To handle SOFT requirements, each new attempt to delete either a node or a
 * leaf in the subgraph is surrounded by a savepoint. Ids added during a
 * savepoint (or a sub-savepoint) or only valid until release is called, at
 * which time they are merged into the final view.
 *
 * @author Josh Moore, josh at glencoesoftware.com
 * @since Beta4.2.3
 */
public class GraphState implements GraphStep.Callback {

    private final static Log log = LogFactory.getLog(GraphState.class);
    /**
     * List of each individual {@link GraphStep} which this instance will
     * perform.
     */
    private final List<GraphStep> steps = new ArrayList<GraphStep>();

    /**
     * List of Maps of db table names to the ids actually deleted from that
     * table. The first entry of the list are the actual results. All later
     * elements are temporary views from some savepoint.
     *
     * TODO : refactor into {@link GraphStep}
     */
    private final LinkedList<Map<String, Set<Long>>> actualIds = new LinkedList<Map<String, Set<Long>>>();

    /**
     * Map from table name to the {@link IObject} class which will be deleted
     * for raising the {@link EventLogMessage}.
     */
    private final Map<String, Class<IObject>> classes = new HashMap<String, Class<IObject>>();

    private final GraphOpts opts = new GraphOpts();

    private final Session session;

    private final GraphStepFactory factory;

    private final SqlAction sql;

    /**
     * @param ctx
     *            Stored the {@link OmeroContext} instance for raising event
     *            during {@link #release(String)}
     * @param session
     *            non-null, active Hibernate session that will be used to delete
     *            all necessary items as well as lookup items for deletion.
     */
    public GraphState(GraphStepFactory factory, SqlAction sql, Session session, GraphSpec spec)
            throws GraphException {
        this.sql = sql;
        this.session = session;
        this.factory = factory;

        add(); // Set the actualIds size==1

        final GraphTables tables = new GraphTables();
        descend(spec, tables);

        final LinkedList<GraphStep> stack = new LinkedList<GraphStep>();
        parse(spec, tables, stack, null);

    }

    //
    // Initialization and id lookup
    //

    /**
     * Walk throw the sub-spec graph actually loading the ids which must be
     * scheduled for delete.
     *
     * @param spec
     * @param paths
     * @throws GraphException
     */
    private void descend(GraphSpec spec, GraphTables tables) throws GraphException {

        final List<GraphEntry> entries = spec.entries();

        for (int i = 0; i < entries.size(); i++) {

            final GraphEntry entry = entries.get(i);
            if (entry.skip()) { // after opts.push()
                if (log.isDebugEnabled()) {
                    log.debug("Skipping " + entry);
                }
                continue;
            }

            final GraphSpec subSpec = entry.getSubSpec();
            final long[][] results = spec.queryBackupIds(session, i, entry, null);
            tables.add(entry, results);
            if (subSpec != null) {
                if (results.length != 0) { // ticket:2823
                    descend(subSpec, tables);
                }
            }
        }
    }

    /**
     * Walk throw the sub-spec graph again, using the results provided to build
     * up a graph of {@link GraphStep} instances.
     */
    private void parse(GraphSpec spec, GraphTables tables, LinkedList<GraphStep> stack, long[] match)
            throws GraphException {

        final List<GraphEntry> entries = spec.entries();

        for (int i = 0; i < entries.size(); i++) {
            final GraphEntry entry = entries.get(i);
            final GraphSpec subSpec = entry.getSubSpec();

            Iterator<List<long[]>> it = tables.columnSets(entry, match);
            while (it.hasNext()) {
                List<long[]> columnSet = it.next();
                if (columnSet.size() == 0) {
                    continue;
                }

                // For the spec containers, we create a single step
                // per column-set.

                if (subSpec != null) {
                    GraphStep step = factory.create(steps.size(), stack, spec, entry, null);

                    stack.add(step);
                    parse(subSpec, tables, stack, columnSet.get(0));
                    stack.removeLast();
                    this.steps.add(step);
                } else {

                    // But for the actual entries, we create a step per
                    // individual row.
                    for (long[] cols : columnSet) {
                        GraphStep step = factory.create(steps.size(), stack, spec, entry, cols);
                        this.steps.add(step);
                    }

                }

            }
        }
    }

    //
    // Found and deleted Ids
    //

    /**
     * Return the total number of ids loaded into this instance.
     */
    public long getTotalFoundCount() {
        return steps.size();
    }

    /**
     * Return the total number of ids which were processed. This is calculated by
     * taking the only the completed savepoints into account.
     */
    public long getTotalProcessedCount() {
        int count = 0;
        for (Map.Entry<String, Set<Long>> entry : actualIds.getFirst().entrySet()) {
            count += entry.getValue().size();
        }
        return count;
    }

    /**
     * Get the set of ids which were actually processed. See
     * {@link #addAll(String, Class, List)}
     */
    public Set<Long> getProcessedIds(String table) {
        Set<Long> set = lookup(table);
        if (set == null) {
            return new HashSet<Long>();
        } else {
            return Collections.unmodifiableSet(set);
        }
    }

    /**
     * Add the actually deleted ids to the current savepoint.
     *
     * It is critical that these ids are actually deleted and that any failure
     * for them to be removed will cause the entire transaction to fail (in
     * which case these ids will be ignored).
     *
     * @throws GraphException
     *             thrown if the {@link EventLogMessage} raised fails.
     */
    void addGraphIds(GraphStep step) throws GraphException {

        classes.put(step.table, step.iObjectType);
        Set<Long> set = lookup(step.table);
        set.add(step.id);

    }

    //
    // Iteration methods, used for actual deletes
    //

    /**
     *
     * @param step
     *            which step is to be invoked. Running a step multiple times is
     *            not supported.
     *
     * @return Any warnings which were noted during execution.
     * @throws GraphException
     *             Any errors which were caused during execution. Which
     *             execution states may be encountered is strongly tied to the
     *             definition of the specification and to the options which are
     *             passed in during initialization.
     */
    public String execute(int j) throws GraphException {

        final GraphStep step = steps.get(j);

        String msgOrNull = step.start(this);
        if (msgOrNull != null) {
            return msgOrNull; // EARLY EXIT
        }

        // Add this instance to the opts. Any method which then tries to
        // ask the opts for the current state will have an accurate view.
        step.push(opts);

        try {

            // Lazy initialization of parents.
            // To guarantee that finalization
            // happens (#3125, #3130), a special
            // marker is added and handled above.
            for (GraphStep parent : step.stack) {
                if (!parent.hasSavepoint()) {
                    parent.savepoint(this);
                }
            }
            step.savepoint(this);

            final QueryBuilder nullOp = optionalNullBuilder(step);
            final QueryBuilder qb = queryBuilder(step);

            try {

                // Phase 1: top-levels
                if (step.stack.size() <= 1) {
                    StopWatch swTop = new CommonsLogStopWatch();
                    step.spec.runTopLevel(session, Arrays.<Long>asList(step.id));
                    swTop.stop("omero.delete.top." + step.id);
                }

                // Phase 2: NULL
                optionallyNullField(session, nullOp, step.id);

                // Phase 3: primary delete
                StopWatch swStep = new CommonsLogStopWatch();
                qb.param("id", step.id);
                Query q = qb.query(session);
                int count = q.executeUpdate();
                if (count > 0) {
                    addGraphIds(step);
                }
                logResults(step, count);
                swStep.lap("omero.delete." + step.table + "." + step.id);

                // Finalize.
                step.release(this);
                return "";

            } catch (ConstraintViolationException cve) {
                return handleConstraintViolation(step, cve);
            }

        } finally {
            step.pop(opts);
        }
    }

    private void logResults(final GraphStep step, final int count) {
        if (count > 0) {
            if (log.isDebugEnabled()) {
                log.debug(String.format("Graphd %s from %s: root=%s", step.id, step.pathMsg, step.entry.getId()));
            }
        } else {
            if (log.isWarnEnabled()) {
                log.warn(String.format("Missing delete %s from %s: root=%s", step.id, step.pathMsg,
                        step.entry.getId()));
            }
        }
    }

    /**
     * Method called when a {@link ConstraintViolationException} can be thrown.
     * This is both during
     * {@link #delete(Session, CurrentDetails, ExtendedMetadata, String, GraphState, List, GraphOpts)}
     * and
     * {@link #execute(Session, CurrentDetails, ExtendedMetadata, GraphState, List, GraphOpts)}
     * .
     *
     * @param session
     * @param opts
     * @param type
     * @param rv
     * @param cve
     */
    private String handleConstraintViolation(final GraphStep step, ConstraintViolationException cve)
            throws GraphException {

        // First, immediately rollback the current savepoint.
        step.rollback(this);

        String cause = "ConstraintViolation: " + cve.getConstraintName();
        String msg = String.format("Could not delete softly %s: %s due to %s", step.pathMsg, step.id, cause);

        // If this entry is "SOFT" then there's nothing
        // special we need to do.
        if (step.entry.isSoft()) {
            log.debug(msg);
            return "Skipping delete of " + step.table + ":" + step.id + "\n";
        }

        // Otherwise calculate if there is any "SOFT" setting about this
        // location in the graph, and clean up all of the related entries.
        // As we check down the stack, we can safely call rollback since
        // the only other option is to rollback the entire transaction.
        for (int i = step.stack.size() - 1; i >= 0; i--) {
            GraphStep parent = step.stack.get(i);
            parent.rollback(this);
            if (parent.entry.isSoft()) {
                disableRelatedEntries(parent);
                log.debug(String.format("%s. Handled by %s: %s", msg, parent.pathMsg, parent.id));
                return cause;
            }
        }

        log.info(String.format("Failed to delete %s: %s due to %s", step.pathMsg, step.id, cause));
        throw cve;

    }

    /**
     * Finds all {@link GraphStep} instances in {@link #steps} which have the
     * given {@link GraphStep} argument in their {@link GraphStep#stack} which
     * amounts to being a descedent. All such instances are set to null in
     * {@link #steps} so that further processing cannot take place on them.
     */
    private void disableRelatedEntries(GraphStep parent) {
        for (GraphStep step : steps) {
            if (step == null || step.stack == null) {
                continue;
            } else if (step.stack.contains(parent)) {
                step.rollbackOnly();
            }
        }
    }

    private QueryBuilder optionalNullBuilder(final GraphStep step) {
        QueryBuilder nullOp = null;
        if (step.entry.isNull()) { // WORKAROUND see #2776, #2966
            // If this is a null operation, we don't want to delete the row,
            // but just modify a value. NB: below we also prevent this from
            // being raised as a delete event. TODO: refactor out to Op
            nullOp = new QueryBuilder();
            nullOp.update(step.table);
            nullOp.append("set relatedTo = null ");
            nullOp.where();
            nullOp.and("relatedTo.id = :id");
        }
        return nullOp;
    }

    private void optionallyNullField(Session session, final QueryBuilder nullOp, Long id) {
        if (nullOp != null) {
            nullOp.param("id", id);
            Query q = nullOp.query(session);
            int updated = q.executeUpdate();
            if (log.isDebugEnabled()) {
                log.debug("Nulled " + updated + " Pixels.relatedTo fields");
            }
        }
    }

    private QueryBuilder queryBuilder(GraphStep step) {
        final QueryBuilder qb = new QueryBuilder();
        qb.delete(step.table);
        qb.where();
        qb.and("id = :id");
        if (!opts.isForce()) {
            permissionsClause(step.ec, qb);
        }
        return qb;
    }

    /**
     * Appends a clause to the {@link QueryBuilder} based on the current user.
     *
     * If the user is an admin like root, then nothing is appened, and any
     * delete is permissible. If the user is a leader of the current group, then
     * the object must be in the current group. Otherwise, the object must
     * belong to the current user.
     */
    public static void permissionsClause(EventContext ec, QueryBuilder qb) {
        if (!ec.isCurrentUserAdmin()) {
            if (ec.getLeaderOfGroupsList().contains(ec.getCurrentGroupId())) {
                qb.and("details.group.id = :gid");
                qb.param("gid", ec.getCurrentGroupId());
            } else {
                // This is only a regular user, then the object must belong to
                // him/her
                qb.and("details.owner.id = :oid");
                qb.param("oid", ec.getCurrentUserId());
            }
        }
    }

    //
    // Helpers
    //

    /**
     * Lookup and initialize if necessary a {@link Set<Long>} for the given
     * table.
     *
     * @param table
     * @return
     */
    private Set<Long> lookup(String table) {
        Set<Long> set = actualIds.getLast().get(table);
        if (set == null) {
            set = new HashSet<Long>();
            actualIds.getLast().put(table, set);
        }
        return set;
    }

    //
    // Callback methods
    //

    public Class<IObject> getClass(String key) {
        return classes.get(key);
    }

    public void add() {
        actualIds.add(new HashMap<String, Set<Long>>());
    }

    public int size() {
        return actualIds.size();
    }

    public Iterable<Map.Entry<String, Set<Long>>> entrySet() {
        return actualIds.getLast().entrySet();
    }

    public int collapse(boolean keep) {
        int count = 0;
        final Map<String, Set<Long>> ids = actualIds.removeLast();
        if (keep) {
            // Update the next map up with the current values
            for (Map.Entry<String, Set<Long>> entry : ids.entrySet()) {
                String key = entry.getKey();
                Map<String, Set<Long>> last = actualIds.getLast();
                Set<Long> old = last.get(key);
                Set<Long> neu = entry.getValue();
                count += neu.size();
                if (old == null) {
                    last.put(key, neu);
                } else {
                    old.addAll(neu);
                }
            }
        } else {
            for (String key : ids.keySet()) {
                Set<Long> old = ids.get(key);
                count += old.size();
            }
        }
        return count;
    }

    public void savepoint(String savepoint) {

        sql.createSavepoint(savepoint);
        log.debug(String.format("Enter savepoint %s: new depth=%s", savepoint, actualIds.size()));

    }

    public void release(String savepoint, int count) throws GraphException {

        if (actualIds.size() == 0) {
            throw new GraphException("Release at depth 0!");
        }

        sql.releaseSavepoint(savepoint);

        log.debug(String.format("Released savepoint %s with %s ids: new depth=%s", savepoint, count,
                actualIds.size()));

    }

    public void rollback(String savepoint, int count) throws GraphException {

        if (actualIds.size() == 0) {
            throw new GraphException("Release at depth 0!");
        }

        sql.rollbackSavepoint(savepoint);

        log.debug(String.format("Rolled back savepoint %s with %s ids: new depth=%s", savepoint, count,
                actualIds.size()));

    }

    //
    // Misc
    //

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(super.toString());
        sb.append("\n");
        for (int i = 0; i < steps.size(); i++) {
            GraphStep step = steps.get(i);
            sb.append(i);
            sb.append(":");
            if (step == null) {
                sb.append("null");
            } else {
                sb.append(step.pathMsg);
                sb.append("==>");
                sb.append(step.id);
                sb.append(" ");
                sb.append("[");
                sb.append(step.stack);
                sb.append("]");
                sb.append("\n");
            }
        }
        return sb.toString();
    }

}