/* * Copyright (c) 2012-2017 Jakub Białek * * 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 com.google.code.ssm.spring; import java.util.concurrent.Callable; import java.util.concurrent.TimeoutException; import lombok.Getter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.Cache; import org.springframework.cache.support.SimpleValueWrapper; import com.google.code.ssm.aop.support.PertinentNegativeNull; import com.google.code.ssm.providers.CacheException; /** * Wrapper around {@link com.google.code.ssm.Cache} that allow to set default expiration time used in all * {@link SSMCache#put(Object, Object)} ( * {@link com.google.code.ssm.Cache#set(String, int, Object, com.google.code.ssm.api.format.SerializationType)}) * requests. * * @author Jakub Białek * @since 3.0.0 * */ public class SSMCache implements Cache { private static final Logger LOGGER = LoggerFactory.getLogger(SSMCache.class); @Getter private final com.google.code.ssm.Cache cache; @Getter private final int expiration; @Getter private final boolean allowClear; /** * * If true then all aliases of the underlying cache will be used to register the cache in Spring, otherwise the * cache will be available only by name. * * @since 3.3.0 */ @Getter private final boolean registerAliases; public SSMCache(final com.google.code.ssm.Cache cache, final int expiration, final boolean allowClear, final boolean registerAliases) { this.cache = cache; this.expiration = expiration; this.allowClear = allowClear; this.registerAliases = registerAliases; } public SSMCache(final com.google.code.ssm.Cache cache, final int expiration, final boolean allowClear) { this(cache, expiration, allowClear, false); } public SSMCache(final com.google.code.ssm.Cache cache, final int expiration) { this(cache, expiration, false); } public SSMCache(final SSMCache ssmCache, final int expiration) { this(ssmCache.cache, expiration, ssmCache.allowClear); } @Override public String getName() { return cache.getName(); } @Override public Object getNativeCache() { return cache; } @Override public ValueWrapper get(final Object key) { if (!cache.isEnabled()) { LOGGER.warn("Cache {} is disabled. Cannot get {} from cache", cache.getName(), key); return null; } Object value = getValue(key); if (value == null) { LOGGER.info("Cache miss. Get by key {} from cache {}", key, cache.getName()); return null; } LOGGER.info("Cache hit. Get by key {} from cache {} value '{}'", new Object[] { key, cache.getName(), value }); return value instanceof PertinentNegativeNull ? new SimpleValueWrapper(null) : new SimpleValueWrapper(value); } /** * Required by Spring 4.0 * * @since 3.4.0 */ @SuppressWarnings("unchecked") public <T> T get(final Object key, final Class<T> type) { if (!cache.isEnabled()) { LOGGER.warn("Cache {} is disabled. Cannot get {} from cache", cache.getName(), key); return null; } Object value = getValue(key); if (value == null) { LOGGER.info("Cache miss. Get by key {} and type {} from cache {}", new Object[] { key, type, cache.getName() }); return null; } if (value instanceof PertinentNegativeNull) { return null; } if (type != null && !type.isInstance(value)) { // in such case default Spring back end for EhCache throws IllegalStateException which interrupts // intercepted method invocation String msg = "Cached value is not of required type [" + type.getName() + "]: " + value; LOGGER.error(msg, new IllegalStateException(msg)); return null; } LOGGER.info("Cache hit. Get by key {} and type {} from cache {} value '{}'", new Object[] { key, type, cache.getName(), value }); return (T) value; } /** * Required by Spring 4.3 * * @since 3.6.1 */ @SuppressWarnings("unchecked") public <T> T get(final Object key, final Callable<T> valueLoader) { if (!cache.isEnabled()) { LOGGER.warn("Cache {} is disabled. Cannot get {} from cache", cache.getName(), key); return loadValue(key, valueLoader); } final ValueWrapper valueWrapper = get(key); if (valueWrapper != null) { return (T) valueWrapper.get(); } synchronized (key.toString().intern()) { final T value = loadValue(key, valueLoader); put(key, value); return value; } } @Override public void put(final Object key, final Object value) { if (!cache.isEnabled()) { LOGGER.warn("Cache {} is disabled. Cannot put value under key {}", cache.getName(), key); return; } if (key != null) { final String cacheKey = getKey(key); try { LOGGER.info("Put '{}' under key {} to cache {}", new Object[] { value, key, cache.getName() }); final Object store = toStoreValue(value); cache.set(cacheKey, expiration, store, null); } catch (TimeoutException e) { LOGGER.warn("An error has ocurred for cache {} and key {}", new Object []{ getName(), cacheKey, e}); } catch (CacheException e) { LOGGER.warn("An error has ocurred for cache {} and key {}", new Object []{ getName(), cacheKey, e}); } catch (RuntimeException e) { // do not propagate any exceptions LOGGER.warn("An error has ocurred for cache {} and key {}", new Object []{ getName(), cacheKey, e}); } } else { LOGGER.info("Cannot put to cache {} because key is null", cache.getName()); } } /** * Required by Spring 4.1 * * @since 3.6.0 */ public ValueWrapper putIfAbsent(final Object key, final Object value) { if (!cache.isEnabled()) { LOGGER.warn("Cache {} is disabled. Cannot put value under key {}", cache.getName(), key); return null; } if (key != null) { final String cacheKey = getKey(key); try { LOGGER.info("Put '{}' under key {} to cache {}", new Object[] { value, key, cache.getName() }); final Object store = toStoreValue(value); final boolean added = cache.add(cacheKey, expiration, store, null); return added ? null : get(key); } catch (TimeoutException e) { LOGGER.warn("An error has ocurred for cache {} and key {}", new Object []{ getName(), cacheKey, e}); } catch (CacheException e) { LOGGER.warn("An error has ocurred for cache {} and key {}", new Object []{ getName(), cacheKey, e}); } catch (RuntimeException e) { // do not propagate any exceptions LOGGER.warn("An error has ocurred for cache {} and key {}", new Object []{ getName(), cacheKey, e}); } } else { LOGGER.info("Cannot put to cache {} because key is null", cache.getName()); } return null; } @Override public void evict(final Object key) { if (!cache.isEnabled()) { LOGGER.warn("Cache {} is disabled. Cannot evict key {}", cache.getName(), key); return; } if (key != null) { final String cacheKey = getKey(key); try { LOGGER.info("Evict {} from cache {}", key, cache.getName()); cache.delete(cacheKey); } catch (TimeoutException e) { LOGGER.warn("An error has ocurred for cache {} and key {}", new Object []{ getName(), cacheKey, e}); } catch (CacheException e) { LOGGER.warn("An error has ocurred for cache {} and key {}", new Object []{ getName(), cacheKey, e}); } catch (RuntimeException e) { // do not propagate any exceptions LOGGER.warn("An error has ocurred for cache {} and key {}", new Object []{ getName(), cacheKey, e}); } } else { LOGGER.info("Cannot evict from cache {} because key is null", cache.getName()); } } @Override public void clear() { if (!cache.isEnabled()) { LOGGER.warn("Cache {} is disabled. Cannot clear cache.", cache.getName()); return; } if (!allowClear) { LOGGER.error("Clearing cache '{}' is not allowed. To enable it set allowClear to true. " + "Make sure that caches don't overlap (one memcached instance isn't used by more than one cache) " + "otherwise clearing one cache will affect another.", getName()); throw new IllegalStateException("Cannot clear cache " + getName()); } try { LOGGER.info("Clear {}", cache.getName()); cache.flush(); } catch (TimeoutException e) { LOGGER.warn("An error has ocurred for cache {}", getName(), e); } catch (CacheException e) { LOGGER.warn("An error has ocurred for cache {}", getName(), e); } catch (RuntimeException e) { // do not propagate any exceptions LOGGER.warn("An error has ocurred for cache {}", getName(), e); } } private Object getValue(Object key) { final String cacheKey = getKey(key); Object value = null; try { value = cache.get(cacheKey, null); } catch (TimeoutException e) { LOGGER.warn("An error has ocurred for cache {} and key {}", new Object []{ getName(), cacheKey, e}); } catch (CacheException e) { LOGGER.warn("An error has ocurred for cache {} and key {}", new Object []{ getName(), cacheKey, e}); } catch (RuntimeException e) { // do not propagate any exceptions LOGGER.warn("An error has ocurred for cache {} and key {}", new Object []{ getName(), cacheKey, e}); } return value; } private String getKey(final Object key) { return key.toString(); } private <T> T loadValue(final Object key, final Callable<T> valueLoader) { try { return valueLoader.call(); } catch (Exception ex) { throw new ValueRetrievalException(key, valueLoader, ex); } } private Object toStoreValue(final Object value) { return value == null ? PertinentNegativeNull.NULL : value; } /** * A custom exception class because org.springframework.cache.Cache.ValueRetrievalException cannot be used * as it is declared in Spring 4.3 but SSM shouldn't required newest Spring version. * * @author Jakub Białek * @since 3.6.1 * */ public static class ValueRetrievalException extends RuntimeException { private static final long serialVersionUID = 7777933028628648800L; public ValueRetrievalException(Object key, Callable<?> loader, Throwable ex) { super(String.format("Value for key '%s' could not be loaded using '%s'", key, loader), ex); } } }