org.cruk.genologics.api.cache.GenologicsAPICache.java Source code

Java tutorial

Introduction

Here is the source code for org.cruk.genologics.api.cache.GenologicsAPICache.java

Source

/*
 * CRUK-CI Genologics REST API Java Client.
 * Copyright (C) 2013 Cancer Research UK Cambridge Institute.
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 */

package org.cruk.genologics.api.cache;

import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.cruk.genologics.api.GenologicsAPI;
import org.cruk.genologics.api.impl.GenologicsAPIImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;

import com.genologics.ri.GenologicsEntity;
import com.genologics.ri.LimsEntity;
import com.genologics.ri.LimsLink;
import com.genologics.ri.Linkable;
import com.genologics.ri.Locatable;
import com.genologics.ri.file.GenologicsFile;

import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Ehcache;
import net.sf.ehcache.Element;

/**
 * A cache optionally deployed around a {@code GenologicsAPI} bean as an aspect.
 * Looks to see what objects have already been fetched or created and, if they
 * are in the cache, doesn't go back to the server for them. Creates, updates and
 * deletes are passed through immediately.
 *
 * <p>
 * The "stateful" {@code Artifact} class produces some problems. There is more than
 * one way these could be handled, where an Artifact's state (specifically its QC flag)
 * will be different for different number of its "state" URI parameter.
 * This implementation uses a "latest available" strategy, where if the state
 * requested for an Artifact is earlier than the one in the cache, the cached version
 * is returned. If a later state is requested, it is fetched and replaces the one in
 * the cache.
 * This strategy may not be suitable for all cases and future refinements will allow
 * different strategies to be selected.
 * </p>
 */
@Aspect
public class GenologicsAPICache {
    /**
     * The version to use for objects and requests that give no state.
     */
    static final long NO_STATE_VALUE = 0L;

    /**
     * The part of the URI that specifies the state number.
     */
    private static final String STATE_TERM = "state=";

    /**
     * The length of the STATE_TERM string.
     */
    private static final int STATE_TERM_LENGTH = STATE_TERM.length();

    /**
     * Logger.
     */
    protected Logger logger = LoggerFactory.getLogger(GenologicsAPICache.class);

    /**
     * The API this aspect will call through to.
     */
    protected GenologicsAPI api;

    /**
     * The Ehcache cache manager.
     */
    protected CacheManager cacheManager;

    /**
     * The behaviour for dealing with stateful entities.
     */
    protected CacheStatefulBehaviour behaviour = CacheStatefulBehaviour.LATEST;

    /**
     * Lock to prevent the cache behaviour changing during an operation.
     */
    private Lock behaviourLock = new ReentrantLock();

    /**
     * Override for the cache behaviour for the next call.
     */
    private ThreadLocal<CacheStatefulBehaviour> behaviourOverride = new ThreadLocal<CacheStatefulBehaviour>();

    /**
     * Empty constructor.
     */
    public GenologicsAPICache() {
    }

    /**
     * Sets the API being used.
     *
     * @param api The GenologicsAPI bean.
     */
    @Required
    public void setGenologicsAPI(GenologicsAPI api) {
        this.api = api;
    }

    /**
     * Set the Ehcache cache manager.
     *
     * @param cacheManager The cache manager.
     */
    @Required
    public void setCacheManager(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    /**
     * Set the behaviour for dealing with stateful objects. Note that changing
     * this behaviour during operation clears the cache.
     *
     * @param behaviour The desired behaviour.
     *
     * @since 2.22
     */
    public void setStatefulBehaviour(CacheStatefulBehaviour behaviour) {
        if (behaviour != null && this.behaviour != behaviour) {
            behaviourLock.lock();
            try {
                this.behaviour = behaviour;

                // Guard for null so the order of Spring property setting
                // doesn't matter.

                if (cacheManager != null) {
                    cacheManager.clearAll();
                }
            } finally {
                behaviourLock.unlock();
            }
        }
    }

    /**
     * Clears the cache of all cached entities.
     */
    public void clear() {
        cacheManager.clearAll();
    }

    /**
     * Get an Ehcache for the given class.
     *
     * @param type The class for the cache required.
     *
     * @return The Ehcache for the class given. If there is no specific
     * cache defined for the class, use the general purpose "LimsEntity"
     * cache.
     *
     * @see #loadOrRetrieve(ProceedingJoinPoint, String, Class)
     */
    public Ehcache getCache(Class<?> type) {
        if (type == null) {
            throw new IllegalArgumentException("type cannot be null");
        }

        Ehcache cache = cacheManager.getEhcache(type.getName());
        if (cache == null) {
            cache = cacheManager.getEhcache(LimsEntity.class.getName());
        }

        return cache;
    }

    /**
     * Join point for changing the cach behaviour on the next call on the same thread.
     * Sets the thread local override behaviour to that given.
     *
     * <p>
     * The behaviour will be reset on any call to another API method from this thread.
     * </p>
     *
     * @param pjp The join point object.
     *
     * @return <code>null</code>
     *
     * @throws Throwable if there is an error.
     *
     * @see GenologicsAPI#nextCallCacheOverride(CacheStatefulBehaviour)
     * @see #resetBehaviour(JoinPoint)
     */
    public Object overrideBehaviour(ProceedingJoinPoint pjp) throws Throwable {
        Object[] args = pjp.getArgs();

        behaviourOverride.set((CacheStatefulBehaviour) args[0]);

        if (args[0] != null) {
            logger.debug("Next call will override the cache behaviour from {} to {}.", behaviour, args[0]);
        }

        return pjp.proceed();
    }

    /**
     * Resets the cache behaviour however it is set to normally behave. This
     * method is called after every public method call on the API, clearing the
     * override setting if it is set.
     *
     * @param jp The join point object.
     *
     * @see GenologicsAPI#nextCallCacheOverride(CacheStatefulBehaviour)
     * @see #overrideBehaviour(ProceedingJoinPoint)
     */
    public void resetBehaviour(JoinPoint jp) {
        String methodName = jp.getSignature().getName();

        if (behaviourOverride.get() != null && !"nextCallCacheOverride".equals(methodName)) {
            behaviourOverride.set(null);

            logger.debug("Reset cache behaviour override to {} (after call to {}).", behaviour, methodName);
        }
    }

    /**
     * Join point for the {@code GenologicsAPI.retrieve} methods. Fetches the
     * object requested, either from the cache or from the API.
     *
     * @param pjp The join point object.
     *
     * @return The object retrieved.
     *
     * @throws Throwable if there is an error.
     *
     * @see GenologicsAPI#retrieve(String, Class)
     * @see GenologicsAPI#retrieve(URI, Class)
     * @see #loadOrRetrieve(ProceedingJoinPoint, String, Class)
     */
    public Object retrieve(ProceedingJoinPoint pjp) throws Throwable {
        assert pjp.getArgs().length == 2 : "Wrong number of arguments.";

        Object thing = pjp.getArgs()[0];
        if (thing == null) {
            throw new IllegalArgumentException("uri cannot be null");
        }
        Class<?> entityClass = (Class<?>) pjp.getArgs()[1];
        String uri = thing.toString();

        return loadOrRetrieve(pjp, uri, entityClass);
    }

    /**
     * Join point for the {@code GenologicsAPI.load} methods taking an id and a class.
     * Fetches the object requested, either from the cache or from the API.
     *
     * @param pjp The join point object.
     *
     * @return The object retrieved.
     *
     * @throws Throwable if there is an error.
     *
     * @see GenologicsAPI#load(String, Class)
     * @see #loadOrRetrieve(ProceedingJoinPoint, String, Class)
     */
    public Object loadById(ProceedingJoinPoint pjp) throws Throwable {
        Object[] args = pjp.getArgs();

        Object id1, id2;
        Class<?> entityClass;
        String uri;

        switch (args.length) {
        case 2:
            id1 = args[0];
            if (id1 == null) {
                throw new IllegalArgumentException("limsid cannot be null");
            }
            entityClass = (Class<?>) args[1];
            uri = toUriString(entityClass, id1.toString());
            return loadOrRetrieve(pjp, uri, entityClass);

        case 3:
            id1 = args[0];
            if (id1 == null) {
                throw new IllegalArgumentException("outerLimsid cannot be null");
            }
            id2 = args[0];
            if (id2 == null) {
                throw new IllegalArgumentException("innerLimsid cannot be null");
            }
            entityClass = (Class<?>) args[2];
            uri = toUriString(entityClass, id1.toString(), id2.toString());
            return loadOrRetrieve(pjp, uri, entityClass);

        default:
            throw new AssertionError("Wrong number of arguments to a load by id method");
        }
    }

    /**
     * Join point for the {@code GenologicsAPI.load} methods taking a {@code LimsLink}.
     * Fetches the object requested, either from the cache or from the API.
     *
     * @param pjp The join point object.
     *
     * @return The object retrieved.
     *
     * @throws Throwable if there is an error.
     *
     * @see GenologicsAPI#load(LimsLink)
     * @see #loadOrRetrieve(ProceedingJoinPoint, String, Class)
     */
    public Object loadByLink(ProceedingJoinPoint pjp) throws Throwable {
        assert pjp.getArgs().length == 1 : "Wrong number of arguments.";

        LimsLink<?> link = (LimsLink<?>) pjp.getArgs()[0];
        if (link == null) {
            throw new IllegalArgumentException("link cannot be null");
        }
        if (link.getUri() == null) {
            throw new IllegalArgumentException("link uri cannot be null");
        }

        String uri = link.getUri().toString();

        return loadOrRetrieve(pjp, uri, link.getEntityClass());
    }

    /**
     * Get the object from its cache wrapper. In its own method to suppress
     * unchecked warnings.
     *
     * @param <E> The type of object held in the cache element.
     * @param wrapper The cache Element.
     *
     * @return The object in the cache element.
     */
    @SuppressWarnings("unchecked")
    protected <E extends Locatable> E getFromWrapper(Element wrapper) {
        return wrapper == null ? null : (E) wrapper.getObjectValue();
    }

    /**
     * Fetch an object from the cache or, if it's not yet been seen, from the
     * API and store the result in the cache for future use.
     *
     * <p>
     * Special consideration has to be made for objects that have a "state"
     * parameter to their URIs. See the class description for more details.
     * </p>
     *
     * @param pjp The join point object.
     * @param uri The URI of the object to fetch.
     * @param entityClass The type of object to fetch.
     *
     * @return The object retrieved.
     *
     * @throws Throwable if there is an error.
     */
    protected Object loadOrRetrieve(ProceedingJoinPoint pjp, String uri, Class<?> entityClass) throws Throwable {
        if (!isCacheable(entityClass)) {
            return pjp.proceed();
        }

        final boolean statefulEntity = isStateful(entityClass);

        final String className = ClassUtils.getShortClassName(entityClass);

        Ehcache cache = getCache(entityClass);

        CacheStatefulBehaviour callBehaviour = behaviourOverride.get();
        if (callBehaviour == null) {
            callBehaviour = behaviour;
        }

        Locatable genologicsObject = null;
        String key = keyFromUri(uri);
        long version = NO_STATE_VALUE;

        Element wrapper = null;
        if (key != null) {
            wrapper = cache.get(key);
            if (wrapper != null) {
                if (!statefulEntity) {
                    genologicsObject = getFromWrapper(wrapper);
                } else {
                    version = versionFromUri(uri);

                    switch (callBehaviour) {
                    case ANY:
                        genologicsObject = getFromWrapper(wrapper);
                        break;

                    case LATEST:
                        if (version == NO_STATE_VALUE || version <= wrapper.getVersion()) {
                            genologicsObject = getFromWrapper(wrapper);
                        }
                        break;

                    case EXACT:
                        if (version == NO_STATE_VALUE || version == wrapper.getVersion()) {
                            genologicsObject = getFromWrapper(wrapper);
                        }
                        break;
                    }
                }
            }
        }

        if (genologicsObject == null) {
            if (logger.isDebugEnabled()) {
                if (version == NO_STATE_VALUE) {
                    logger.debug("Don't have {} {} - calling through to API {}", className, key,
                            pjp.getSignature().getName());
                } else {
                    logger.debug("Have a different version of {} {} - calling through to API {}", className, key,
                            pjp.getSignature().getName());
                }
            }

            genologicsObject = (Locatable) pjp.proceed();

            if (wrapper == null) {
                // Not already in the cache, so it needs to be stored.
                cache.put(createCacheElement(genologicsObject));
            } else {
                // Most entities already in the cache will just stay there.
                // If though we have a stateful entity, there may be cause
                // to replace the object in the cache depending on how the
                // cache normally behaves. Typically this will be replacing the
                // existing with a newer version or replacing for a difference.
                // When we don't care about versions, the one already in the cache
                // can remain.

                if (statefulEntity) {
                    switch (behaviour) {
                    case ANY:
                        break;

                    case LATEST:
                        if (version > wrapper.getVersion()) {
                            cache.put(createCacheElement(genologicsObject));
                        }
                        break;

                    case EXACT:
                        if (version != wrapper.getVersion()) {
                            cache.put(createCacheElement(genologicsObject));
                        }
                        break;
                    }
                }
            }
        } else {
            if (logger.isDebugEnabled()) {
                logger.debug("Already have {} {} in the cache.", className, key);
            }
        }

        return genologicsObject;
    }

    /**
     * Join point for the {@code GenologicsAPI.loadAll} method.
     * Examines the cache for objects already loaded and only fetches those
     * that are not already seen (or, for stateful objects, those whose requested
     * state is later than that in the cache).
     *
     * @param <E> The type of LIMS entity referred to.
     * @param pjp The join point object.
     *
     * @return The list of entities retrieved.
     *
     * @throws Throwable if there is an error.
     *
     * @see GenologicsAPI#loadAll(Collection)
     */
    public <E extends Locatable> List<E> loadAll(ProceedingJoinPoint pjp) throws Throwable {
        @SuppressWarnings("unchecked")
        Collection<LimsLink<E>> links = (Collection<LimsLink<E>>) pjp.getArgs()[0];

        List<E> results = new ArrayList<E>(links == null ? 0 : links.size());

        if (links != null && !links.isEmpty()) {
            Ehcache cache = null;

            List<LimsLink<E>> toFetch = new ArrayList<LimsLink<E>>(links.size());
            List<E> alreadyCached = new ArrayList<E>(links.size());

            Boolean cacheable = null;
            String className = null;
            Boolean stateful = null;

            CacheStatefulBehaviour callBehaviour = behaviourOverride.get();
            if (callBehaviour == null) {
                callBehaviour = behaviour;
            }

            behaviourLock.lock();
            try {
                Iterator<LimsLink<E>> linkIterator = links.iterator();

                // Loop through the links requested and accumulate two lists of links:
                // those that are not in the cache and need to be fetched and those that
                // have already been fetched. While doing this, assemble in "results" those
                // entities already in the cache that don't need to be fetch. This list will
                // have nulls inserted where the entity needs to be fetched.

                while (linkIterator.hasNext()) {
                    LimsLink<E> link = linkIterator.next();
                    if (link == null) {
                        throw new IllegalArgumentException("link contains a null");
                    }
                    if (link.getUri() == null) {
                        throw new IllegalArgumentException("A link in the collection has no URI set.");
                    }

                    if (className == null) {
                        className = ClassUtils.getShortClassName(link.getEntityClass());
                        cacheable = isCacheable(link.getEntityClass());
                        stateful = isStateful(link.getEntityClass());
                    }

                    E entity = null;
                    if (!cacheable) {
                        // Fetch always.
                        toFetch.add(link);
                    } else {
                        if (cache == null) {
                            cache = getCache(link.getEntityClass());
                        }

                        String key = keyFromLocatable(link);

                        Element wrapper = cache.get(key);
                        if (wrapper == null) {
                            toFetch.add(link);
                        } else {
                            long version = versionFromLocatable(link);

                            switch (callBehaviour) {
                            case ANY:
                                entity = getFromWrapper(wrapper);
                                alreadyCached.add(entity);
                                break;

                            case LATEST:
                                if (version != NO_STATE_VALUE && version > wrapper.getVersion()) {
                                    toFetch.add(link);
                                } else {
                                    entity = getFromWrapper(wrapper);
                                    alreadyCached.add(entity);
                                }
                                break;

                            case EXACT:
                                if (version != NO_STATE_VALUE && version != wrapper.getVersion()) {
                                    toFetch.add(link);
                                } else {
                                    entity = getFromWrapper(wrapper);
                                    alreadyCached.add(entity);
                                }
                                break;
                            }
                        }
                    }
                    results.add(entity);
                }
            } finally {
                behaviourLock.unlock();
            }

            if (logger.isWarnEnabled()) {
                if (cache.getCacheConfiguration().getMaxEntriesLocalHeap() < links.size()) {
                    logger.warn(
                            "{} {}s are requested, but the cache will only hold {}. Repeated fetches of this collection will always call through to the API.",
                            links.size(), className, cache.getCacheConfiguration().getMaxEntriesLocalHeap());
                }
            }

            if (logger.isDebugEnabled()) {
                if (alreadyCached.size() == links.size()) {
                    logger.debug("All {} {}s requested are already in the cache.", links.size(), className);
                } else {
                    logger.debug("Have {} {}s in the cache; {} to retrieve.", alreadyCached.size(), className,
                            toFetch.size());
                }
            }

            // If there is anything to fetch, perform the call to the API then
            // fill in the nulls in the "results" list from the entities returned
            // from the API.
            // The end result is that newly fetched items are put into the cache
            // and "results" is a fully populated list.

            if (!toFetch.isEmpty()) {
                assert cacheable != null : "No cacheable flag found";
                assert stateful != null : "No stateful flag found";

                Object[] args = { toFetch };
                @SuppressWarnings("unchecked")
                List<E> fetched = (List<E>) pjp.proceed(args);

                ListIterator<E> resultIterator = results.listIterator();
                ListIterator<E> fetchIterator = fetched.listIterator();

                while (resultIterator.hasNext()) {
                    E entity = resultIterator.next();
                    if (entity == null) {
                        assert fetchIterator.hasNext() : "Run out of items in the fetched list.";
                        entity = fetchIterator.next();
                        resultIterator.set(entity);

                        if (cacheable) {
                            if (!stateful) {
                                // Entities without state will only have been fetched because they
                                // were not in the cache. These should just be added.

                                cache.put(createCacheElement(entity));
                            } else {
                                // Stateful entities may already be in the cache but may have been
                                // fetched because the requested version is newer or of a different
                                // state. Some care needs to be taken to update its cached version
                                // depending on how the cache normally behaves.

                                String key = keyFromLocatable(entity);
                                Element wrapper = cache.get(key);

                                if (wrapper == null) {
                                    // Not already cached, so simply add this entity whatever
                                    // its state.

                                    cache.put(createCacheElement(entity));
                                } else {
                                    // As we have a stateful entity, there may be cause
                                    // to replace the object in the cache depending on how the
                                    // cache normally behaves. Typically this will be replacing the
                                    // existing with a newer version or replacing for a difference.
                                    // When we don't care about versions, the one already in the cache
                                    // can remain.

                                    long version = versionFromLocatable(entity);

                                    switch (behaviour) {
                                    case ANY:
                                        break;

                                    case LATEST:
                                        if (version > wrapper.getVersion()) {
                                            cache.put(createCacheElement(entity));
                                        }
                                        break;

                                    case EXACT:
                                        if (version != wrapper.getVersion()) {
                                            cache.put(createCacheElement(entity));
                                        }
                                        break;
                                    }
                                }
                            }
                        }
                    }
                }
                assert !fetchIterator.hasNext() : "Have further items fetched after populating results list.";
            }
        }

        return results;
    }

    /**
     * Join point for the {@code GenologicsAPI.reload} method.
     * Force a reload from the API of an object by fetching again and updating
     * its cache entry.
     *
     * @param pjp The join point object.
     *
     * @throws Throwable if there is an error.
     *
     * @see GenologicsAPI#reload(LimsEntity)
     */
    public void reload(ProceedingJoinPoint pjp) throws Throwable {
        Locatable entity = (Locatable) pjp.getArgs()[0];

        pjp.proceed();

        if (isCacheable(entity)) {
            Ehcache cache = getCache(entity.getClass());
            cache.put(createCacheElement(entity));
        }
    }

    /**
     * Join point for the {@code GenologicsAPI.create} method.
     * Create the object through the API and, if it can be cached, record it
     * in the cache.
     *
     * @param pjp The join point object.
     *
     * @throws Throwable if there is an error.
     *
     * @see GenologicsAPI#create(Locatable)
     */
    public void create(ProceedingJoinPoint pjp) throws Throwable {
        Locatable entity = (Locatable) pjp.getArgs()[0];

        pjp.proceed();

        if (isCacheable(entity)) {
            Ehcache cache = getCache(entity.getClass());

            cache.put(createCacheElement(entity));
        }
    }

    /**
     * Join point for the {@code GenologicsAPI.createAll} method.
     * Create the objects through the API and, if they can be cached, record them
     * in the cache.
     *
     * @param pjp The join point object.
     *
     * @throws Throwable if there is an error.
     *
     * @see GenologicsAPI#createAll(Collection)
     */
    public void createAll(ProceedingJoinPoint pjp) throws Throwable {
        @SuppressWarnings("unchecked")
        Collection<Locatable> entities = (Collection<Locatable>) pjp.getArgs()[0];

        pjp.proceed();

        if (isCacheable(entities)) {
            Ehcache cache = null;

            for (Locatable entity : entities) {
                if (cache == null) {
                    cache = getCache(entity.getClass());
                }
                cache.put(createCacheElement(entity));
            }
        }
    }

    /**
     * Join point for the {@code GenologicsAPI.update} method.
     * Update the object through the API and, if it can be cached, update the record
     * in the cache.
     *
     * @param pjp The join point object.
     *
     * @throws Throwable if there is an error.
     *
     * @see GenologicsAPI#update(Locatable)
     */
    public void update(ProceedingJoinPoint pjp) throws Throwable {
        Locatable entity = (Locatable) pjp.getArgs()[0];

        pjp.proceed();

        if (isCacheable(entity)) {
            Ehcache cache = getCache(entity.getClass());
            cache.put(createCacheElement(entity));
        }
    }

    /**
     * Join point for the {@code GenologicsAPI.updateAll} method.
     * Update the objects through the API and, if they can be cached, update their records
     * in the cache.
     *
     * @param pjp The join point object.
     *
     * @throws Throwable if there is an error.
     *
     * @see GenologicsAPI#updateAll(Collection)
     */
    public void updateAll(ProceedingJoinPoint pjp) throws Throwable {
        @SuppressWarnings("unchecked")
        Collection<Locatable> entities = (Collection<Locatable>) pjp.getArgs()[0];

        pjp.proceed();

        if (isCacheable(entities)) {
            Ehcache cache = null;

            for (Locatable entity : entities) {
                if (cache == null) {
                    cache = getCache(entity.getClass());
                }
                cache.put(createCacheElement(entity));
            }
        }
    }

    /**
     * Join point for the {@code GenologicsAPI.delete} method.
     * Delete the object through the API and, if it can be cached, remove it from the cache.
     *
     * @param pjp The join point object.
     *
     * @throws Throwable if there is an error.
     *
     * @see GenologicsAPI#delete(Locatable)
     */
    public void delete(ProceedingJoinPoint pjp) throws Throwable {
        Locatable entity = (Locatable) pjp.getArgs()[0];

        String key = keyFromLocatable(entity);

        pjp.proceed();

        if (isCacheable(entity)) {
            Ehcache cache = getCache(entity.getClass());
            cache.remove(key);
        }
    }

    /**
     * Join point for the {@code GenologicsAPI.deleteAll} method.
     * Delete the objects through the API and, if they can be cached, remove their records
     * from the cache.
     *
     * @param pjp The join point object.
     *
     * @throws Throwable if there is an error.
     *
     * @see GenologicsAPI#deleteAll(Collection)
     */
    public void deleteAll(ProceedingJoinPoint pjp) throws Throwable {
        @SuppressWarnings("unchecked")
        Collection<Locatable> entities = (Collection<Locatable>) pjp.getArgs()[0];

        Ehcache cache = null;

        List<String> keys = null;
        if (isCacheable(entities)) {
            keys = new ArrayList<String>(entities.size());
            for (Locatable entity : entities) {
                if (entity != null && entity.getUri() != null) {
                    keys.add(keyFromLocatable(entity));

                    if (cache == null) {
                        cache = getCache(entity.getClass());
                    }
                }
            }
        }

        pjp.proceed();

        if (keys != null) {
            assert cache != null : "No cache set";
            cache.removeAll(keys);
        }
    }

    /**
     * Join point for the methods that take an object in to perform an operation and
     * return an object as a result (not necessarily object passed in). For example,
     * {@code GenologicsAPI.executeProcess} and {@code GenologicsAPI.beginProcessStep}.
     *
     * <p>Run the operation and store the resulting object in the cache (if cacheable).</p>
     *
     * @param pjp The join point object.
     *
     * @return The object created by the operation.
     *
     * @throws Throwable if there is an error.
     *
     * @see GenologicsAPI#executeProcess(com.genologics.ri.processexecution.ExecutableProcess)
     * @see GenologicsAPI#beginProcessStep(com.genologics.ri.step.StepCreation)
     */
    public Locatable runSomething(ProceedingJoinPoint pjp) throws Throwable {
        Locatable result = (Locatable) pjp.proceed();

        if (isCacheable(result)) {
            Ehcache cache = getCache(result.getClass());
            cache.put(createCacheElement(result));
        }

        return result;
    }

    /**
     * Join point for the {@code GenologicsAPI.uploadFile} method.
     * Call through to the API to do the work and store the resulting {@code GenologicsFile}
     * object in the cache.
     *
     * @param pjp The join point object.
     *
     * @return The file record in the Genologics LIMS.
     *
     * @throws Throwable if there is an error.
     *
     * @see GenologicsAPI#uploadFile(com.genologics.ri.LimsEntityLinkable, java.net.URL, boolean)
     */
    public GenologicsFile uploadFile(ProceedingJoinPoint pjp) throws Throwable {
        GenologicsFile file = (GenologicsFile) pjp.proceed();

        if (isCacheable(file)) {
            Ehcache cache = getCache(file.getClass());
            cache.put(createCacheElement(file));
        }

        return file;
    }

    /**
     * Join point for the {@code GenologicsAPI.deleteAndRemoveFile} method.
     * Call through to the API to do the work and remove the {@code GenologicsFile}
     * object from the cache.
     *
     * @param pjp The join point object.
     *
     * @throws Throwable if there is an error.
     *
     * @see GenologicsAPI#deleteAndRemoveFile(Linkable)
     */
    public void deleteAndRemoveFile(ProceedingJoinPoint pjp) throws Throwable {
        Linkable<?> file = (Linkable<?>) pjp.getArgs()[0];

        pjp.proceed();

        if (isCacheable(file)) {
            Ehcache cache = getCache(file.getClass());
            cache.remove(keyFromLocatable(file));
        }
    }

    /**
     * Assemble a URI for an object's id and class.
     *
     * <p>
     * Uses the reference to the GenologicsAPI to obtain the current
     * server address.
     * </p>
     *
     * <p>
     * This method essentially replicates {@link GenologicsAPIImpl#limsIdToUri(String, Class)}
     * but is looser on the class object it accepts for entityClass and doesn't create the
     * URI object for the identifier.
     * </p>
     *
     * @param entityClass The class of the entity.
     * @param ids The LIMS id(s) of the entity.
     *
     * @return A URI in string form for the entity.
     */
    protected String toUriString(Class<?> entityClass, String... ids) {
        GenologicsEntity entityAnno = entityClass.getAnnotation(GenologicsEntity.class);
        if (entityAnno == null) {
            throw new IllegalArgumentException("The class " + entityClass.getName()
                    + " has not been annotated with the GenologicsEntity annotation.");
        }

        StringBuilder uri = new StringBuilder(api.getServerApiAddress());

        if (entityAnno.primaryEntity() != void.class) {
            if (ids.length != 2) {
                throw new IllegalArgumentException(entityClass.getName()
                        + " has a single section endpoint in the API. " + "Use load(String, Class) for this type.");
            }

            GenologicsEntity primaryAnno = entityAnno.primaryEntity().getAnnotation(GenologicsEntity.class);
            if (primaryAnno == null) {
                throw new IllegalArgumentException("The class " + entityAnno.primaryEntity().getName()
                        + " has not been annotated with the GenologicsEntity annotation.");
            }

            uri.append(primaryAnno.uriSection()).append('/').append(ids[0]);
            uri.append(entityAnno.uriSection()).append('/').append(ids[1]);
        } else {
            if (ids.length != 1) {
                throw new IllegalArgumentException(
                        entityClass.getName() + " has a double section endpoint in the API. "
                                + "Use load(String, String, Class) for this type.");
            }

            uri.append(entityAnno.uriSection()).append('/').append(ids[0]);
            if (StringUtils.isNotEmpty(entityAnno.uriSubsection())) {
                uri.append('/').append(entityAnno.uriSubsection());
            }
        }

        return uri.toString();
    }

    /**
     * Test whether a collection of entities is cacheable. Finds the first
     * non-null item in the list an tests its {@code GenologicsEntity} annotation.
     * Assumes the collection is homogeneous in its content.
     *
     * @param entities The collection to test.
     *
     * @return {@code true} if the first non-null item is the list is cacheable,
     * {@code false} otherwise (including for a null or empty list).
     *
     * @see #isCacheable(Object)
     */
    public boolean isCacheable(Collection<?> entities) {
        if (entities != null) {
            for (Object thing : entities) {
                if (thing != null) {
                    return isCacheable(thing);
                }
            }
        }
        return false;
    }

    /**
     * Test whether an entity is cacheable. If the object given is not null,
     * examine its {@code GenologicsEntity} annotation for its "cacheable"
     * attribute and return that value.
     *
     * @param thing The object to test.
     *
     * @return {@code true} if {@code thing} is not null, is annotated with
     * the {@code GenologicsEntity} annotation, and that annotation has its
     * "cacheable" attribute set to true; {@code false} otherwise.
     *
     * @see GenologicsEntity#cacheable()
     */
    public boolean isCacheable(Object thing) {
        boolean cacheable = false;
        if (thing != null) {
            Class<?> entityClass = thing.getClass();
            if (LimsLink.class.isAssignableFrom(entityClass)) {
                entityClass = ((LimsLink<?>) thing).getEntityClass();
            }
            cacheable = isCacheable(entityClass);
        }
        return cacheable;
    }

    /**
     * Test whether entities of a class are cacheable. Tests whether the given
     * class is annotated with the {@code GenologicsEntity} annotation and, if
     * so, its "cacheable" attribute is set.
     *
     * @param entityClass The class to test.
     *
     * @return {@code true} if {@code entityClass} is annotated with
     * the {@code GenologicsEntity} annotation, and that annotation has its
     * "cacheable" attribute set to true; {@code false} otherwise.
     *
     * @see GenologicsEntity#cacheable()
     */
    public boolean isCacheable(Class<?> entityClass) {
        GenologicsEntity entityAnno = entityClass.getAnnotation(GenologicsEntity.class);
        return entityAnno != null && entityAnno.cacheable();
    }

    /**
     * Test whether a collection of entities is stateful. Finds the first
     * non-null item in the list an tests its {@code GenologicsEntity} annotation.
     * Assumes the collection is homogeneous in its content.
     *
     * @param entities The collection to test.
     *
     * @return {@code true} if the first non-null item is the list is stateful,
     * {@code false} otherwise (including for a null or empty list).
     *
     * @see #isStateful(Object)
     */
    public boolean isStateful(Collection<?> entities) {
        if (entities != null) {
            for (Object thing : entities) {
                if (thing != null) {
                    return isStateful(thing);
                }
            }
        }
        return false;
    }

    /**
     * Test whether an entity is stateful. If the object given is not null,
     * examine its {@code GenologicsEntity} annotation for its "stateful"
     * attribute and return that value.
     *
     * @param thing The object to test.
     *
     * @return {@code true} if {@code thing} is not null, is annotated with
     * the {@code GenologicsEntity} annotation, and that annotation has its
     * "stateful" attribute set to true; {@code false} otherwise.
     *
     * @see GenologicsEntity#stateful()
     */
    public boolean isStateful(Object thing) {
        boolean stateful = false;
        if (thing != null) {
            Class<?> entityClass = thing.getClass();
            if (LimsLink.class.isAssignableFrom(entityClass)) {
                entityClass = ((LimsLink<?>) thing).getEntityClass();
            }
            stateful = isStateful(entityClass);
        }
        return stateful;
    }

    /**
     * Test whether entities of a class are stateful. Tests whether the given
     * class is annotated with the {@code GenologicsEntity} annotation and, if
     * so, its "stateful" attribute is set.
     *
     * @param entityClass The class to test.
     *
     * @return {@code true} if {@code entityClass} is annotated with
     * the {@code GenologicsEntity} annotation, and that annotation has its
     * "stateful" attribute set to true; {@code false} otherwise.
     *
     * @see GenologicsEntity#stateful()
     */
    public boolean isStateful(Class<?> entityClass) {
        GenologicsEntity entityAnno = entityClass.getAnnotation(GenologicsEntity.class);
        return entityAnno != null && entityAnno.stateful();
    }

    /**
     * Create a cache element wrapper for the given entity. Extracts from the entity's URI
     * the cache key as the full path of the entity and, if relevant, the state
     * from the "state" part of the URI's query string.
     *
     * @param entity The entity to create a cache wrapper around.
     *
     * @return A Ehcache Element with the key, state (version) and entity set.
     */
    public Element createCacheElement(Locatable entity) {
        String key = keyFromLocatable(entity);
        long version = versionFromLocatable(entity);

        return new Element(key, entity, version);
    }

    /**
     * Extract the state value from the given object's URI, if such an entity can
     * have a state value.
     *
     * @param thing The entity to extract the state for.
     *
     * @return The state number, or {@code NO_STATE_VALUE} if either the object
     * is not a stateful object or there is no state information in the object's URI.
     */
    public long versionFromLocatable(Locatable thing) {
        return isStateful(thing) ? versionFromUri(thing.getUri()) : NO_STATE_VALUE;
    }

    /**
     * Extract the state value from a URI.
     *
     * @param uri The URI to dissect for a state value.
     *
     * @return The state number, or {@code NO_STATE_VALUE} if there is no state
     * information in the URI.
     */
    public long versionFromUri(URI uri) {
        return uri == null ? NO_STATE_VALUE : versionFromUri(uri.toString());
    }

    /**
     * Extract the state value from a string version of a URI.
     *
     * @param uri The URI string to dissect for a state value.
     *
     * @return The state number, or {@code NO_STATE_VALUE} if there is no state
     * information in the URI.
     */
    public long versionFromUri(String uri) {
        long version = NO_STATE_VALUE;
        int query = uri.indexOf('?');
        if (query >= 0) {
            int statePosition = uri.indexOf(STATE_TERM, query + 1);
            if (statePosition >= 0) {
                version = 0L;
                int length = uri.length();
                for (int i = statePosition + STATE_TERM_LENGTH; i < length; i++) {
                    char c = uri.charAt(i);
                    if (Character.isDigit(c)) {
                        version = version * 10 + (c - '0');
                    } else {
                        break;
                    }
                }
            }
        }
        return version;
    }

    /**
     * Extract the cache key value from the given object's URI. This is the
     * full path of the entity, less any query string.
     *
     * @param thing The entity to extract the key for.
     *
     * @return The cache key value.
     */
    public String keyFromLocatable(Locatable thing) {
        return thing == null ? null : keyFromUri(thing.getUri());
    }

    /**
     * Extract the cache key value from the given URI. This is the
     * full path of the entity, less any query string.
     *
     * @param uri The URI to extract the key from.
     *
     * @return The cache key value.
     */
    public String keyFromUri(URI uri) {
        return uri == null ? null : keyFromUri(uri.toString());
    }

    /**
     * Extract the cache key value from the given URI string. This is the
     * full path of the entity, less any query string.
     *
     * @param uri The URI string to extract the key from.
     *
     * @return The cache key value.
     */
    public String keyFromUri(String uri) {
        String key = uri;
        int query = key.indexOf('?');
        if (query > 0) {
            key = key.substring(0, query);
        }
        return key;
    }
}