org.flite.cach3.aop.CacheBase.java Source code

Java tutorial

Introduction

Here is the source code for org.flite.cach3.aop.CacheBase.java

Source

/*
 * Copyright (c) 2011-2013 Flite, Inc
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package org.flite.cach3.aop;

import net.spy.memcached.*;
import org.apache.commons.lang.*;
import org.apache.commons.lang.math.*;
import org.apache.velocity.*;
import org.apache.velocity.app.*;
import org.aspectj.lang.*;
import org.aspectj.lang.Signature;
import org.aspectj.lang.reflect.*;
import org.flite.cach3.annotations.*;
import org.flite.cach3.api.*;
import org.flite.cach3.config.*;
import org.flite.cach3.exceptions.*;

import java.io.*;
import java.lang.reflect.*;
import java.security.*;
import java.util.*;

public class CacheBase {

    static final String SEPARATOR = ":";

    //   protected MemcachedClientIF cache;
    CacheKeyMethodStore methodStore;
    protected Cach3State state;
    protected VelocityContextFactory factory;

    public void setFactory(VelocityContextFactory factory) {
        this.factory = factory;
    }

    public void setState(Cach3State state) {
        this.state = state;
    }

    protected boolean isCacheDisabled() {
        return state == null ? false : state.isCacheDisabled();
    }

    protected int getJitterDefault() {
        return state.getJitterDefault();
    }

    public void setMethodStore(CacheKeyMethodStore methodStore) {
        this.methodStore = methodStore;
    }

    public static Method getMethodToCache(final JoinPoint jp) throws NoSuchMethodException {
        final Signature sig = jp.getSignature();
        if (!(sig instanceof MethodSignature)) {
            throw new InvalidAnnotationException("This annotation is only valid on a method.");
        }
        final MethodSignature msig = (MethodSignature) sig;
        final Object target = jp.getTarget();
        return target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
    }

    public static String generateObjectId(final Method keyMethod, final Object keyObject) throws Exception {
        final String objectId = (String) keyMethod.invoke(keyObject, null);
        if (objectId == null || objectId.length() < 1) {
            throw new RuntimeException("Got an empty key value from " + keyMethod.getName());
        }
        return objectId;
    }

    protected static final char[] WS = new char[] { ' ', '\n', '\t' };

    public static String buildCacheKey(final String objectId, final String namespace, final String prefix) {
        if (objectId == null || objectId.length() < 1) {
            throw new InvalidParameterException("Ids for objects in the cache must be at least 1 character long.");
        }
        final StringBuilder result = new StringBuilder(namespace).append(SEPARATOR);
        if (StringUtils.isNotBlank(prefix)) {
            result.append(prefix);
        }
        result.append(objectId);
        if (result.length() > 255) {
            throw new InvalidParameterException(
                    "Ids for objects in the cache must not exceed 255 characters: [" + result.toString() + "]");
        }
        final String resultString = result.toString();
        if (StringUtils.containsAny(resultString, WS)) {
            throw new InvalidParameterException(
                    "Ids for objects in the cache must not have whitespace: [" + result.toString() + "]");
        }
        return resultString;
    }

    public static Object getIndexObject(final int index, final Object retVal, final Object[] args,
            final String methodString) throws Exception {
        return getIndexObject(true, index, retVal, args, methodString);
    }

    public static Object getIndexObject(final int index, final Object[] args, final String methodString)
            throws Exception {
        return getIndexObject(false, index, null, args, methodString);
    }

    public static Object getIndexObject(final boolean allowRetVal, final int index, final Object retVal,
            final Object[] args, final String methodString) throws Exception {
        if (index < -1 || (index == -1 && !allowRetVal)) {
            throw new InvalidParameterException(String.format("An index of %s is invalid", index));
        }
        if (args.length <= index) {
            throw new InvalidParameterException(String
                    .format("An index of %s is too big for the number of arguments in [%s]", index, methodString));
        }
        final Object indexObject = index == -1 ? retVal : args[index];
        if (indexObject == null) {
            throw new InvalidParameterException(
                    String.format("The argument passed into [%s] at index %s is null.", methodString, index));
        }
        return indexObject;
    }

    public static Object validateReturnValueAsKeyObject(final Object returnValue, final Method methodToCache)
            throws Exception {
        if (returnValue == null) {
            throw new InvalidParameterException(String.format(
                    "The result of the method [%s] is null, which will not give an appropriate cache key.",
                    methodToCache.toString()));
        }
        return returnValue;
    }

    protected Method getKeyMethod(final Object keyObject) throws NoSuchMethodException {
        return getKeyMethod(keyObject, methodStore);
    }

    public static Method getKeyMethod(final Object keyObject, final CacheKeyMethodStore methodStore)
            throws NoSuchMethodException {
        final Method storedMethod = methodStore.find(keyObject.getClass());
        if (storedMethod != null) {
            return storedMethod;
        }
        final Method[] methods = keyObject.getClass().getDeclaredMethods();
        Method targetMethod = null;
        for (final Method method : methods) {
            if (method != null && method.getAnnotation(CacheKeyMethod.class) != null) {
                if (method.getParameterTypes().length > 0) {
                    throw new InvalidAnnotationException(
                            String.format("Method [%s] must have 0 arguments to be annotated with [%s]",
                                    method.toString(), CacheKeyMethod.class.getName()));
                }
                if (!String.class.equals(method.getReturnType())) {
                    throw new InvalidAnnotationException(
                            String.format("Method [%s] must return a String to be annotated with [%s]",
                                    method.toString(), CacheKeyMethod.class.getName()));
                }
                if (targetMethod != null) {
                    throw new InvalidAnnotationException(String.format(
                            "Class [%s] should have only one method annotated with [%s]. See [%s] and [%s]",
                            keyObject.getClass().getName(), CacheKeyMethod.class.getName(), targetMethod.getName(),
                            method.getName()));
                }
                targetMethod = method;
            }
        }

        if (targetMethod == null) {
            targetMethod = keyObject.getClass().getMethod("toString", null);
        }

        methodStore.add(keyObject.getClass(), targetMethod);

        return targetMethod;
    }

    public static void verifyReturnTypeIsList(final Method method, final Class annotationClass) {
        if (verifyTypeIsList(method.getReturnType())) {
            return;
        }
        throw new InvalidAnnotationException(String.format(
                "The annotation [%s] is only valid on a method that returns a [%s]. "
                        + "[%s] does not fulfill this requirement by returning [%s].",
                annotationClass.getName(), List.class.getName(), method.toString(),
                method.getReturnType().getName()));
    }

    protected static boolean verifyTypeIsList(final Class clazz) {
        return java.util.List.class.isAssignableFrom(clazz);
    }

    protected static boolean verifyTypeIsLong(final Class clazz) {
        return Long.class.isAssignableFrom(clazz);
    }

    protected static boolean verifyTypeIsInteger(final Class clazz) {
        return Integer.class.isAssignableFrom(clazz);
    }

    protected static Object applyDataTemplateType(Object cacheObject, final Class type) {
        if (verifyTypeIsLong(type)) {
            cacheObject = Long.valueOf((String) cacheObject);
        } else if (verifyTypeIsInteger(type)) {
            cacheObject = Integer.valueOf((String) cacheObject);
        }

        return cacheObject;
    }

    public static String getBaseKey(final String keyTemplate, final Integer keyIndex, final Object retVal,
            final Object[] args, final String methodString, final VelocityContextFactory factory,
            final CacheKeyMethodStore methodStore) throws Exception {
        if (StringUtils.isBlank(keyTemplate)) {
            final Object keyObject = getIndexObject(keyIndex, retVal, args, methodString);
            return generateObjectId(getKeyMethod(keyObject, methodStore), keyObject);
        }

        final VelocityContext context = factory.getNewExtendedContext();
        context.put("args", args);
        context.put("retVal", retVal);

        return (String) applyTemplate(context, keyTemplate);
    }

    public static List<String> getBaseKeys(final List<Object> keyObjects, final String template,
            final Object retVal, final Object[] args, final VelocityContextFactory factory,
            final CacheKeyMethodStore methodStore) throws Exception {
        final List<String> results = new ArrayList<String>();
        for (int ix = 0; ix < keyObjects.size(); ix++) {
            final Object object = keyObjects.get(ix);
            final String base;
            if (StringUtils.isBlank(template)) {
                final Method method = getKeyMethod(object, methodStore);
                base = generateObjectId(method, object);
            } else {
                final VelocityContext context = factory.getNewExtendedContext();
                context.put("args", args);
                context.put("index", ix);
                context.put("indexObject", object);
                context.put("retVal", retVal);

                base = (String) applyTemplate(context, template);
            }
            results.add(base);
        }

        return results;
    }

    public static Object getMergedData(final Object dataObject, final String template, final Object retVal,
            final Object[] args, final VelocityContextFactory factory) {
        if (StringUtils.isBlank(template)) {
            return dataObject;
        }

        final VelocityContext context = factory.getNewExtendedContext();
        context.put("args", args);
        context.put("retVal", retVal);

        return applyTemplate(context, template);
    }

    public static List<Object> getMergedDataList(final List<Object> dataList, final String template,
            final Object retVal, final Object[] args, final VelocityContextFactory factory) {
        if (StringUtils.isBlank(template)) {
            return dataList;
        }

        final List<Object> results = new ArrayList<Object>();
        for (int ix = 0; ix < dataList.size(); ix++) {
            final Object object = dataList.get(ix);
            final VelocityContext context = factory.getNewExtendedContext();
            context.put("args", args);
            context.put("index", ix);
            context.put("indexObject", object);
            context.put("retVal", retVal);

            results.add(applyTemplate(context, template));
        }

        return results;
    }

    protected static Object applyTemplate(final VelocityContext context, final String template) {
        final StringWriter writer = new StringWriter(1024);
        Velocity.evaluate(context, writer, CacheBase.class.getSimpleName(), template);
        Object value = writer.toString();
        if (template.equals(value)) {
            throw new InvalidParameterException("Calculated key is equal to the velocityTemplate.");
        }

        return value;
    }

    protected <L extends CacheListener> List<L> getPertinentListeners(final Class<L> clazz, final String namespace)
            throws Exception {
        if (clazz == null) {
            throw new InvalidParameterException("Clazz type must be defined.");
        }
        if (namespace == null || namespace.length() == 0) {
            throw new InvalidParameterException("Namespace must be defined.");
        }

        final List<L> results = new ArrayList<L>();
        final List<L> baseList = state.getListeners(clazz);
        if (baseList != null && !baseList.isEmpty()) {
            for (final L base : baseList) {
                if (base.getNamespacesOfInterest() == null || base.getNamespacesOfInterest().isEmpty()
                        || base.getNamespacesOfInterest().contains(namespace)) {
                    results.add(base);
                }
            }
        }

        return results;
    }

    protected MemcachedClientIF getMemcachedClient() {
        return state.getMemcachedClient();
    }

    /*default*/ static final int JITTER_BOUND = 60 * 60 * 24 * 30;

    public static int calculateJitteredExpiration(final int expiration, final int jitter) {
        if (jitter <= 0 || jitter > 99) {
            return expiration;
        }
        if (expiration >= JITTER_BOUND) {
            return expiration;
        }

        final double seed = RandomUtils.nextDouble() * expiration * jitter;
        final int difference = (int) Math.floor(seed / 100);
        return (expiration - difference);
    }

}