This is an automated email from the ASF dual-hosted git repository. bdemers pushed a commit to branch jcache-main in repository https://gitbox.apache.org/repos/asf/shiro.git
commit b919b4977a4ce4b825f5c457624ccb3ed5c1fb9b Author: Brian Demers <[email protected]> AuthorDate: Fri Jan 14 18:25:44 2022 -0500 Adds cache module for JCache This will allow for any jcache implementation to work with Shiro, as well as new erversions of EhCache & Hazelcast. Fixes: SHIRO-816 Fixes: SHIRO-813 --- support/jcache/pom.xml | 88 +++++++ .../apache/shiro/cache/jcache/JCacheManager.java | 255 +++++++++++++++++++++ support/jcache/src/main/resources/META-INF/NOTICE | 15 ++ .../shiro/cache/jcache/JCacheManagerTest.groovy | 146 ++++++++++++ support/pom.xml | 1 + 5 files changed, 505 insertions(+) diff --git a/support/jcache/pom.xml b/support/jcache/pom.xml new file mode 100644 index 00000000..6e45484c --- /dev/null +++ b/support/jcache/pom.xml @@ -0,0 +1,88 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one + ~ or more contributor license agreements. See the NOTICE file + ~ distributed with this work for additional information + ~ regarding copyright ownership. The ASF 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 + ~ + ~ 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. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd"> + + <parent> + <groupId>org.apache.shiro</groupId> + <artifactId>shiro-support</artifactId> + <version>1.8.1-SNAPSHOT</version> + <relativePath>../pom.xml</relativePath> + </parent> + + <modelVersion>4.0.0</modelVersion> + <artifactId>shiro-jcache</artifactId> + <name>Apache Shiro :: Support :: JCache</name> + <packaging>bundle</packaging> + + <properties> + <jcache.osgi.importRange>[1.1,2)</jcache.osgi.importRange> + </properties> + + <dependencies> + <dependency> + <groupId>org.apache.shiro</groupId> + <artifactId>shiro-cache</artifactId> + </dependency> + <dependency> + <groupId>javax.cache</groupId> + <artifactId>cache-api</artifactId> + <version>1.1.1</version> + </dependency> + <!-- Test dependencies --> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>jcl-over-slf4j</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.cache2k</groupId> + <artifactId>cache2k-jcache</artifactId> + <version>2.4.1.Final</version> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <instructions> + <Bundle-SymbolicName>org.apache.shiro.jcache</Bundle-SymbolicName> + <Export-Package>org.apache.shiro.jcache*;version=${project.version}</Export-Package> + <Import-Package> + org.apache.shiro*;version="${shiro.osgi.importRange}", + com.hazelcast*;version="${jcache.osgi.importRange}", + * + </Import-Package> + </instructions> + </configuration> + </plugin> + </plugins> + </build> + +</project> diff --git a/support/jcache/src/main/java/org/apache/shiro/cache/jcache/JCacheManager.java b/support/jcache/src/main/java/org/apache/shiro/cache/jcache/JCacheManager.java new file mode 100644 index 00000000..8760652a --- /dev/null +++ b/support/jcache/src/main/java/org/apache/shiro/cache/jcache/JCacheManager.java @@ -0,0 +1,255 @@ +package org.apache.shiro.cache.jcache; + +import org.apache.shiro.cache.Cache; +import org.apache.shiro.cache.CacheException; +import org.apache.shiro.cache.CacheManager; +import org.apache.shiro.util.Destroyable; +import org.apache.shiro.util.Initializable; +import org.apache.shiro.util.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.cache.Caching; +import javax.cache.configuration.MutableConfiguration; +import javax.cache.spi.CachingProvider; +import java.net.URL; +import java.util.Collection; +import java.util.Iterator; +import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * Shiro {@code CacheManager} implementation utilizing JCache for all cache functionality. + * <p/> + * This class can {@link #setCacheManager(javax.cache.CacheManager) accept} a manually configured + * {@link javax.cache.CacheManager javax.cache.CacheManager} instance, + * a {@code cacheConfig} URI can be specified, or a call to {@link CachingProvider#getCacheManager()} will be used. + * <p/> + * This implementation requires a JCache implementation available on the classpath. + * <p/> + * @since 1.9 + */ +public class JCacheManager implements CacheManager, Initializable, Destroyable { + + /** + * This class's private log instance. + */ + private static final Logger log = LoggerFactory.getLogger(JCacheManager.class); + + private javax.cache.CacheManager jCacheManager; + + private String cacheConfig; + + /** + * Indicates if the CacheManager instance was implicitly/automatically created by this instance, indicating that + * it should be automatically cleaned up as well on shutdown. + */ + private boolean cacheManagerImplicitlyCreated = false; + + @Override + public <K, V> Cache<K, V> getCache(String name) throws CacheException { + + javax.cache.Cache<K, V> cache = ensureCacheManager().getCache(name); + + if (cache == null) { + synchronized (this) { + cache = ensureCacheManager().getCache(name); + if (cache == null) { + log.debug("Cache with name '{}' does not yet exist. Creating now.", name); + cache = ensureCacheManager().createCache(name, new MutableConfiguration<>()); + log.debug("Added JCache named [{}]", name); + } else { + log.debug("Using existing JCache named [{}]", cache.getName()); + } + } + } + + return new JCache<>(cache); + } + + /** + * Initializes this instance. + * <p/> + * If a CacheManager has been + * explicitly set (e.g. via Dependency Injection or programmatically) prior to calling this + * method, this method does nothing. + * <p/> + * Because Shiro cannot use the failsafe defaults (fail-safe expunges cached objects after 2 minutes, + * something not desirable for Shiro sessions), this class manages an internal default configuration for + * this case. + * + * @throws org.apache.shiro.cache.CacheException + * if there are any CacheExceptions thrown by JCache. + */ + public final void init() throws CacheException { + ensureCacheManager(); + } + + private javax.cache.CacheManager ensureCacheManager() { + try { + if (this.jCacheManager == null) { + log.debug("cacheManager property not set. Constructing CacheManager instance... "); + CachingProvider cachingProvider = Caching.getCachingProvider(); + + if (StringUtils.hasText(cacheConfig)) { + + URL config = getClass().getResource(cacheConfig); + if (config == null) { + throw new IllegalArgumentException("Could not load JCache configuration resource: " + cacheConfig); + } + + this.jCacheManager = cachingProvider.getCacheManager(config.toURI(), getClass().getClassLoader()); + } else { + this.jCacheManager = cachingProvider.getCacheManager(); + } + + cacheManagerImplicitlyCreated = true; + log.debug("implicit cacheManager created successfully."); + } + return this.jCacheManager; + } catch (Exception e) { + throw new CacheException(e); + } + } + + /** + * Shuts-down the wrapped JCache CacheManager <b>only if implicitly created</b>. + * <p/> + * If another component injected + * a non-null CacheManager into this instance before calling {@link #init() init}, this instance expects that same + * component to also destroy the CacheManager instance, and it will not attempt to do so. + */ + public void destroy() { + if (cacheManagerImplicitlyCreated) { + try { + jCacheManager.close(); + } catch (Throwable t) { + log.warn("Unable to cleanly shutdown implicitly created CacheManager instance. Ignoring (shutting down)...", t); + } finally { + this.jCacheManager = null; + this.cacheManagerImplicitlyCreated = false; + } + } + } + + public String getCacheConfig() { + return cacheConfig; + } + + public void setCacheConfig(String jCacheConfig) { + this.cacheConfig = jCacheConfig; + } + + public javax.cache.CacheManager getCacheManager() { + return jCacheManager; + } + + public void setCacheManager(javax.cache.CacheManager jCacheManager) { + this.jCacheManager = jCacheManager; + } + + static class JCache<K,V> implements Cache<K,V> { + + private final javax.cache.Cache<K,V> cache; + + JCache(javax.cache.Cache<K,V> cache) { + this.cache = cache; + } + /** + * Gets a value of an element which matches the given key. + * + * @param key the key of the element to return. + * @return The value placed into the cache with an earlier put, or null if not found or expired + */ + @Override + public V get(K key) throws CacheException { + try { + log.trace("Getting object from cache [{}] for key [{}]", cache.getName(), key); + if (key == null) { + return null; + } else { + V element = cache.get(key); + if (element == null) { + log.trace("Element for [{}] is null.", key); + return null; + } else { + return element; + } + } + } catch (Throwable t) { + throw new CacheException(t); + } + } + + /** + * Puts an object into the cache. + * + * @param key the key. + * @param value the value. + */ + public V put(K key, V value) throws CacheException { + log.trace("Putting object in cache [{}] for key [{}]", cache.getName(), key); + try { + V previous = get(key); + cache.put(key, value); + return previous; + } catch (Throwable t) { + throw new CacheException(t); + } + } + + /** + * Removes the element which matches the key. + * + * <p>If no element matches, nothing is removed and no Exception is thrown.</p> + * + * @param key the key of the element to remove + */ + public V remove(K key) throws CacheException { + log.trace("Removing object from cache [{}] for key [{}]", cache.getName(), key); + try { + return cache.getAndRemove(key); + } catch (Throwable t) { + throw new CacheException(t); + } + } + + /** + * Removes all elements in the cache, but leaves the cache in a useable state. + */ + public void clear() throws CacheException { + log.trace("Clearing all objects from cache [{}]", cache.getName()); + try { + cache.removeAll(); + } catch (Throwable t) { + throw new CacheException(t); + } + } + + public int size() { + return (int) toStream(cache.iterator()).count(); + } + + @Override + public Set<K> keys() { + return toStream(cache.iterator()) + .map(javax.cache.Cache.Entry::getKey) + .collect(Collectors.toSet()); + } + + @Override + public Collection<V> values() { + return toStream(cache.iterator()) + .map(javax.cache.Cache.Entry::getValue) + .collect(Collectors.toSet()); + } + + private Stream<javax.cache.Cache.Entry<K, V>> toStream(Iterator<javax.cache.Cache.Entry<K, V>> iterator) { + return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false); + } + } +} diff --git a/support/jcache/src/main/resources/META-INF/NOTICE b/support/jcache/src/main/resources/META-INF/NOTICE new file mode 100644 index 00000000..9d26a95f --- /dev/null +++ b/support/jcache/src/main/resources/META-INF/NOTICE @@ -0,0 +1,15 @@ +Apache Shiro +Copyright 2008-2020 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +The implementation for org.apache.shiro.util.SoftHashMap is based +on initial ideas from Dr. Heinz Kabutz's publicly posted version +available at http://www.javaspecialists.eu/archive/Issue015.html, +with continued modifications. + +Certain parts (StringUtils, IpAddressMatcher, etc.) of the source +code for this product was copied for simplicity and to reduce +dependencies from the source code developed by the Spring Framework +Project (http://www.springframework.org). diff --git a/support/jcache/src/test/groovy/org/apache/shiro/cache/jcache/JCacheManagerTest.groovy b/support/jcache/src/test/groovy/org/apache/shiro/cache/jcache/JCacheManagerTest.groovy new file mode 100644 index 00000000..a4e44a3f --- /dev/null +++ b/support/jcache/src/test/groovy/org/apache/shiro/cache/jcache/JCacheManagerTest.groovy @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 + * + * 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.apache.shiro.cache.jcache + +import org.apache.shiro.cache.Cache +import org.apache.shiro.cache.CacheException +import org.junit.Assert +import org.junit.Test + +import static org.hamcrest.MatcherAssert.assertThat + +import static org.hamcrest.Matchers.* + +/** + * Unit tests for {@link JCacheManager}. + * + * @since 1.9 + */ +class JCacheManagerTest { + + @Test + void invalidConfigFile() { + JCacheManager cacheManager = new JCacheManager() + cacheManager.setCacheConfig("./invalid/location") + def exception = expectThrows CacheException, { cacheManager.init() } + assertThat exception.message, containsString("Could not load JCache configuration resource: ./invalid/location") + } + + @Test + void happyPath() { + JCacheManager cacheManager = new JCacheManager() + cacheManager.init() + Cache cache = cacheManager.getCache("foobar") + assertThat cache, notNullValue() + cache.put("Foo", "Bar") + assertThat cache.get("Foo"), is("Bar") + } + + @Test + void sizeTest() { + JCacheManager cacheManager = new JCacheManager() + cacheManager.init() + Cache cache = cacheManager.getCache("size-test") + assertThat cache, notNullValue() + cache.put("one", "value") + assertThat cache.size(), is(1) + } + + @Test + void clear() { + JCacheManager cacheManager = new JCacheManager() + cacheManager.init() + Cache cache = cacheManager.getCache("clear-test") + assertThat cache, notNullValue() + cache.put("one", "value") + cache.clear() + assertThat cache.get("one"), nullValue() + } + + @Test + void remove() { + JCacheManager cacheManager = new JCacheManager() + cacheManager.init() + Cache cache = cacheManager.getCache("remove-test") + assertThat cache, notNullValue() + cache.put("one", "value1") + cache.put("two", "value2") + cache.remove("one") + assertThat cache.get("one"), nullValue() + assertThat cache.get("two"), is("value2") + } + + @Test + void values() { + JCacheManager cacheManager = new JCacheManager() + cacheManager.init() + Cache cache = cacheManager.getCache("values-test") + assertThat cache, notNullValue() + cache.put("one", "value1") + cache.put("two", "value2") + assertThat cache.values(), containsInAnyOrder("value1", "value2") + } + + @Test + void keys() { + JCacheManager cacheManager = new JCacheManager() + cacheManager.init() + Cache cache = cacheManager.getCache("keys-test") + assertThat cache, notNullValue() + cache.put("one", "value1") + cache.put("two", "value2") + assertThat cache.keys(), containsInAnyOrder("one", "two") + } + + @Test + void putWithPrevious() { + JCacheManager cacheManager = new JCacheManager() + cacheManager.init() + Cache cache = cacheManager.getCache("putWithPrevious-test") + assertThat cache, notNullValue() + assertThat cache.put("one", "value1"), nullValue() + assertThat cache.put("one", "value2"), is("value1") + assertThat cache.get("one"), is("value2") + } + + @Test + void destroy() { + JCacheManager cacheManager = new JCacheManager() + cacheManager.init() + assertThat cacheManager.cacheManagerImplicitlyCreated, is(true) + Cache cache = cacheManager.getCache("destroy-test") + assertThat cache.put("one", "value1"), nullValue() + cacheManager.destroy() + assertThat cacheManager.cacheManagerImplicitlyCreated, is(false) + assertThat cacheManager.jCacheManager, nullValue() + } + + static <T extends Throwable> T expectThrows(Class<T> exceptionClass, Closure closure) { + try { + closure.run() + } catch (Throwable t) { + if (exceptionClass.isAssignableFrom(t.getClass())) { + return t as T + } + throw t + } + Assert.fail("Expected ${exceptionClass.getName()} to be thrown"); + return null + } +} diff --git a/support/pom.xml b/support/pom.xml index bb2bb89e..f7e328cc 100644 --- a/support/pom.xml +++ b/support/pom.xml @@ -32,6 +32,7 @@ <modules> <module>aspectj</module> + <module>jcache</module> <module>ehcache</module> <module>hazelcast</module> <module>quartz</module>
