diff --git a/build.gradle b/build.gradle index 7002f17e8eab..c515f12abdc7 100644 --- a/build.gradle +++ b/build.gradle @@ -93,6 +93,7 @@ configure(allprojects) { project -> "http://aopalliance.sourceforge.net/doc/", "http://www.eclipse.org/aspectj/doc/released/aspectj5rt-api/", "http://ehcache.org/apidocs/", + "http://docs.jboss.org/infinispan/5.2/apidocs/", "http://quartz-scheduler.org/api/2.1.7/", "http://jackson.codehaus.org/1.9.12/javadoc/", "http://fasterxml.github.com/jackson-core/javadoc/2.2.0/", @@ -395,6 +396,10 @@ project("spring-jdbc") { project("spring-context-support") { description = "Spring Context Support" + repositories { + maven { url "https://repository.jboss.org/nexus/content/repositories/releases/" } // infinispan + } + dependencies { compile(project(":spring-core")) compile(project(":spring-beans")) @@ -404,6 +409,7 @@ project("spring-context-support") { optional("javax.mail:mail:1.4.7") optional("javax.cache:cache-api:0.6") optional("net.sf.ehcache:ehcache-core:2.6.5") + optional("org.infinispan:infinispan-core:5.2.6.Final") optional("org.quartz-scheduler:quartz:1.8.6") { exclude group: "org.slf4j", module: "slf4j-log4j12" } diff --git a/spring-context-support/src/main/java/org/springframework/cache/infinispan/InfinispanCache.java b/spring-context-support/src/main/java/org/springframework/cache/infinispan/InfinispanCache.java new file mode 100644 index 000000000000..cf38841b85e3 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/infinispan/InfinispanCache.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.springframework.cache.infinispan; + +import org.infinispan.lifecycle.ComponentStatus; +import org.springframework.cache.Cache; +import org.springframework.cache.support.SimpleValueWrapper; +import org.springframework.util.Assert; + +/** + * {@link org.springframework.cache.Cache} implementation on top of an + * {@link org.infinispan.Cache} instance. + * + * @author Philippe Marschall + * @since 4.0 + */ +public class InfinispanCache implements Cache { + + private static final Object NULL_HOLDER = NullHolder.INSTANE; + + @SuppressWarnings("rawtypes") + private final org.infinispan.Cache cache; + + private final boolean allowNullValues; + + + /** + * Create an {@link org.springframework.cache.infinispan.InfinispanCache} instance. + * @param infinispanCache backing Infinispan Cache instance + */ + public InfinispanCache(org.infinispan.Cache infinispanCache) { + this(infinispanCache, true); + } + + /** + * Create an {@link org.springframework.cache.infinispan.InfinispanCache} instance. + * @param infinispanCache backing Infinispan Cache instance + * @param allowNullValues whether to accept and convert null values for this cache + */ + public InfinispanCache(org.infinispan.Cache infinispanCache, boolean allowNullValues) { + Assert.notNull(infinispanCache, "Cache must not be null"); + ComponentStatus status = infinispanCache.getStatus(); + Assert.isTrue(status == ComponentStatus.RUNNING, + "A 'running' cache is required - current cache is " + status); + this.cache = infinispanCache; + this.allowNullValues = allowNullValues; + } + + @Override + public String getName() { + return this.cache.getName(); + } + + @Override + public org.infinispan.Cache getNativeCache() { + return this.cache; + } + + @Override + public ValueWrapper get(Object key) { + Object value = this.cache.get(key); + return (value != null ? new SimpleValueWrapper(fromStoreValue(value)) : null); + } + + @Override + @SuppressWarnings("unchecked") + public void put(Object key, Object value) { + this.cache.put(key, toStoreValue(value)); + } + + @Override + public void evict(Object key) { + this.cache.remove(key); + } + + @Override + public void clear() { + this.cache.clear(); + } + + + + /** + * Convert the given value from the internal store to a user value + * returned from the get method (adapting {@code null}). + * @param storeValue the store value + * @return the value to return to the user + */ + protected Object fromStoreValue(Object storeValue) { + if (this.allowNullValues && storeValue == NULL_HOLDER) { + return null; + } + return storeValue; + } + + /** + * Convert the given user value, as passed into the put method, + * to a value in the internal store (adapting {@code null}). + * @param userValue the given user value + * @return the value to store + */ + protected Object toStoreValue(Object userValue) { + if (this.allowNullValues && userValue == null) { + return NULL_HOLDER; + } + return userValue; + } + + + @SuppressWarnings("serial") + static enum NullHolder { + INSTANE; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/infinispan/InfinispanCacheManager.java b/spring-context-support/src/main/java/org/springframework/cache/infinispan/InfinispanCacheManager.java new file mode 100644 index 000000000000..2931b3fa7311 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/infinispan/InfinispanCacheManager.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.springframework.cache.infinispan; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.infinispan.lifecycle.ComponentStatus; +import org.infinispan.manager.EmbeddedCacheManager; +import org.springframework.cache.Cache; +import org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager; +import org.springframework.util.Assert; + +/** + * {@link org.springframework.cache.CacheManager} implementation + * backed by a Infinispan {@link EmbeddedCacheManager}. + * + * @author Philippe Marschall + * @since 4.0 + */ +public class InfinispanCacheManager extends AbstractTransactionSupportingCacheManager { + + private EmbeddedCacheManager cacheManager; + + private boolean allowNullValues = true; + + + /** + * Create a new InfinispanCacheManager, setting the target Infinispan CacheManager + * through the {@link #setCacheManager} bean property. + */ + public InfinispanCacheManager() { + } + + /** + * Create a new InfinispanCacheManager for the given backing Infinispan. + * @param cacheManager the backing Infinispan {@link EmbeddedCacheManager} + */ + public InfinispanCacheManager(EmbeddedCacheManager cacheManager) { + this.cacheManager = cacheManager; + } + + + /** + * Set the backing Infinispan {@link EmbeddedCacheManager}. + */ + public void setCacheManager(EmbeddedCacheManager cacheManager) { + this.cacheManager = cacheManager; + } + + /** + * Return the backing Infinispan {@link EmbeddedCacheManager}. + */ + public EmbeddedCacheManager getCacheManager() { + return this.cacheManager; + } + + /** + * Specify whether to accept and convert null values for all caches + * in this cache manager. + *

Default is "true", despite Infinispan itself not supporting null values. + * An internal holder object will be used to store user-level null values. + */ + public void setAllowNullValues(boolean allowNullValues) { + this.allowNullValues = allowNullValues; + } + + /** + * Return whether this cache manager accepts and converts null values + * for all of its caches. + */ + public boolean isAllowNullValues() { + return this.allowNullValues; + } + + + @Override + protected Collection loadCaches() { + Assert.notNull(this.cacheManager, "A backing CacheManager is required"); + ComponentStatus status = this.cacheManager.getStatus(); + Assert.isTrue(ComponentStatus.RUNNING == status, + "A 'running' Infinispan CacheManager is required - current cache is " + status); + + Set cacheNames = this.cacheManager.getCacheNames(); + Collection caches = new LinkedHashSet(cacheNames.size()); + + for (String cacheName : cacheNames) { + org.infinispan.Cache infinispanCache = this.cacheManager.getCache(cacheName, true); + caches.add(new InfinispanCache(infinispanCache, this.allowNullValues)); + } + return caches; + } + + @Override + public Cache getCache(String name) { + Cache cache = super.getCache(name); + if (cache == null) { + // check the Infinispan cache again + // (in case the cache was added at runtime) + org.infinispan.Cache infinispanCache = this.cacheManager.getCache(name, false); + if (infinispanCache != null) { + cache = new InfinispanCache(infinispanCache, this.allowNullValues); + addCache(cache); + } + } + return cache; + } +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/infinispan/InfinispanCacheManagerFactoryBean.java b/spring-context-support/src/main/java/org/springframework/cache/infinispan/InfinispanCacheManagerFactoryBean.java new file mode 100644 index 000000000000..e4561745cc42 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/infinispan/InfinispanCacheManagerFactoryBean.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.springframework.cache.infinispan; + +import java.io.IOException; +import java.io.InputStream; + +import org.infinispan.manager.DefaultCacheManager; +import org.infinispan.manager.EmbeddedCacheManager; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.Resource; + +/** + * {@link FactoryBean} for a Infinispan {@link EmbeddedCacheManager}. + * + *

This class is intended to be used in conjunction with Spring XML configuration and + * Infinispan XML configuration. While it does work with Spring Java configuration in + * such cases you're likely better off instantiating the cache manager yourself and using + * + * Infinispan fluent configuration. + * + * @author Philippe Marschall + * @since 4.0 + */ +public class InfinispanCacheManagerFactoryBean + implements FactoryBean, InitializingBean, DisposableBean { + + private Resource configLocation; + + private EmbeddedCacheManager cacheManager; + + + /** + * Set the location of the Infinispan config file. A typical value is "/WEB-INF/infinispan.xml". + * @see org.infinispan.manager.DefaultCacheManager#DefaultCacheManager(java.io.InputStream) + */ + public void setConfigLocation(Resource configLocation) { + this.configLocation = configLocation; + } + + @Override + public void afterPropertiesSet() throws IOException { + if (this.configLocation != null) { + InputStream configurationStream = this.configLocation.getInputStream(); + try { + this.cacheManager = new DefaultCacheManager(configurationStream, true); + } finally { + configurationStream.close(); + } + } else { + this.cacheManager = new DefaultCacheManager(true); + } + } + + + @Override + public EmbeddedCacheManager getObject() { + return this.cacheManager; + } + + @Override + public Class getObjectType() { + return this.cacheManager != null ? this.cacheManager.getClass() : EmbeddedCacheManager.class; + } + + @Override + public boolean isSingleton() { + return true; + } + + + @Override + public void destroy() { + this.cacheManager.stop(); + } +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/infinispan/package-info.java b/spring-context-support/src/main/java/org/springframework/cache/infinispan/package-info.java new file mode 100644 index 000000000000..7ec2f08ae48f --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/infinispan/package-info.java @@ -0,0 +1,7 @@ +/** + * Support classes for the open source cache + * Infinispan, + * allowing to set up an Infinispan CacheManager and Caches + * as beans in a Spring context. + */ +package org.springframework.cache.infinispan; diff --git a/spring-context-support/src/test/java/org/springframework/cache/infinispan/InfinispanCacheTests.java b/spring-context-support/src/test/java/org/springframework/cache/infinispan/InfinispanCacheTests.java new file mode 100644 index 000000000000..ac6fd1be4ddc --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/infinispan/InfinispanCacheTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.springframework.cache.infinispan; + +import org.infinispan.manager.DefaultCacheManager; +import org.infinispan.manager.EmbeddedCacheManager; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.cache.Cache; +import org.springframework.tests.Assume; +import org.springframework.tests.TestGroup; + +import static java.util.concurrent.TimeUnit.*; +import static org.junit.Assert.*; + +/** + * @author Philippe Marschall + */ +public class InfinispanCacheTests { + + protected final static String CACHE_NAME = "testCache"; + + protected org.infinispan.Cache nativeCache; + + protected EmbeddedCacheManager cacheManager; + + protected Cache cache; + + + @Before + public void setUp() throws Exception { + cacheManager = new DefaultCacheManager("org/springframework/cache/infinispan/testInfinispan.xml", true); + nativeCache = cacheManager.getCache(CACHE_NAME, true); + cache = new InfinispanCache(nativeCache); + cache.clear(); + } + + @After + public void tearDown() { + nativeCache.stop(); + cacheManager.stop(); + } + + + @Test + public void testCacheName() { + assertEquals(CACHE_NAME, cache.getName()); + } + + @Test + public void testNativeCache() { + assertSame(nativeCache, cache.getNativeCache()); + } + + @Test + public void testCachePut() { + Object key = "enescu"; + Object value = "george"; + + assertNull(cache.get(key)); + cache.put(key, value); + assertEquals(value, cache.get(key).get()); + } + + @Test + public void testCacheRemove() { + Object key = "enescu"; + Object value = "george"; + + assertNull(cache.get(key)); + cache.put(key, value); + } + + @Test + public void testCacheClear() { + assertNull(cache.get("enescu")); + cache.put("enescu", "george"); + assertNull(cache.get("vlaicu")); + cache.put("vlaicu", "aurel"); + cache.clear(); + assertNull(cache.get("vlaicu")); + assertNull(cache.get("enescu")); + } + + @Test + public void testExpiredElements() throws Exception { + Assume.group(TestGroup.LONG_RUNNING); + String key = "brancusi"; + String value = "constantin"; + // ttl = 10s + nativeCache.put(key, value, 3, SECONDS); + + assertEquals(value, cache.get(key).get()); + // wait for the entry to expire + Thread.sleep(5 * 1000); + assertNull(cache.get(key)); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/infinispan/InfinispanSupportTests.java b/spring-context-support/src/test/java/org/springframework/cache/infinispan/InfinispanSupportTests.java new file mode 100644 index 000000000000..ced6f7351170 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/infinispan/InfinispanSupportTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.springframework.cache.infinispan; + +import org.infinispan.eviction.EvictionStrategy; +import org.infinispan.manager.EmbeddedCacheManager; +import org.junit.Test; +import org.springframework.core.io.ClassPathResource; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +/** + * @author Philippe Marschall + */ +public class InfinispanSupportTests { + + @Test + public void testLoadingBlankCacheManager() throws Exception { + InfinispanCacheManagerFactoryBean cacheManagerFb = new InfinispanCacheManagerFactoryBean(); + assertEquals(EmbeddedCacheManager.class, cacheManagerFb.getObjectType()); + assertTrue("Singleton property", cacheManagerFb.isSingleton()); + cacheManagerFb.afterPropertiesSet(); + try { + EmbeddedCacheManager cm = cacheManagerFb.getObject(); + assertThat("Loaded CacheManager with no caches", cm.getCacheNames(), empty()); + org.infinispan.Cache myCache1 = cm.getCache("myCache1", false); + assertNull("No myCache1 defined", myCache1); + } + finally { + cacheManagerFb.destroy(); + } + } + + @Test + public void testLoadingCacheManagerFromConfigFile() throws Exception { + InfinispanCacheManagerFactoryBean cacheManagerFb = new InfinispanCacheManagerFactoryBean(); + cacheManagerFb.setConfigLocation(new ClassPathResource("testInfinispan.xml", getClass())); + cacheManagerFb.afterPropertiesSet(); + try { + EmbeddedCacheManager cm = cacheManagerFb.getObject(); + assertThat("Correct number of caches loaded", cm.getCacheNames(), hasSize(1)); + org.infinispan.Cache myCache1 = cm.getCache("myCache1"); + assertNotNull("No myCache1 defined", myCache1); + assertEquals("myCache1 is not LIRS", myCache1.getCacheConfiguration().eviction().strategy(), EvictionStrategy.LIRS); + assertTrue("myCache1.maxElements == 300", myCache1.getCacheConfiguration().eviction().maxEntries() == 300); + } + finally { + cacheManagerFb.destroy(); + } + } + +} diff --git a/spring-context-support/src/test/resources/org/springframework/cache/infinispan/testInfinispan.xml b/spring-context-support/src/test/resources/org/springframework/cache/infinispan/testInfinispan.xml new file mode 100644 index 000000000000..efca56afd9f3 --- /dev/null +++ b/spring-context-support/src/test/resources/org/springframework/cache/infinispan/testInfinispan.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + \ No newline at end of file