You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@shiro.apache.org by fp...@apache.org on 2023/01/29 18:21:29 UTC

[shiro] 01/03: Adds cache module for JCache

This is an automated email from the ASF dual-hosted git repository.

fpapon pushed a commit to branch jcache-1.11
in repository https://gitbox.apache.org/repos/asf/shiro.git

commit 46dfc427859da3d8a805143f5dd33cc9810de009
Author: Brian Demers <bd...@apache.org>
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 a980a7b7..0562bb75 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>