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..38fb4f3739
--- /dev/null
+++ b/support/jcache/src/main/java/org/apache/shiro/cache/jcache/JCacheManager.java
@@ -0,0 +1,273 @@
+/*
+ * 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.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