/**
* Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright ownership. Apereo
* licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the License at the
* following location:
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apereo.portal.utils.cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jasig.services.persondir.support.IUsernameAttributeProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springmodules.cache.key.CacheKeyGenerator;
public class PersonDirectoryCacheKeyGenerator implements CacheKeyGenerator {
protected final Log logger = LogFactory.getLog(this.getClass());
private final LoadingCache<Method, CachableMethod> resolvedMethodCache =
CacheBuilder.newBuilder()
.build(
new CacheLoader<Method, CachableMethod>() {
@Override
public CachableMethod load(Method key) throws Exception {
return resolveCacheableMethod(key);
}
});
private IUsernameAttributeProvider usernameAttributeProvider;
private boolean ignoreEmptyAttributes = false;
@Autowired
public void setUsernameAttributeProvider(IUsernameAttributeProvider usernameAttributeProvider) {
this.usernameAttributeProvider = usernameAttributeProvider;
}
/**
* If seed attributes with empty values (null, empty string or empty list values) should be
* ignored when generating the cache key. Defaults to false.
*/
public void setIgnoreEmptyAttributes(boolean ignoreEmptyAttributes) {
this.ignoreEmptyAttributes = ignoreEmptyAttributes;
}
@Override
public Serializable generateKey(MethodInvocation methodInvocation) {
//Determine the targeted CachableMethod
final CachableMethod cachableMethod =
this.resolvedMethodCache.getUnchecked(methodInvocation.getMethod());
//Use the resolved cachableMethod to determine the seed Map and then get the hash of the key elements
final Object[] methodArguments = methodInvocation.getArguments();
final CacheKey.CacheKeyBuilder<String, Serializable> cacheKeyBuilder =
CacheKey.builder(cachableMethod.getName());
switch (cachableMethod) {
//Both methods that take a Map argument can just have the first argument returned
case PEOPLE_MAP:
case PEOPLE_MULTIVALUED_MAP:
case MULTIVALUED_USER_ATTRIBUTES__MAP:
case USER_ATTRIBUTES__MAP:
{
final Map<String, Object> queryMap = (Map<String, Object>) methodArguments[0];
//If possible tag the cache key with the username
final String usernameAttribute =
this.usernameAttributeProvider.getUsernameAttribute();
Object usernameValue = queryMap.get(usernameAttribute);
if (usernameValue instanceof String) {
cacheKeyBuilder.addTag(
UsernameTaggedCacheEntryPurger.createCacheEntryTag(
(String) usernameValue));
} else if (usernameValue instanceof List) {
final List usernameValueList = (List) usernameValue;
if (usernameValueList.size() == 1) {
usernameValue = usernameValueList.get(0);
if (usernameValue instanceof String) {
cacheKeyBuilder.addTag(
UsernameTaggedCacheEntryPurger.createCacheEntryTag(
(String) usernameValue));
}
}
}
for (final Map.Entry<String, Object> e : queryMap.entrySet()) {
final String key = e.getKey();
final Object value = e.getValue();
//Skip null/empty attribute values
if (ignoreEmptyAttributes
&& (value == null
|| (value instanceof Collection
&& ((Collection) value).isEmpty())
|| (value instanceof Map && ((Map) value).isEmpty())
|| (value.getClass().isArray()
&& Array.getLength(value) == 0))) {
continue;
}
if (value == null || value instanceof Serializable) {
cacheKeyBuilder.put(key, (Serializable) value);
} else {
cacheKeyBuilder.put(key, value.getClass());
}
}
break;
}
//The multivalued attributes with a string needs to be converted to Map<String, List<Object>>
case MULTIVALUED_USER_ATTRIBUTES__STR:
{
final String uid = (String) methodArguments[0];
if (StringUtils.isEmpty(uid)) {
break;
}
cacheKeyBuilder.add(uid);
cacheKeyBuilder.addTag(UsernameTaggedCacheEntryPurger.createCacheEntryTag(uid));
break;
}
//The single valued attributes with a string needs to be converted to Map<String, Object>
case PERSON_STR:
case USER_ATTRIBUTES__STR:
{
final String uid = (String) methodArguments[0];
if (StringUtils.isEmpty(uid)) {
break;
}
cacheKeyBuilder.add(uid);
cacheKeyBuilder.addTag(UsernameTaggedCacheEntryPurger.createCacheEntryTag(uid));
break;
}
case POSSIBLE_USER_ATTRIBUTE_NAMES:
case AVAILABLE_QUERY_ATTRIBUTES:
{
break;
}
default:
{
throw new IllegalArgumentException(
"Unsupported CachableMethod resolved: '" + cachableMethod + "'");
}
}
if (cacheKeyBuilder.size() == 0) {
if (this.logger.isDebugEnabled()) {
this.logger.debug(
"No cache key generated for MethodInvocation='" + methodInvocation + "'");
}
return null;
}
final CacheKey cacheKey = cacheKeyBuilder.build();
if (this.logger.isDebugEnabled()) {
this.logger.debug(
"Generated cache key '"
+ cacheKey
+ "' for MethodInvocation='"
+ methodInvocation
+ "'");
}
return cacheKey;
}
/**
* Iterates over the {@link CachableMethod} instances to determine which instance the passed
* {@link MethodInvocation} applies to.
*/
protected CachableMethod resolveCacheableMethod(Method targetMethod) {
final Class<?> targetClass = targetMethod.getDeclaringClass();
for (final CachableMethod cachableMethod : CachableMethod.values()) {
Method cacheableMethod = null;
try {
cacheableMethod =
targetClass.getMethod(cachableMethod.getName(), cachableMethod.getArgs());
} catch (SecurityException e) {
this.logger.warn(
"Security exception while attempting to if the target class '"
+ targetClass
+ "' implements the cachable method '"
+ cachableMethod
+ "'",
e);
} catch (NoSuchMethodException e) {
final String message =
"Taret class '"
+ targetClass
+ "' does not implement possible cachable method '"
+ cachableMethod
+ "'. Is the advice applied to the correct bean and methods?";
if (this.logger.isDebugEnabled()) {
this.logger.debug(message, e);
} else {
this.logger.warn(message);
}
}
if (targetMethod.equals(cacheableMethod)) {
return cachableMethod;
}
}
throw new IllegalArgumentException(
"Do not know how to generate a cache for for '"
+ targetMethod
+ "' on class '"
+ targetClass
+ "'. Is the advice applied to the correct bean and methods?");
}
/** Methods on {@link org.jasig.services.persondir.IPersonAttributeDao} that are cachable */
public enum CachableMethod {
@Deprecated
MULTIVALUED_USER_ATTRIBUTES__MAP("getMultivaluedUserAttributes", Map.class),
@Deprecated
MULTIVALUED_USER_ATTRIBUTES__STR("getMultivaluedUserAttributes", String.class),
@Deprecated
USER_ATTRIBUTES__MAP("getUserAttributes", Map.class),
@Deprecated
USER_ATTRIBUTES__STR("getUserAttributes", String.class),
PERSON_STR("getPerson", String.class),
PEOPLE_MAP("getPeople", Map.class),
PEOPLE_MULTIVALUED_MAP("getPeopleWithMultivaluedAttributes", Map.class),
POSSIBLE_USER_ATTRIBUTE_NAMES("getPossibleUserAttributeNames"),
AVAILABLE_QUERY_ATTRIBUTES("getAvailableQueryAttributes");
private final String name;
private final Class<?>[] args;
private CachableMethod(String name, Class<?>... args) {
this.name = name;
this.args = args;
}
public String getName() {
return this.name;
}
public Class<?>[] getArgs() {
return this.args;
}
@Override
public String toString() {
return this.name + "(" + Arrays.asList(this.args) + ")";
}
}
}