From 74b1a1d898288f48a1e1d913ba39e4bd8fc6140e Mon Sep 17 00:00:00 2001 From: Brian Demers Date: Fri, 14 Jan 2022 18:25:44 -0500 Subject: [PATCH 1/2] 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 ++++++ .../shiro/cache/jcache/JCacheManager.java | 255 ++++++++++++++++++ .../jcache/src/main/resources/META-INF/NOTICE | 15 ++ .../cache/jcache/JCacheManagerTest.groovy | 146 ++++++++++ support/pom.xml | 1 + 5 files changed, 505 insertions(+) create mode 100644 support/jcache/pom.xml create mode 100644 support/jcache/src/main/java/org/apache/shiro/cache/jcache/JCacheManager.java create mode 100644 support/jcache/src/main/resources/META-INF/NOTICE create mode 100644 support/jcache/src/test/groovy/org/apache/shiro/cache/jcache/JCacheManagerTest.groovy diff --git a/support/jcache/pom.xml b/support/jcache/pom.xml new file mode 100644 index 0000000000..6e45484cad --- /dev/null +++ b/support/jcache/pom.xml @@ -0,0 +1,88 @@ + + + + + + org.apache.shiro + shiro-support + 1.8.1-SNAPSHOT + ../pom.xml + + + 4.0.0 + shiro-jcache + Apache Shiro :: Support :: JCache + bundle + + + [1.1,2) + + + + + org.apache.shiro + shiro-cache + + + javax.cache + cache-api + 1.1.1 + + + + org.slf4j + jcl-over-slf4j + test + + + org.slf4j + slf4j-simple + test + + + org.cache2k + cache2k-jcache + 2.4.1.Final + test + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + org.apache.shiro.jcache + org.apache.shiro.jcache*;version=${project.version} + + org.apache.shiro*;version="${shiro.osgi.importRange}", + com.hazelcast*;version="${jcache.osgi.importRange}", + * + + + + + + + + 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 0000000000..8760652a5b --- /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. + *

+ * 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. + *

+ * This implementation requires a JCache implementation available on the classpath. + *

+ * @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 Cache getCache(String name) throws CacheException { + + javax.cache.Cache 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. + *

+ * If a CacheManager has been + * explicitly set (e.g. via Dependency Injection or programmatically) prior to calling this + * method, this method does nothing. + *

+ * 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 only if implicitly created. + *

+ * 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 implements Cache { + + private final javax.cache.Cache cache; + + JCache(javax.cache.Cache 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. + * + *

If no element matches, nothing is removed and no Exception is thrown.

+ * + * @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 keys() { + return toStream(cache.iterator()) + .map(javax.cache.Cache.Entry::getKey) + .collect(Collectors.toSet()); + } + + @Override + public Collection values() { + return toStream(cache.iterator()) + .map(javax.cache.Cache.Entry::getValue) + .collect(Collectors.toSet()); + } + + private Stream> toStream(Iterator> 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 0000000000..9d26a95ffb --- /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 0000000000..a4e44a3f6e --- /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 expectThrows(Class 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 97dbf1c164..83e6aa24f8 100644 --- a/support/pom.xml +++ b/support/pom.xml @@ -33,6 +33,7 @@ aspectj + jcache ehcache hazelcast quartz From 1aa455e378022fb28f558f5c81848bebef5d682a Mon Sep 17 00:00:00 2001 From: Brian Demers Date: Fri, 14 Jan 2022 18:30:54 -0500 Subject: [PATCH 2/2] Add missing license header --- .../shiro/cache/jcache/JCacheManager.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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 index 8760652a5b..38fb4f3739 100644 --- 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 @@ -1,3 +1,21 @@ +/* + * 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;