ome.services.query.HierarchyNavigator.java Source code

Java tutorial

Introduction

Here is the source code for ome.services.query.HierarchyNavigator.java

Source

/*
 * Copyright (C) 2013-2014 University of Dundee & Open Microscopy Environment.
 * 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 ome.services.query;

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

import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;

import ome.api.IQuery;
import ome.parameters.Parameters;

/**
 * Query the database for relationships between model objects.
 * Caches results, so designed for a short lifetime.
 * @author m.t.b.carroll@dundee.ac.uk
 * @since 5.0
 */
public class HierarchyNavigator {
    /* This class and {@link HierarchyWrap} are designed to make it easy to adjust the Java types
     * via which the model object hierarchy is navigated, and to make the HQL queries efficient
     * (batching, caching), at the small expense of constructing instances of simple Java objects.
     * The methods are not public to avoid polluting users of subclasses of {@link HierarchyWrap}.
     */

    /** HQL queries to map from ID of first target type to that of the second */
    private static final ImmutableMap<Map.Entry<String, String>, String> hqlFromTo;

    static {
        /* note that there is not yet any treatment of /PlateAcquisition or /WellSample */
        final Builder<Map.Entry<String, String>, String> builder = ImmutableMap.builder();
        builder.put(Maps.immutableEntry("/Project", "/Dataset"),
                "SELECT parent.id, child.id FROM ProjectDatasetLink WHERE parent.id IN (:" + Parameters.IDS + ")");
        builder.put(Maps.immutableEntry("/Dataset", "/Image"),
                "SELECT parent.id, child.id FROM DatasetImageLink WHERE parent.id IN (:" + Parameters.IDS + ")");
        builder.put(Maps.immutableEntry("/Screen", "/Plate"),
                "SELECT parent.id, child.id FROM ScreenPlateLink WHERE parent.id IN (:" + Parameters.IDS + ")");
        builder.put(Maps.immutableEntry("/Plate", "/Well"),
                "SELECT plate.id, id FROM Well WHERE plate.id IN (:" + Parameters.IDS + ")");
        builder.put(Maps.immutableEntry("/Well", "/Image"),
                "SELECT well.id, image.id FROM WellSample WHERE well.id IN (:" + Parameters.IDS + ")");
        builder.put(Maps.immutableEntry("/Fileset", "/Image"),
                "SELECT fileset.id, id FROM Image WHERE fileset.id IN (:" + Parameters.IDS + ")");
        builder.put(Maps.immutableEntry("/Image", "/Fileset"),
                "SELECT id, fileset.id FROM Image WHERE fileset.id IS NOT NULL AND id IN (:" + Parameters.IDS
                        + ")");
        builder.put(Maps.immutableEntry("/Image", "/Well"),
                "SELECT image.id, well.id FROM WellSample WHERE image.id IN (:" + Parameters.IDS + ")");
        builder.put(Maps.immutableEntry("/Well", "/Plate"),
                "SELECT id, plate.id FROM Well WHERE id IN (:" + Parameters.IDS + ")");
        builder.put(Maps.immutableEntry("/Plate", "/Screen"),
                "SELECT child.id, parent.id FROM ScreenPlateLink WHERE child.id IN (:" + Parameters.IDS + ")");
        builder.put(Maps.immutableEntry("/Image", "/Dataset"),
                "SELECT child.id, parent.id FROM DatasetImageLink WHERE child.id IN (:" + Parameters.IDS + ")");
        builder.put(Maps.immutableEntry("/Dataset", "/Project"),
                "SELECT child.id, parent.id FROM ProjectDatasetLink WHERE child.id IN (:" + Parameters.IDS + ")");
        hqlFromTo = builder.build();
    }

    /** available query service */
    protected final IQuery iQuery;

    /** cache of query results */
    private final ModelObjectCache cache = new ModelObjectCache();

    /**
     * Construct a new hierarchy navigator.
     * @param iQuery the query service
     */
    protected HierarchyNavigator(IQuery iQuery) {
        this.iQuery = iQuery;
    }

    /**
     * Perform the database query to discover the IDs of the related objects.
     * @param toType the type of the objects to which the query object may be related, not <code>null</code>
     * @param fromType the query object's type, not <code>null</code>
     * @param fromIds the query objects' database IDs, none <code>null</code>
     * @return pairs of database IDs: of the query object, and an object to which it relates
     */
    private List<Object[]> doQuery(String toType, String fromType, Collection<Long> fromIds) {
        final String queryString = hqlFromTo.get(Maps.immutableEntry(fromType, toType));
        if (queryString == null) {
            throw new IllegalArgumentException("not implemented for " + fromType + " to " + toType);
        }
        return this.iQuery.projection(queryString, new Parameters().addIds(fromIds));
    }

    /**
     * Batch bulk database queries to prime the cache for {@link #doLookup(String, String, Long)}.
     * It is not necessary to call this method, but it is advised if many lookups are anticipated.
     * @param toType the type of the objects to which the query objects may be related, not <code>null</code>
     * @param fromType the query object's type, not <code>null</code>
     * @param fromIds the query objects' database IDs, none <code>null</code>
     */
    protected void prepareLookups(String toType, String fromType, Collection<Long> fromIds) {
        /* note which query object IDs have not already had results cached */
        final Set<Long> fromIdsToQuery = new HashSet<Long>(fromIds);
        for (final long fromId : fromIds) {
            if (cache.getFromCache(fromType, fromId, toType) != null) {
                fromIdsToQuery.remove(fromId);
            }
        }
        if (fromIdsToQuery.isEmpty()) {
            /* ... all of them are already cached */
            return;
        }
        /* collate the results from multiple batches */
        final SetMultimap<Long, Long> fromIdsToIds = HashMultimap.create();
        for (final List<Long> fromIdsToQueryBatch : Iterables.partition(fromIdsToQuery, 256)) {
            for (final Object[] queryResult : doQuery(toType, fromType, fromIdsToQueryBatch)) {
                fromIdsToIds.put((Long) queryResult[0], (Long) queryResult[1]);
            }
        }
        /* cache the results by query object */
        for (final Entry<Long, Collection<Long>> fromIdToIds : fromIdsToIds.asMap().entrySet()) {
            cache.putIntoCache(fromType, fromIdToIds.getKey(), toType, ImmutableSet.copyOf(fromIdToIds.getValue()));
        }
        /* note empty results so that the database is not again queried */
        for (final Long fromId : Sets.difference(fromIdsToQuery, fromIdsToIds.keySet())) {
            cache.putIntoCache(fromType, fromId, toType, ImmutableSet.<Long>of());
        }
    }

    /**
     * Look up which objects of a given type relate to the given query object.
     * Caches results, and one may bulk-cache results in advance using {@link #prepareLookups(String, String, Collection)}.
     * @param toType the type of the objects to which the query object may be related, not <code>null</code>
     * @param fromType the query object's type, not <code>null</code>
     * @param fromIds the query object's database ID, not <code>null</code>
     * @return the related objects' database IDs, never <code>null</code>
     */
    protected ImmutableSet<Long> doLookup(String toType, String fromType, Long fromId) {
        final ImmutableSet<Long> result = cache.getFromCache(fromType, fromId, toType);
        if (result == null) {
            /* cache miss, so query the single object */
            final ImmutableSet.Builder<Long> toIdsBuilder = ImmutableSet.builder();
            for (final Object[] queryResult : doQuery(toType, fromType, Collections.singleton(fromId))) {
                toIdsBuilder.add((Long) queryResult[1]);
            }
            final ImmutableSet<Long> toIds = toIdsBuilder.build();
            cache.putIntoCache(fromType, fromId, toType, toIds);
            return toIds;
        } else {
            /* cache hit */
            return result;
        }
    }
}