omero.cmd.graphs.Preprocessor.java Source code

Java tutorial

Introduction

Here is the source code for omero.cmd.graphs.Preprocessor.java

Source

/*
 * Copyright (C) 2013-2014 Glencoe Software, Inc. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

package omero.cmd.graphs;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.lang.builder.HashCodeBuilder;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;

import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.SetMultimap;

import ome.api.IQuery;
import ome.services.query.HierarchyNavigatorWrap;
import omero.cmd.Chgrp;
import omero.cmd.Delete;
import omero.cmd.DoAll;
import omero.cmd.GraphModify;
import omero.cmd.Helper;
import omero.cmd.Request;

/**
 * Preprocessors have a chance to modify the list of
 * {@link Request} instances which are passed to a
 * {@link DoAll}. If this strategy is continued, this
 * should be refactored behind a discoverable interface.
 *
 * @author Josh Moore, josh at glencoesoftware.com
 * @author m.t.b.carroll@dundee.ac.uk
 * @since 5.0
 */
public class Preprocessor {

    private final static Logger log = LoggerFactory.getLogger(Preprocessor.class);

    /**
     * The types of target for which we care about adjusting graph operation requests.
     * @author m.t.b.carroll@dundee.ac.uk
     * @since 5.0
     */
    public static enum TargetType {
        IMAGE, FILESET, DATASET, PROJECT, WELL, PLATE, SCREEN;

        /** lookup from initial-capital name to type, e.g. "/Image" to {@link TargetType#IMAGE} */
        public static final ImmutableMap<String, TargetType> byName;
        /** lookup from type to initial-capital name, e.g. {@link TargetType#IMAGE} to "/Image" */
        public static final ImmutableMap<TargetType, String> byType;

        static {
            final ImmutableMap.Builder<String, TargetType> byNameBuilder = ImmutableMap.builder();
            final ImmutableMap.Builder<TargetType, String> byTypeBuilder = ImmutableMap.builder();
            for (final TargetType value : TargetType.values()) {
                final String name = '/' + value.name().substring(0, 1) + value.name().substring(1).toLowerCase();
                byNameBuilder.put(name, value);
                byTypeBuilder.put(value, name);
            }
            byName = byNameBuilder.build();
            byType = byTypeBuilder.build();
        }
    }

    /**
     * A tuple of target type and target ID.
     * Instances with the same type and ID are {@link #equals(Object)} and have the same {@link #hashCode()}.
     * @author m.t.b.carroll@dundee.ac.uk
     * @since 5.0
     */
    public static class GraphModifyTarget {
        public final TargetType targetType;
        public final long targetId;

        /**
         * Construct a target type-ID tuple.
         * @param targetType the target type
         * @param targetId the target ID
         */
        public GraphModifyTarget(TargetType targetType, long targetId) {
            if (targetType == null) {
                throw new IllegalArgumentException("target type must be set");
            }
            this.targetType = targetType;
            this.targetId = targetId;
        }

        /**
         * Return only the targets of the given type.
         * @param targets some targets
         * @param targetType the desired type of target
         * @return the targets of the desired type
         */
        public static List<GraphModifyTarget> filterByType(Collection<GraphModifyTarget> targets,
                TargetType targetType) {
            final List<GraphModifyTarget> filtered = new ArrayList<GraphModifyTarget>();
            for (final GraphModifyTarget target : targets) {
                if (target.targetType == targetType) {
                    filtered.add(target);
                }
            }
            return filtered;
        }

        @Override
        public boolean equals(Object o) {
            if (o == this) {
                return true;
            }
            if (o instanceof GraphModifyTarget) {
                final GraphModifyTarget same = (GraphModifyTarget) o;
                return this.targetType == same.targetType && this.targetId == same.targetId;
            } else {
                return false;
            }
        }

        @Override
        public int hashCode() {
            return new HashCodeBuilder().append(this.targetType.ordinal()).append(this.targetId).toHashCode();
        }

        @Override
        public String toString() {
            return this.targetType + ":" + this.targetId;
        }
    }

    /** the target type hierarchy, an ordered list descending from higher to lower */
    protected static final ImmutableList<Entry<TargetType, TargetType>> targetTypeHierarchy;

    static {
        final com.google.common.collect.ImmutableList.Builder<Entry<TargetType, TargetType>> builder = ImmutableList
                .builder();
        builder.add(Maps.immutableEntry(TargetType.PROJECT, TargetType.DATASET));
        builder.add(Maps.immutableEntry(TargetType.DATASET, TargetType.IMAGE));
        builder.add(Maps.immutableEntry(TargetType.SCREEN, TargetType.PLATE));
        builder.add(Maps.immutableEntry(TargetType.PLATE, TargetType.WELL));
        builder.add(Maps.immutableEntry(TargetType.WELL, TargetType.IMAGE));
        builder.add(Maps.immutableEntry(TargetType.FILESET, TargetType.IMAGE));
        targetTypeHierarchy = builder.build();
    }

    protected final List<Request> requests;

    protected final HierarchyNavigatorWrap<TargetType, GraphModifyTarget> hierarchyNavigator;

    /**
     * Transform the list of requests.
     * A prohibited prefix must be in the list, and the first must precede any prohibited suffixes.
     * @param isRelevant which type of graph operation's requests to transform
     * @param newRequestTarget the target of the new request to create to replace the removed ones
     * @param prohibitedPrefixes the requests that may not precede the new request
     * @param prohibitedSuffixes the requests that may be omitted after the new request
     */
    protected final void transform(Predicate<Request> isRelevant, GraphModifyTarget newRequestTarget,
            Set<GraphModifyTarget> prohibitedPrefixes, Set<GraphModifyTarget> prohibitedSuffixes) {

        boolean added = false;

        for (int index = this.requests.size() - 1; index >= 0; index--) {

            if (!isRelevant.apply(requests.get(index))) {
                continue;
            }

            final GraphModify request = (GraphModify) this.requests.get(index);
            final TargetType targetType = TargetType.byName.get(request.type);
            if (targetType == null) {
                continue;
            }

            /* it is a relevant request with an understood target type */
            final GraphModifyTarget requestTarget = new GraphModifyTarget(targetType, request.id);
            /* must we now prefix it? */
            if (!added && prohibitedPrefixes.contains(requestTarget)) {
                added = true;
                /* FIXME: this does not modify user-set options */
                final GraphModify newRequest = (GraphModify) ((IGraphModifyRequest) request).copy();
                newRequest.type = TargetType.byType.get(newRequestTarget.targetType);
                newRequest.id = newRequestTarget.targetId;
                this.requests.add(index + 1, newRequest);
            }
            /* must we remove it? */
            if (prohibitedSuffixes.contains(requestTarget)) {
                if (!added) {
                    throw new IllegalArgumentException(
                            "some prohibited prefix must occur before any prohibited suffixes");
                }
                this.requests.remove(index);
            }
        }
        if (!added) {
            throw new IllegalArgumentException("no prohibited prefix is among the requests");
        }
    }

    public Preprocessor(List<Request> requests, Helper helper) {
        final IQuery iQuery = helper.getServiceFactory().getQueryService();

        this.hierarchyNavigator = new HierarchyNavigatorWrap<TargetType, GraphModifyTarget>(iQuery) {
            @Override
            protected String typeToString(TargetType type) {
                return TargetType.byType.get(type);
            }

            @Override
            protected TargetType stringToType(String typeName) {
                return TargetType.byName.get(typeName);
            }

            @Override
            protected Entry<String, Long> entityToStringLong(GraphModifyTarget entity) {
                return Maps.immutableEntry(TargetType.byType.get(entity.targetType), entity.targetId);
            }

            @Override
            protected GraphModifyTarget stringLongToEntity(String typeName, long id) {
                return new GraphModifyTarget(TargetType.byName.get(typeName), id);
            }
        };

        this.requests = requests;

        process();
    }

    public Preprocessor(List<Request> requests,
            HierarchyNavigatorWrap<TargetType, GraphModifyTarget> hierarchyNavigator) {
        this.hierarchyNavigator = hierarchyNavigator;
        this.requests = requests;

        process();
    }

    /**
     * Returns a copy of the requests field or an empty list if null.
     */
    public List<Request> getRequests() {
        if (requests == null) {
            return new ArrayList<Request>();
        }
        return new ArrayList<Request>(requests);
    }

    /**
     * Recursively find all the direct and indirect containers of the given target.
     * @param contained a target
     * @return all the target's containers
     */
    private Set<GraphModifyTarget> getAllContainers(GraphModifyTarget contained) {
        final Set<GraphModifyTarget> allContainers = new HashSet<GraphModifyTarget>();
        final Set<GraphModifyTarget> pendingContained = new HashSet<GraphModifyTarget>();
        pendingContained.add(contained);
        while (!pendingContained.isEmpty()) {
            final Iterator<GraphModifyTarget> pendingContainedsIterator = pendingContained.iterator();
            final GraphModifyTarget nextContained = pendingContainedsIterator.next();
            pendingContainedsIterator.remove();
            final Set<GraphModifyTarget> nextContainers = new HashSet<GraphModifyTarget>();
            for (final Entry<TargetType, TargetType> relationship : targetTypeHierarchy) {
                if (relationship.getValue() == nextContained.targetType) {
                    nextContainers.addAll(hierarchyNavigator.doLookup(relationship.getKey(), nextContained));
                }
            }
            allContainers.addAll(nextContainers);
            pendingContained.addAll(nextContainers);
        }
        return allContainers;
    }

    /**
     * Generate predicates where each identifies a distinct group of requests that should be processed together.
     * @return the predicates to guide request list processing
     */
    private Collection<Predicate<Request>> predicatesForRequests() {
        final Set<Long> chgrpRequestGroups = new HashSet<Long>();
        boolean deleteRequest = false;

        final List<Predicate<Request>> predicates = new ArrayList<Predicate<Request>>();
        for (final Request request : this.requests) {
            if (!(request instanceof GraphModify) || ((GraphModify) request).type == null) {
                // do nothing
            } else if (request instanceof Delete && !deleteRequest) {
                deleteRequest = true;
                predicates.add(new Predicate<Request>() {
                    public boolean apply(Request request) {
                        return Delete.class.isAssignableFrom(request.getClass())
                                && TargetType.byName.get(((GraphModify) request).type) != null;
                    }
                });
            } else if (request instanceof Chgrp) {
                final long targetGroup = ((Chgrp) request).grp;
                if (chgrpRequestGroups.add(targetGroup)) {
                    predicates.add(new Predicate<Request>() {
                        public boolean apply(Request request) {
                            return Chgrp.class.isAssignableFrom(request.getClass())
                                    && ((Chgrp) request).grp == targetGroup
                                    && TargetType.byName.get(((GraphModify) request).type) != null;
                        }
                    });
                }
            }
        }
        return predicates;
    }

    /**
     * Preprocess the list of requests.
     */
    protected void process() {
        boolean modified = false;
        for (final Predicate<Request> isRelevant : predicatesForRequests()) {
            final SetMultimap<TargetType, GraphModifyTarget> targets = HashMultimap.create();

            /* direct references */

            for (final Request relevantRequest : Collections2.filter(this.requests, isRelevant)) {
                final GraphModify gm = (GraphModify) relevantRequest;
                final TargetType targetType = TargetType.byName.get(gm.type);
                targets.put(targetType, new GraphModifyTarget(targetType, gm.id));
            }

            /* indirect references */

            for (final Entry<TargetType, TargetType> relationship : Preprocessor.targetTypeHierarchy) {
                final TargetType containerType = relationship.getKey();
                final TargetType containedType = relationship.getValue();
                final Set<GraphModifyTarget> containers = targets.get(containerType);
                this.hierarchyNavigator.prepareLookups(containedType, containers);
                for (final GraphModifyTarget container : containers) {
                    targets.putAll(containedType, this.hierarchyNavigator.doLookup(containedType, container));
                }
            }

            /* review the referenced FS images */
            this.hierarchyNavigator.prepareLookups(TargetType.FILESET, targets.get(TargetType.IMAGE));
            while (!targets.get(TargetType.IMAGE).isEmpty()) {
                final GraphModifyTarget image = targets.get(TargetType.IMAGE).iterator().next();
                /* find the image's fileset */
                final Set<GraphModifyTarget> containers = this.hierarchyNavigator.doLookup(TargetType.FILESET,
                        image);
                final Iterator<GraphModifyTarget> filesetIterator = GraphModifyTarget
                        .filterByType(containers, TargetType.FILESET).iterator();
                final GraphModifyTarget fileset;
                if (!filesetIterator.hasNext()) {
                    /* pre-FS image */
                    targets.get(TargetType.IMAGE).remove(image);
                    continue;
                }
                fileset = filesetIterator.next();
                if (filesetIterator.hasNext()) {
                    throw new IllegalStateException("image is contained in multiple filesets");
                }
                /* check that all the fileset's images are referenced */
                final Set<GraphModifyTarget> filesetImages = this.hierarchyNavigator.doLookup(TargetType.IMAGE,
                        fileset);
                final boolean completeFileset = targets.get(TargetType.IMAGE).containsAll(filesetImages);
                /* this iteration will handle this fileset sufficiently, so do not revisit it */
                targets.get(TargetType.IMAGE).removeAll(filesetImages);
                /* this preprocessing is applied only when all of a fileset's images are referenced */
                if (!(completeFileset)) {
                    continue;
                }
                /* okay, list the fileset as a target among the requests, after all of its images and their containers */
                final Set<GraphModifyTarget> prohibitedPrefixes = new HashSet<GraphModifyTarget>(filesetImages);
                for (final GraphModifyTarget filesetImage : filesetImages) {
                    prohibitedPrefixes.addAll(getAllContainers(filesetImage));
                }
                /* and, once listed, neither the fileset nor its images need subsequent requests */
                final Set<GraphModifyTarget> prohibitedSuffixes = new HashSet<GraphModifyTarget>(filesetImages);
                prohibitedSuffixes.add(fileset);
                /* adjust the list of requests accordingly */
                transform(isRelevant, fileset, prohibitedPrefixes, prohibitedSuffixes);
                modified = true;
            }
        }

        if (modified && log.isDebugEnabled()) {
            StringBuilder sb = new StringBuilder();
            sb.append(requestToString(requests.get(0)));
            for (int i = 1; i < requests.size(); i++) {
                sb.append(",");
                sb.append(requestToString(requests.get(i)));
            }
            log.debug("transformed to: {}", sb);
        }
    }

    /**
     * Convert a single request to a pretty string.
     */
    protected String requestToString(final Request request) {
        StringBuilder requestString = new StringBuilder();
        if (request instanceof Delete) {
            requestString.append("DELETE");
        } else if (request instanceof Chgrp) {
            requestString.append("CHGRP");
            requestString.append('(');
            requestString.append(((Chgrp) request).grp);
            requestString.append(')');
        } else {
            requestString.append('?');
        }
        if (request instanceof GraphModify) {
            final GraphModify graphModify = (GraphModify) request;
            requestString.append('[');
            requestString.append(graphModify.type);
            requestString.append(':');
            requestString.append(graphModify.id);
            requestString.append(']');
        }
        return requestString.toString();
    }
}