From 44bed7ae2b335d0a1a44113e7aebdfaa50ce82fc Mon Sep 17 00:00:00 2001 From: Arvind Krishnakumar <61501885+arvindkrishnakumar-okta@users.noreply.github.com> Date: Thu, 6 Oct 2022 11:13:43 -0500 Subject: [PATCH] OKTA-526761: Add Caching support (#768) * add caching --- README.md | 46 ++ THIRD-PARTY-NOTICES | 2 +- api/pom.xml | 2 +- .../main/java/com/okta/sdk/cache/Cache.java | 57 +++ .../sdk/cache/CacheConfigurationBuilder.java | 89 ++++ .../java/com/okta/sdk/cache/CacheManager.java | 40 ++ .../okta/sdk/cache/CacheManagerBuilder.java | 123 ++++++ .../main/java/com/okta/sdk/cache/Caches.java | 130 ++++++ .../com/okta/sdk/client/ClientBuilder.java | 59 +++ ...{apiClient.mustache => ApiClient.mustache} | 111 ++++- .../main/java/quickstart/ReadmeSnippets.java | 23 + impl/pom.xml | 34 +- .../sdk/impl/cache/CacheConfiguration.java | 48 ++ .../com/okta/sdk/impl/cache/DefaultCache.java | 417 ++++++++++++++++++ .../impl/cache/DefaultCacheConfiguration.java | 71 +++ .../DefaultCacheConfigurationBuilder.java | 66 +++ .../sdk/impl/cache/DefaultCacheManager.java | 271 ++++++++++++ .../cache/DefaultCacheManagerBuilder.java | 82 ++++ .../okta/sdk/impl/cache/DisabledCache.java | 61 +++ .../sdk/impl/cache/DisabledCacheManager.java | 42 ++ .../sdk/impl/client/DefaultClientBuilder.java | 93 +++- .../sdk/impl/config/ClientConfiguration.java | 62 ++- .../config/DefaultEnvVarNameConverter.java | 2 + .../com/okta/sdk/impl/cache/CachesTest.groovy | 76 ++++ .../impl/cache/DefaultCacheManagerTest.groovy | 136 ++++++ .../sdk/impl/cache/DefaultCacheTest.groovy | 266 +++++++++++ .../cache/DisabledCacheManagerTest.groovy | 42 ++ .../sdk/impl/cache/DisabledCacheTest.groovy | 42 ++ pom.xml | 14 +- 29 files changed, 2451 insertions(+), 56 deletions(-) create mode 100644 api/src/main/java/com/okta/sdk/cache/Cache.java create mode 100644 api/src/main/java/com/okta/sdk/cache/CacheConfigurationBuilder.java create mode 100644 api/src/main/java/com/okta/sdk/cache/CacheManager.java create mode 100644 api/src/main/java/com/okta/sdk/cache/CacheManagerBuilder.java create mode 100644 api/src/main/java/com/okta/sdk/cache/Caches.java rename api/src/main/resources/custom_templates/{apiClient.mustache => ApiClient.mustache} (87%) create mode 100644 impl/src/main/java/com/okta/sdk/impl/cache/CacheConfiguration.java create mode 100644 impl/src/main/java/com/okta/sdk/impl/cache/DefaultCache.java create mode 100644 impl/src/main/java/com/okta/sdk/impl/cache/DefaultCacheConfiguration.java create mode 100644 impl/src/main/java/com/okta/sdk/impl/cache/DefaultCacheConfigurationBuilder.java create mode 100644 impl/src/main/java/com/okta/sdk/impl/cache/DefaultCacheManager.java create mode 100644 impl/src/main/java/com/okta/sdk/impl/cache/DefaultCacheManagerBuilder.java create mode 100644 impl/src/main/java/com/okta/sdk/impl/cache/DisabledCache.java create mode 100644 impl/src/main/java/com/okta/sdk/impl/cache/DisabledCacheManager.java create mode 100644 impl/src/test/groovy/com/okta/sdk/impl/cache/CachesTest.groovy create mode 100644 impl/src/test/groovy/com/okta/sdk/impl/cache/DefaultCacheManagerTest.groovy create mode 100644 impl/src/test/groovy/com/okta/sdk/impl/cache/DefaultCacheTest.groovy create mode 100644 impl/src/test/groovy/com/okta/sdk/impl/cache/DisabledCacheManagerTest.groovy create mode 100644 impl/src/test/groovy/com/okta/sdk/impl/cache/DisabledCacheTest.groovy diff --git a/README.md b/README.md index 27a3b50fa48..ee43b2c04f4 100644 --- a/README.md +++ b/README.md @@ -565,6 +565,52 @@ okta.client.requestTimeout = 0 okta.client.rateLimit.maxRetries = 0 ``` +## Caching + +By default, a simple production-grade in-memory CacheManager will be enabled when the Client instance is created. This CacheManager implementation has the following characteristics: + +- It assumes a default time-to-live and time-to-idle of 1 hour for all cache entries. +- It auto-sizes itself based on your application's memory usage. It will not cause OutOfMemoryExceptions. + +**The default cache manager is not suitable for an application deployed across multiple JVMs.** + +This is because the default implementation is 100% in-memory (in-process) in the current JVM. If more than one JVM is deployed with the same application codebase - for example, a web application deployed on multiple identical hosts for scaling or high availability - each JVM would have it's own in-memory cache. + +As a result, if your application that uses an Okta Client instance is deployed across multiple JVMs, you SHOULD ensure that the Client is configured with a CacheManager implementation that uses coherent and clustered/distributed memory. + +See the [`ClientBuilder` Javadoc](https://developer.okta.com/okta-sdk-java/apidocs/com/okta/sdk/client/ClientBuilder) for more details on caching. + +### Caching for applications deployed on a single JVM + +If your application is deployed on a single JVM and you still want to use the default CacheManager implementation, but the default cache configuration does not meet your needs, you can specify a different configuration. For example: + +[//]: # (method: complexCaching) +```java +Caches.newCacheManager() + .withDefaultTimeToLive(300, TimeUnit.SECONDS) // default + .withDefaultTimeToIdle(300, TimeUnit.SECONDS) //general default + .withCache(forResource(User.class) //User-specific cache settings + .withTimeToLive(1, TimeUnit.HOURS) + .withTimeToIdle(30, TimeUnit.MINUTES)) + .withCache(forResource(Group.class) //Group-specific cache settings + .withTimeToLive(1, TimeUnit.HOURS)) + //... etc ... + .build(); +``` +[//]: # (end: complexCaching) + +### Disable Caching + +While production applications will usually enable a working CacheManager as described above, you might wish to disable caching entirely. You can do this by configuring a disabled CacheManager instance. For example: + +[//]: # (method: disableCaching) +```java +ApiClient client = Clients.builder() + .setCacheManager(Caches.newDisabledCacheManager()) + .build(); +``` +[//]: # (end: disableCaching) + ## Building the SDK In most cases, you won't need to build the SDK from source. If you want to build it yourself, take a look at the [build instructions wiki](https://github.com/okta/okta-sdk-java/wiki/Build-It) (though just cloning the repo and running `mvn install` should get you going). diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index 9cd5386a2d4..503c93c536f 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -53,10 +53,10 @@ This project includes: Jackson-Datatype-ThreeTenBackport under The Apache Software License, Version 2.0 Jackson-JAXRS: base under The Apache Software License, Version 2.0 Jackson-JAXRS: JSON under The Apache Software License, Version 2.0 + Jakarta Activation API jar under EDL 1.0 Jakarta Bean Validation API under Apache License 2.0 Jakarta XML Binding API under Eclipse Distribution License - v 1.0 Java Native Access under LGPL, version 2.1 or Apache License v2.0 - JavaBeans Activation Framework API jar under EDL 1.0 JavaMail API (no providers) under CDDL/GPLv2+CE javax.annotation API under CDDL + GPLv2 with classpath exception JJWT :: API under Apache License, Version 2.0 diff --git a/api/pom.xml b/api/pom.xml index 193d510999c..d6c5e65a882 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -160,7 +160,7 @@ org.openapitools openapi-generator-maven-plugin - 6.0.1 + 6.2.0 diff --git a/api/src/main/java/com/okta/sdk/cache/Cache.java b/api/src/main/java/com/okta/sdk/cache/Cache.java new file mode 100644 index 00000000000..c22005356b0 --- /dev/null +++ b/api/src/main/java/com/okta/sdk/cache/Cache.java @@ -0,0 +1,57 @@ +/* + * Copyright 2014 Stormpath, Inc. + * Modifications Copyright 2018 Okta, Inc. + * + * 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 com.okta.sdk.cache; + +/** + * A Cache efficiently stores temporary objects primarily to improve an application's performance. + *

+ * This interface provides an abstraction (wrapper) API on top of an underlying + * cache framework's cache instance (e.g. JCache, Ehcache, Hazelcast, JCS, OSCache, JBossCache, TerraCotta, Coherence, + * GigaSpaces, etc, etc), allowing a Okta SDK user to configure any cache mechanism they choose. + * + * @since 0.5.0 + */ +public interface Cache { + + /** + * Returns the cached value stored under the specified {@code key} or + * {@code null} if there is no cache entry for that {@code key}. + * + * @param key the key that the value was previous added with + * @return the cached object or {@code null} if there is no entry for the specified {@code key} + */ + V get(K key); + + + /** + * Adds a cache entry. + * + * @param key the key used to identify the object being stored. + * @param value the value to be stored in the cache. + * @return the previous value associated with the given {@code key} or {@code null} if there was no previous value + */ + V put(K key, V value); + + + /** + * Removes the cached value stored under the specified {@code key}. + * + * @param key the key used to identify the object being stored. + * @return the removed value or {@code null} if there was no value cached. + */ + V remove(K key); +} diff --git a/api/src/main/java/com/okta/sdk/cache/CacheConfigurationBuilder.java b/api/src/main/java/com/okta/sdk/cache/CacheConfigurationBuilder.java new file mode 100644 index 00000000000..47962a01ee0 --- /dev/null +++ b/api/src/main/java/com/okta/sdk/cache/CacheConfigurationBuilder.java @@ -0,0 +1,89 @@ +/* + * Copyright 2014 Stormpath, Inc. + * Modifications Copyright 2018 Okta, Inc. + * + * 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 com.okta.sdk.cache; + +import java.util.concurrent.TimeUnit; + +/** + * A Builder to specify configuration for {@link Cache} regions. This is usually used while building a CacheManager via + * the {@link CacheManagerBuilder}. CacheConfigurationBuilders can be constructed with the {@link Caches Caches} + * utility class. For example: + *

+ * Caches.named("cacheRegionNameHere")
+ *     .{@link #withTimeToLive(long, TimeUnit) withTimeToLive(1, TimeUnit.DAYS)}
+ *     .{@link #withTimeToIdle(long, TimeUnit) withTimeToIdle(2, TimeUnit.HOURS)};
+ * 
+ * or + *
+ * Caches.forResource(Account.class)
+ *     .{@link #withTimeToLive(long, TimeUnit) withTimeToLive(1, TimeUnit.DAYS)}
+ *     .{@link #withTimeToIdle(long, TimeUnit) withTimeToIdle(2, TimeUnit.HOURS)};
+ * 
+ * + * @see #withTimeToLive(long, TimeUnit) + * @see #withTimeToIdle(long, TimeUnit) + * @see Caches#forResource(Class) + * @see Caches#named(String) + * @since 0.5.0 + */ +public interface CacheConfigurationBuilder { + + /** + * Sets the associated {@code Cache} region's entry Time to Live (TTL). + *

+ * Time to Live is the amount of time a cache entry may exist after first being created before it will expire and no + * longer be available. If a cache entry ever becomes older than this amount of time (regardless of how often + * it is accessed), it will be removed from the cache as soon as possible. + *

+ * If this value is not configured, it is assumed that the Cache's entries could potentially live indefinitely. + * Note however that entries can still be expunged due to other conditions (e.g. memory constraints, Time to + * Idle setting, etc). + * Usage + *

+     *     ...withTimeToLive(30, TimeUnit.MINUTES)...
+     *     ...withTimeToLive(1, TimeUnit.HOURS)...
+     * 
+ * + * @param ttl Time To Live scalar value + * @param ttlTimeUnit Time to Live unit of time + * @return the associated {@code Cache} region's entry Time to Live (TTL). + */ + CacheConfigurationBuilder withTimeToLive(long ttl, TimeUnit ttlTimeUnit); + + /** + * Sets the associated {@code Cache} region's entry Time to Idle (TTI). + *

+ * Time to Idle is the amount of time a cache entry may be idle (unused / not accessed) before it will expire and + * no longer be available. If a cache entry is not accessed at all after this amount of time, it will be removed + * from the cache as soon as possible. + *

+ * If this value is not configured, it is assumed that the Cache's entries could potentially live indefinitely. + * Note however that entries can still be expunged due to other conditions (e.g. memory constraints, Time to + * Live setting, etc). + * Usage + *

+     *     ...withTimeToIdle(30, TimeUnit.MINUTES)...
+     *     ...withTimeToIdle(1, TimeUnit.HOURS)...
+     * 
+ * + * @param tti Time To Idle scalar value + * @param ttiTimeUnit Time to Idle unit of time + * @return the associated {@code Cache} region's entry Time to Idle (TTI). + */ + CacheConfigurationBuilder withTimeToIdle(long tti, TimeUnit ttiTimeUnit); + +} diff --git a/api/src/main/java/com/okta/sdk/cache/CacheManager.java b/api/src/main/java/com/okta/sdk/cache/CacheManager.java new file mode 100644 index 00000000000..20dcc75e6cc --- /dev/null +++ b/api/src/main/java/com/okta/sdk/cache/CacheManager.java @@ -0,0 +1,40 @@ +/* + * Copyright 2014 Stormpath, Inc. + * Modifications Copyright 2018 Okta, Inc. + * + * 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 com.okta.sdk.cache; + +/** + * A CacheManager provides and maintains the lifecycle of {@link Cache Cache} instances. + *

+ * This interface provides an abstraction (wrapper) API on top of an underlying + * cache framework's main Manager component (e.g. JCache, Ehcache, Hazelcast, JCS, OSCache, JBossCache, TerraCotta, + * Coherence, GigaSpaces, etc, etc), allowing a Okta SDK user to configure any cache mechanism they choose. + * + * @since 0.5.0 + */ +public interface CacheManager { + + /** + * Acquires the cache with the specified {@code name}. If a cache does not yet exist with that name, a new one + * will be created with that name and returned. + * + * @param name the name of the cache to acquire. + * @param type of cache key + * @param type of cache value + * @return the Cache with the given name + */ + Cache getCache(String name); +} diff --git a/api/src/main/java/com/okta/sdk/cache/CacheManagerBuilder.java b/api/src/main/java/com/okta/sdk/cache/CacheManagerBuilder.java new file mode 100644 index 00000000000..88f69864297 --- /dev/null +++ b/api/src/main/java/com/okta/sdk/cache/CacheManagerBuilder.java @@ -0,0 +1,123 @@ +/* + * Copyright 2014 Stormpath, Inc. + * Modifications Copyright 2018 Okta, Inc. + * + * 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 com.okta.sdk.cache; + +import java.util.concurrent.TimeUnit; + +/** + * Builder for creating simple {@link CacheManager} instances suitable for SINGLE-JVM APPLICATIONS. If your + * application is deployed (mirrored or clustered) across multiple JVMs, you might not + * want to use this builder and use your own clusterable CacheManager implementation instead. See Clustering below. + * Clustering + * The default CacheManager instances created by this Builder DO NOT SUPPORT CLUSTERING. + *

+ * If you use this Builder and your application is deployed on multiple JVMs, each of your application instances will + * have their own local cache of Okta data. Depending on your application requirements, and your + * cache TTL and TTI settings, this could introduce a significant difference in cached data seen by your application + * instances, which would likely impact user management behavior. For example, one application instance could see an + * account as ENABLED, but the other application instance could see it as DISABLED. + *

+ * For some applications, this discrepancy might be an acceptable trade-off, especially if you configure + * {@link #withDefaultTimeToIdle(long, TimeUnit) timeToIdle} and + * {@link #withDefaultTimeToLive(long, TimeUnit) timeToLive} settings low enough. For example, + * maybe a TTL of 5 or 10 minutes is an acceptable time to see 'stale' account data. For other applications, this might + * not be acceptable. If it is acceptable, configuring the timeToIdle and timeToLive settings will allow you to + * fine-tune how much variance you allow. + *

+ * However, if you are concerned about this difference in data and you want the Okta SDK's cache to be coherent + * across your application nodes (typically a good thing to have), it is strongly recommended that you do not use this + * Builder and instead configure the Okta SDK with a clustered {@code CacheManager} implementation of your choosing. + * This approach still gives you excellent performance improvements and ensures that your cached data is coherent (seen + * as the same) across all of your application instances. + *

+ * This comes with an increased cost of course: setting up a caching product and/or cluster. However, this is not + * much of a problem in practice: most multi-instance applications already leverage caching clusters for their own + * application needs. In these environments, and with a proper {@code CacheManager} implementation leveraging a + * clustered cache, the Okta Java SDK will live quite happily using this same caching infrastructure. + *

+ * A coherent cache deployment ensures all of your application instances/nodes can utilize the same cache policy and + * see the same cached security/identity data. Some example clustered caching solutions: Hazelcast, + * Ehcache+Terracotta, Memcache, Redis, Coherence, GigaSpaces, etc. + * + * @since 0.5.0 + */ +public interface CacheManagerBuilder { + + /** + * Sets the default Time to Live (TTL) for all cache regions managed by the {@link #build() built} + * {@code CacheManager}. You may override this default for individual cache regions by using the + * {@link #withCache(CacheConfigurationBuilder) withCache} for each region you wish to configure. + *

+ * Time to Live is the amount of time a cache entry may exist after first being created before it will expire and no + * longer be available. If a cache entry ever becomes older than this amount of time (regardless of how often + * it is accessed), it will be removed from the cache as soon as possible. + *

+ * If this value is not configured, it is assumed that cache entries could potentially live indefinitely. + * Note however that entries can still be expunged due to other conditions (e.g. memory constraints, Time to + * Idle setting, etc). + * Usage + *

+     *     ...withDefaultTimeToLive(30, TimeUnit.MINUTES)...
+     *     ...withDefaultTimeToLive(1, TimeUnit.HOURS)...
+     * 
+ * + * @param ttl default Time To Live scalar value + * @param timeUnit default Time to Live unit of time + * @return the builder instance for method chaining. + */ + CacheManagerBuilder withDefaultTimeToLive(long ttl, TimeUnit timeUnit); + + /** + * Sets the default Time to Idle (TTI) for all cache regions managed by the {@link #build() built} + * {@code CacheManager}. You may override this default for individual cache regions by using the + * {@link #withCache(CacheConfigurationBuilder) withCache} for each region you wish to configure. + *

+ * Time to Idle is the amount of time a cache entry may be idle (unused / not accessed) before it will expire and + * no longer be available. If a cache entry is not accessed at all after this amount of time, it will be removed + * from the cache as soon as possible. + *

+ * If this value is not configured, it is assumed that cache entries could potentially live indefinitely. + * Note however that entries can still be expunged due to other conditions (e.g. memory constraints, Time to + * Live setting, etc). + * Usage + *

+     *     ...withDefaultTimeToLive(30, TimeUnit.MINUTES)...
+     *     ...withDefaultTimeToLive(1, TimeUnit.HOURS)...
+     * 
+ * + * @param tti default Time To Idle scalar value + * @param timeUnit default Time to Idle unit of time + * @return the builder instance for method chaining. + */ + CacheManagerBuilder withDefaultTimeToIdle(long tti, TimeUnit timeUnit); + + /** + * Adds configuration settings for a specific Cache region managed by the {@link #build() built} + * {@code CacheManager}, like the region's Time to Live and Time to Idle. + * + * @param builder the CacheConfigurationBuilder instance that will be used to a cache's configuration. + * @return this instance for method chaining. + */ + CacheManagerBuilder withCache(CacheConfigurationBuilder builder); + + /** + * Returns a new {@link CacheManager} instance reflecting Builder's current configuration. + * + * @return a new {@link CacheManager} instance reflecting Builder's current configuration. + */ + CacheManager build(); +} diff --git a/api/src/main/java/com/okta/sdk/cache/Caches.java b/api/src/main/java/com/okta/sdk/cache/Caches.java new file mode 100644 index 00000000000..81453f2bac9 --- /dev/null +++ b/api/src/main/java/com/okta/sdk/cache/Caches.java @@ -0,0 +1,130 @@ +/* + * Copyright 2014 Stormpath, Inc. + * Modifications Copyright 2018 Okta, Inc. + * + * 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 com.okta.sdk.cache; + +import com.okta.commons.lang.Classes; + +/** + * Static utility/helper factory methods for + * building {@link CacheManager}s and their associated cache regions, suitable for SINGLE-JVM APPLICATIONS. + * + *

If your application is deployed on multiple JVMs (e.g. a distributed/clustered web app), you might not want to + * use the builders here and instead implement the {@link CacheManager} API directly to use your distributed/clustered + * cache technology of choice.

+ * + *

See the {@link CacheManagerBuilder} JavaDoc for more information the effects of caching in + * single-jvm vs distributed-jvm applications.

+ * + * Usage Example + * + *
+ * import static com.okta.sdk.cache.Caches.*;
+ *
+ * ...
+ *
+ * Caches.{@link #newCacheManager() newCacheManager()}
+ *     .withDefaultTimeToLive(1, TimeUnit.DAYS) //general default
+ *     .withDefaultTimeToIdle(2, TimeUnit.HOURS) //general default
+ *     .withCache({@link Caches#forResource(Class) forResource}(Account.class) //Account-specific cache settings
+ *         .withTimeToLive(1, TimeUnit.HOURS)
+ *         .withTimeToIdle(30, TimeUnit.MINUTES))
+ *     .withCache({@link Caches#forResource(Class) forResource}(Group.class) //Group-specific cache settings
+ *         .withTimeToLive(2, TimeUnit.HOURS))
+ *
+ *     // ... etc ...
+ *
+ *     .build(); //build the CacheManager
+ * 
+ * + *

The above TTL and TTI times are just examples showing API usage - the times themselves are not + * recommendations. Choose TTL and TTI times based on your application requirements.

+ * + * @since 0.5.0 + */ +public class Caches { + + /** + * Instantiates a new {@code CacheManagerBuilder} suitable for SINGLE-JVM APPLICATIONS. If your application + * is deployed on multiple JVMs (e.g. for a distributed/clustered web app), you might not want to use this method + * and instead implement the {@link CacheManager} API directly to use your distributed/clustered cache technology + * of choice. + * + *

See the {@link CacheManagerBuilder} JavaDoc for more information the effects of caching in + * single-jvm vs distributed-jvm applications.

+ * + * @return a new {@code CacheManagerBuilder} suitable for SINGLE-JVM APPLICATIONS. + */ + public static CacheManagerBuilder newCacheManager() { + return (CacheManagerBuilder) Classes.newInstance("com.okta.sdk.impl.cache.DefaultCacheManagerBuilder"); + } + + /** + * Instantiates a new {@code CacheManager} that disables caching entirely. While production applications + * will usually enable a working CacheManager, you might configure a disabled CacheManager for + * your Client when testing or debugging to remove 'moving parts' for better clarity into request/response + * behavior. + * + * @return a new disabled {@code CacheManager} instance. All caching + */ + public static CacheManager newDisabledCacheManager() { + return (CacheManager) Classes.newInstance("com.okta.sdk.impl.cache.DisabledCacheManager"); + } + + /** + * Returns a new {@link CacheConfigurationBuilder} to configure a cache region that will store data for instances + * of type {@code clazz}. + * + *

This is a convenience method equivalent to {@link #named(String) named(clazz.getName())}, but it could help + * with readability, for example:

+ * + *
+     * import static com.okta.sdk.cache.Caches.*
+     * ...
+     * newCacheManager()
+     *     .withCache(forResource(Account.class).withTimeToIdle(10, TimeUnit.MINUTES))
+     *     .build();
+     * 
+ * + * @param clazz the resource class that will have a backing cache region for storing data of that type + * @param Resource sub-interface + * @return a new {@link CacheConfigurationBuilder} to configure a cache region that will store data for instances + * of type {@code clazz}. + */ + public static CacheConfigurationBuilder forResource(Class clazz) { + return named(clazz.getName()); + } + + /** + * Returns a new {@link CacheConfigurationBuilder} used to configure a cache region with the specified name. For + * example: + * + *
+     * import static com.okta.sdk.cache.Caches.*
+     * ...
+     * newCacheManager()
+     *     .withCache(named("myCacheRegion").withTimeToIdle(10, TimeUnit.MINUTES))
+     *     .build();
+     * 
+ * + * @param name the name of the cache region for which the configuration will apply + * @return a new {@link CacheConfigurationBuilder} used to configure a cache region with the specified name. + */ + public static CacheConfigurationBuilder named(String name) { + return (CacheConfigurationBuilder) Classes.newInstance("com.okta.sdk.impl.cache.DefaultCacheConfigurationBuilder", name); + } + +} diff --git a/api/src/main/java/com/okta/sdk/client/ClientBuilder.java b/api/src/main/java/com/okta/sdk/client/ClientBuilder.java index 4c425c64f3f..c8fa594e9f8 100644 --- a/api/src/main/java/com/okta/sdk/client/ClientBuilder.java +++ b/api/src/main/java/com/okta/sdk/client/ClientBuilder.java @@ -18,6 +18,7 @@ import com.okta.commons.http.config.Proxy; import com.okta.sdk.authc.credentials.ClientCredentials; +import com.okta.sdk.cache.CacheManager; import org.openapitools.client.ApiClient; import java.io.InputStream; @@ -64,6 +65,10 @@ public interface ClientBuilder { String DEFAULT_CLIENT_API_TOKEN_PROPERTY_NAME = "okta.client.token"; + String DEFAULT_CLIENT_CACHE_ENABLED_PROPERTY_NAME = "okta.client.cache.enabled"; + String DEFAULT_CLIENT_CACHE_TTL_PROPERTY_NAME = "okta.client.cache.defaultTtl"; + String DEFAULT_CLIENT_CACHE_TTI_PROPERTY_NAME = "okta.client.cache.defaultTti"; + String DEFAULT_CLIENT_CACHE_CACHES_PROPERTY_NAME = "okta.client.cache.caches"; String DEFAULT_CLIENT_ORG_URL_PROPERTY_NAME = "okta.client.orgUrl"; String DEFAULT_CLIENT_CONNECTION_TIMEOUT_PROPERTY_NAME = "okta.client.connectionTimeout"; String DEFAULT_CLIENT_AUTHENTICATION_SCHEME_PROPERTY_NAME = "okta.client.authenticationScheme"; @@ -104,6 +109,60 @@ public interface ClientBuilder { */ ClientBuilder setProxy(Proxy proxy); + /** + * Sets the {@link CacheManager} that should be used to cache Okta REST resources, reducing round-trips to the + * Okta API server and enhancing application performance. + * + * Single JVM Applications + * + *

If your application runs on a single JVM-based applications, the + * {@link com.okta.sdk.cache.CacheManagerBuilder CacheManagerBuilder} should be sufficient for your needs. You + * create a {@code CacheManagerBuilder} by using the {@link com.okta.sdk.cache.Caches Caches} utility class, + * for example:

+ * + *
+     * import static com.okta.sdk.cache.Caches.*;
+     *
+     * ...
+     *
+     * ApiClient client = Clients.builder()...
+     *     .setCacheManager(
+     *         {@link com.okta.sdk.cache.Caches#newCacheManager() newCacheManager()}
+     *         .withDefaultTimeToLive(1, TimeUnit.DAYS) //general default
+     *         .withDefaultTimeToIdle(2, TimeUnit.HOURS) //general default
+     *         .withCache({@link com.okta.sdk.cache.Caches#forResource(Class) forResource}(User.class) //User-specific cache settings
+     *             .withTimeToLive(1, TimeUnit.HOURS)
+     *             .withTimeToIdle(30, TimeUnit.MINUTES))
+     *         .withCache({@link com.okta.sdk.cache.Caches#forResource(Class) forResource}(Group.class) //Group-specific cache settings
+     *             .withTimeToLive(2, TimeUnit.HOURS))
+     *         .build() //build the CacheManager
+     *     )
+     *     .build(); //build the Client
+     * 
+ * + *

The above TTL and TTI times are just examples showing API usage - the times themselves are not + * recommendations. Choose TTL and TTI times based on your application requirements.

+ * + * Multi-JVM / Clustered Applications + * + *

The default {@code CacheManager} instances returned by the + * {@link com.okta.sdk.cache.CacheManagerBuilder CacheManagerBuilder} might not be sufficient for a + * multi-instance application that runs on multiple JVMs and/or hosts/servers, as there could be cache-coherency + * problems across the JVMs. See the {@link com.okta.sdk.cache.CacheManagerBuilder CacheManagerBuilder} + * JavaDoc for additional information.

+ * + *

In these multi-JVM environments, you will likely want to create a simple CacheManager implementation that + * wraps your distributed Caching API/product of choice and then plug that implementation in to the Okta SDK + * via this method. Hazelcast is one known cluster-safe caching product, and the Okta SDK has out-of-the-box + * support for this as an extension module. See the top-level class JavaDoc for a Hazelcast configuration + * example.

+ * + * @param cacheManager the {@link CacheManager} that should be used to cache Okta REST resources, reducing + * round-trips to the Okta API server and enhancing application performance. + * @return the ClientBuilder instance for method chaining + */ + ClientBuilder setCacheManager(CacheManager cacheManager); + /** * Overrides the default (very secure) * Okta SSWS Digest diff --git a/api/src/main/resources/custom_templates/apiClient.mustache b/api/src/main/resources/custom_templates/ApiClient.mustache similarity index 87% rename from api/src/main/resources/custom_templates/apiClient.mustache rename to api/src/main/resources/custom_templates/ApiClient.mustache index c990eba198a..01ff5876ae1 100644 --- a/api/src/main/resources/custom_templates/apiClient.mustache +++ b/api/src/main/resources/custom_templates/ApiClient.mustache @@ -19,6 +19,15 @@ package {{invokerPackage}}; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; {{/withXml}} +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.okta.commons.lang.Classes; +import com.okta.sdk.cache.Cache; +import com.okta.sdk.cache.CacheManager; +import java.util.LinkedHashMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.core.ParameterizedTypeReference; @@ -37,11 +46,11 @@ import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; {{#withXml}} import org.springframework.http.converter.HttpMessageConverter; - import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; {{/withXml}} import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; @@ -76,6 +85,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.TimeZone; import java.util.stream.Collectors; @@ -135,18 +145,17 @@ private Map authentications; private DateFormat dateFormat; -public ApiClient() { -init(); -} +private Cache cache; -public ApiClient(RestTemplate restTemplate) { -this.restTemplate = restTemplate; +private CacheManager cacheManager; + +public ApiClient() { init(); } -public ApiClient(RestTemplate restTemplate, RetryTemplate retryTemplate) { +public ApiClient(RestTemplate restTemplate, CacheManager cacheManager) { this.restTemplate = restTemplate; -this.retryTemplate = retryTemplate; +this.cacheManager = cacheManager; init(); } @@ -178,6 +187,8 @@ if (restTemplate == null) { restTemplate = buildRestTemplate(); } retryTemplate = buildRetryTemplate(); + +cache = cacheManager.getCache("default"); } /** @@ -745,14 +756,72 @@ List currentInterceptors = this.restTemplate.getIn RequestEntity requestEntity = requestBuilder.body(selectBody(body, formParams, contentType)); - ResponseEntity responseEntity = retryTemplate.execute(context -> restTemplate.exchange(requestEntity, returnType)); - - if (responseEntity.getStatusCode().is2xxSuccessful()) { - return responseEntity; - } else { - // The error handler built into the RestTemplate should handle 400 and 500 series errors. - throw new RestClientException("API returned " + responseEntity.getStatusCode() + " and it wasn't handled by the RestTemplate error handler"); - } + String cacheKey = requestEntity.getUrl().toString(); + + if (Objects.nonNull(requestEntity.getUrl().getQuery())) { + cacheKey = requestEntity.getUrl().toString().split("\\?")[0]; + } + + if (method == HttpMethod.DELETE) { + cache.remove(cacheKey); + } + + if (method == HttpMethod.GET && + !returnType.getType().getTypeName().contains("List") && + !(Objects.nonNull(requestEntity.getUrl().getQuery()) && requestEntity.getUrl().getQuery().contains("expand"))) { + + ResponseEntity cacheData = cache.get(cacheKey); + + if (Objects.isNull(cacheData) || !cacheData.hasBody()) { + ResponseEntity responseEntity = retryTemplate.execute(context -> restTemplate.exchange(requestEntity, returnType)); + + if (responseEntity != null && responseEntity.getStatusCode().is2xxSuccessful()) { + + if (method == HttpMethod.GET && Objects.nonNull(responseEntity.getBody())) { + + Map map = objectMapper().convertValue(responseEntity.getBody(), LinkedHashMap.class); + + String href = null; + + if (Objects.nonNull(map)) { + Map links = (Map) map.get("_links"); + if (Objects.nonNull(links)) { + LinkedHashMap self = links.get("self"); + if (Objects.nonNull(self)) { + href = (String) self.get("href"); + } + } + } + + if (Objects.nonNull(href)) { + cache.put(href, responseEntity); + } else { + cache.put(cacheKey, responseEntity); + } + } + + return responseEntity; + } else { + // The error handler built into the RestTemplate should handle 400 and 500 series errors. + throw new RestClientException("API returned " + responseEntity.getStatusCode() + " and it wasn't handled by the RestTemplate error handler"); + } + } else { + // return data from cache + return cache.get(cacheKey); + } + } else { + ResponseEntity responseEntity = retryTemplate.execute(context -> restTemplate.exchange(requestEntity, returnType)); + + if (responseEntity != null && responseEntity.getStatusCode().is2xxSuccessful()) { + if (cache.get(cacheKey) != null) { + cache.put(cacheKey, responseEntity); + } + return responseEntity; + } else { + // The error handler built into the RestTemplate should handle 400 and 500 series errors. + throw new RestClientException("API returned " + responseEntity.getStatusCode() + " and it wasn't handled by the RestTemplate error handler"); + } + } } /** @@ -841,6 +910,16 @@ List currentInterceptors = this.restTemplate.getIn return retryTemplate; } + private ObjectMapper objectMapper() { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.registerModule(new JsonNullableModule()); + + return objectMapper; + } + /** * Update query and header parameters based on authentication settings. * diff --git a/examples/quickstart/src/main/java/quickstart/ReadmeSnippets.java b/examples/quickstart/src/main/java/quickstart/ReadmeSnippets.java index 82964b07c53..d5417c721c4 100644 --- a/examples/quickstart/src/main/java/quickstart/ReadmeSnippets.java +++ b/examples/quickstart/src/main/java/quickstart/ReadmeSnippets.java @@ -16,6 +16,7 @@ package quickstart; import com.okta.sdk.authc.credentials.TokenClientCredentials; +import com.okta.sdk.cache.Caches; import com.okta.sdk.client.Clients; import com.okta.sdk.resource.common.PagedList; import com.okta.sdk.resource.group.GroupBuilder; @@ -55,6 +56,9 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.concurrent.TimeUnit; + +import static com.okta.sdk.cache.Caches.forResource; /** * Example snippets used for this projects README.md. @@ -289,6 +293,25 @@ private void paging() { usersPagedListOne.getItems().stream().forEach(tmpUser -> log.info("User: {}", tmpUser.getProfile().getEmail())); } + private void complexCaching() { + Caches.newCacheManager() + .withDefaultTimeToLive(300, TimeUnit.SECONDS) // default + .withDefaultTimeToIdle(300, TimeUnit.SECONDS) //general default + .withCache(forResource(User.class) //User-specific cache settings + .withTimeToLive(1, TimeUnit.HOURS) + .withTimeToIdle(30, TimeUnit.MINUTES)) + .withCache(forResource(Group.class) //Group-specific cache settings + .withTimeToLive(1, TimeUnit.HOURS)) + //... etc ... + .build(); //build the CacheManager + } + + private void disableCaching() { + ApiClient client = Clients.builder() + .setCacheManager(Caches.newDisabledCacheManager()) + .build(); + } + private static ApiClient buildApiClient(String orgBaseUrl, String apiKey) { ApiClient apiClient = new ApiClient(); diff --git a/impl/pom.xml b/impl/pom.xml index e31302f938c..4e0efb3640a 100644 --- a/impl/pom.xml +++ b/impl/pom.xml @@ -162,23 +162,23 @@ - - - - - - - - - - - - - - - - - + + org.codehaus.mojo + build-helper-maven-plugin + + + generate-sources + + add-source + + + + ${project.build.directory}/generated-sources/openapi + + + + + diff --git a/impl/src/main/java/com/okta/sdk/impl/cache/CacheConfiguration.java b/impl/src/main/java/com/okta/sdk/impl/cache/CacheConfiguration.java new file mode 100644 index 00000000000..13e8e14163d --- /dev/null +++ b/impl/src/main/java/com/okta/sdk/impl/cache/CacheConfiguration.java @@ -0,0 +1,48 @@ +/* + * Copyright 2014 Stormpath, Inc. + * Modifications Copyright 2018 Okta, Inc. + * + * 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 com.okta.sdk.impl.cache; + +import java.time.Duration; + +/** + * Represents configuration settings for a particular {@link com.okta.sdk.cache.Cache Cache} region. + * + * @since 0.5.0 + */ +public interface CacheConfiguration { + + /** + * Returns the name of the {@code Cache} for which this configuration applies. + * + * @return the name of the {@code Cache} for which this configuration applies. + */ + String getName(); + + /** + * Returns the Time-to-Live setting to apply for all entries in the associated {@code Cache}. + * + * @return the Time-to-Live setting to apply for all entries in the associated {@code Cache}. + */ + Duration getTimeToLive(); + + /** + * Returns the Time-to-Idle setting to apply for all entries in the associated {@code Cache}. + * + * @return the Time-to-Idle setting to apply for all entries in the associated {@code Cache}. + */ + Duration getTimeToIdle(); +} diff --git a/impl/src/main/java/com/okta/sdk/impl/cache/DefaultCache.java b/impl/src/main/java/com/okta/sdk/impl/cache/DefaultCache.java new file mode 100644 index 00000000000..a319494da8b --- /dev/null +++ b/impl/src/main/java/com/okta/sdk/impl/cache/DefaultCache.java @@ -0,0 +1,417 @@ +/* + * Copyright 2014 Stormpath, Inc. + * Modifications Copyright 2018 Okta, Inc. + * + * 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 com.okta.sdk.impl.cache; + +import com.okta.commons.lang.Assert; +import com.okta.sdk.cache.Cache; +import com.okta.sdk.impl.util.SoftHashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +/** + * A {@code DefaultCache} is a {@link Cache Cache} implementation that uses a backing {@link Map} instance to store + * and retrieve cached data. + * Thread Safety + * This implementation is thread-safe only if the backing map is thread-safe. + * + * @since 0.5.0 + */ +public class DefaultCache implements Cache { + + private final Logger logger = LoggerFactory.getLogger(DefaultCache.class); + + /** + * Backing map instance that stores the cache entries. + */ + private final Map> map; + + /** + * The amount of time allowed to pass since an entry was first created. An entry older than this time, + * regardless of how often it might be used, will be removed from the cache as soon as possible. + */ + private volatile Duration timeToLive; + + /** + * The amount of time allowed to pass since an entry was last used (inserted or accessed). An entry that has not + * been used in this amount of time will be removed from the cache as soon as possible. + */ + private volatile Duration timeToIdle; + + /** + * The name of this cache. + */ + private final String name; + + private final AtomicLong accessCount; + private final AtomicLong hitCount; + private final AtomicLong missCount; + + /** + * Creates a new {@code DefaultCache} instance with the specified {@code name}, expected to be unique among all + * other caches in the parent {@code CacheManager}. + *

+ * This constructor uses a {@link SoftHashMap} instance as the cache's backing map, which is thread-safe and + * auto-sizes itself based on the application's memory constraints. + *

+ * Finally, the {@link #setTimeToIdle(Duration) timeToIdle} and + * {@link #setTimeToLive(Duration) timeToLive} settings are both {@code null}, + * indicating that cache entries will live indefinitely (except due to memory constraints as managed by the + * {@code SoftHashMap}). + * + * @param name the name to assign to this instance, expected to be unique among all other caches in the parent + * {@code CacheManager}. + * @see SoftHashMap + * @see #setTimeToIdle(Duration) + * @see #setTimeToLive(Duration) + */ + public DefaultCache(String name) { + this(name, new SoftHashMap>()); + } + + /** + * Creates a new {@code DefaultCache} instance with the specified {@code name}, storing entries in the specified + * {@code backingMap}. It is expected that the {@code backingMap} implementation be thread-safe and preferrably + * auto-sizing based on memory constraints (see {@link SoftHashMap} for such an implementation). + *

+ * The {@link #setTimeToIdle(Duration) timeToIdle} and + * {@link #setTimeToLive(Duration) timeToLive} settings are both {@code null}, + * indicating that cache entries will live indefinitely (except due to memory constraints as managed by the + * {@code backingMap} instance). + * + * @param name name to assign to this instance, expected to be unique among all other caches in the parent + * {@code CacheManager}. + * @param backingMap the (ideally thread-safe) map instance to store the Cache entries. + * @see SoftHashMap + * @see #setTimeToIdle(Duration) + * @see #setTimeToLive(Duration) + */ + public DefaultCache(String name, Map> backingMap) { + this(name, backingMap, null, null); + } + + /** + * Creates a new {@code DefaultCache} instance with the specified {@code name}, storing entries in the specified + * {@code backingMap}, using the specified {@code timeToLive} and {@code timeToIdle} settings. + *

+ * It is expected that the {@code backingMap} implementation be thread-safe and preferrably + * auto-sizing based on memory constraints (see {@link SoftHashMap} for such an implementation). + * + * @param name name to assign to this instance, expected to be unique among all other caches in the parent + * {@code CacheManager}. + * @param backingMap the (ideally thread-safe) map instance to store the Cache entries. + * @param timeToIdle the amount of time cache entries may remain idle until they should be removed from the cache. + * @param timeToLive the amount of time cache entries may exist until they should be removed from the cache. + * @throws IllegalArgumentException if either {@code timeToLive} or {@code timeToIdle} are non-null and + * represent a non-positive (zero or negative) value. This is only enforced for + * non-null values - {@code null} values are allowed for either argument. + * @see #setTimeToIdle(Duration) + * @see #setTimeToLive(Duration) + */ + public DefaultCache(String name, Map> backingMap, Duration timeToLive, Duration timeToIdle) { + Assert.notNull(name, "Cache name cannot be null."); + Assert.notNull(backingMap, "Backing map cannot be null."); + assertTtl(timeToLive); + assertTti(timeToIdle); + this.name = name; + this.map = backingMap; + this.timeToLive = timeToLive; + this.timeToIdle = timeToIdle; + this.accessCount = new AtomicLong(0); + this.hitCount = new AtomicLong(0); + this.missCount = new AtomicLong(0); + } + + protected static void assertTtl(Duration ttl) { + if (ttl != null) { + Assert.isTrue(!ttl.isZero() && !ttl.isNegative(), "timeToLive duration must be greater than zero"); + } + } + + protected static void assertTti(Duration tti) { + if (tti != null) { + Assert.isTrue(!tti.isZero() && !tti.isNegative(), "timeToIdle duration must be greater than zero"); + } + } + + public V get(K key) { + + this.accessCount.incrementAndGet(); + + Entry entry = map.get(key); + + if (entry == null) { + missCount.incrementAndGet(); + return null; + } + + long nowMillis = System.currentTimeMillis(); + + Duration ttl = this.timeToLive; + Duration tti = this.timeToIdle; + + if (ttl != null) { + Duration sinceCreation = Duration.ofMillis(nowMillis - entry.getCreationTimeMillis()); + if (sinceCreation.compareTo(ttl) > 0) { + map.remove(key); + missCount.incrementAndGet(); //count an expired TTL as a miss + logger.trace("Removing {} from cache due to TTL, sinceCreation: {}", key, sinceCreation); + return null; + } + } + + if (tti != null) { + Duration sinceLastAccess = Duration.ofMillis(nowMillis - entry.getLastAccessTimeMillis()); + if (sinceLastAccess.compareTo(tti) > 0) { + map.remove(key); + missCount.incrementAndGet(); //count an expired TTI as a miss + logger.trace("Removing {} from cache due to TTI, sinceLastAccess: {}", key, sinceLastAccess); + return null; + } + } + + entry.lastAccessTimeMillis = nowMillis; + + hitCount.incrementAndGet(); + + return entry.getValue(); + } + + public V put(K key, V value) { + Entry newEntry = new Entry(value); + Entry previous = map.put(key, newEntry); + if (previous != null) { + return previous.value; + } + return null; + } + + @Override + public V remove(K key) { + accessCount.incrementAndGet(); + Entry previous = map.remove(key); + if (previous != null) { + hitCount.incrementAndGet(); + return previous.value; + } else { + missCount.incrementAndGet(); + return null; + } + } + + /** + * Returns the amount of time a cache entry may exist after first being created before it will expire and no + * longer be available. If a cache entry ever becomes older than this amount of time (regardless of how often + * it is accessed), it will be removed from the cache as soon as possible. + * + * @return the amount of time a cache entry may exist after first being created before it will expire and no + * longer be available. + */ + public Duration getTimeToLive() { + return timeToLive; + } + + /** + * Sets the amount of time a cache entry may exist after first being created before it will expire and no + * longer be available. If a cache entry ever becomes older than this amount of time (regardless of how often + * it is accessed), it will be removed from the cache as soon as possible. + * + * @param timeToLive the amount of time a cache entry may exist after first being created before it will expire and + * no longer be available. + */ + public void setTimeToLive(Duration timeToLive) { + assertTtl(timeToLive); + this.timeToLive = timeToLive; + } + + /** + * Returns the amount of time a cache entry may be idle - unused (not accessed) - before it will expire and + * no longer be available. If a cache entry is not accessed at all after this amount of time, it will be + * removed from the cache as soon as possible. + * + * @return the amount of time a cache entry may be idle - unused (not accessed) - before it will expire and + * no longer be available. + */ + public Duration getTimeToIdle() { + return timeToIdle; + } + + /** + * Sets the amount of time a cache entry may be idle - unused (not accessed) - before it will expire and + * no longer be available. If a cache entry is not accessed at all after this amount of time, it will be + * removed from the cache as soon as possible. + * + * @param timeToIdle the amount of time a cache entry may be idle - unused (not accessed) - before it will expire + * and no longer be available. + */ + public void setTimeToIdle(Duration timeToIdle) { + assertTti(timeToIdle); + this.timeToIdle = timeToIdle; + } + + /** + * Returns the number of attempts to return a cache entry. Note that because {@link #remove(Object)} will return + * a value, calls to both {@link #get(Object)} and {@link #remove(Object)} will increment this number. + * + * @return the number of attempts to return a cache entry + * @see #getHitCount() + * @see #getMissCount() + * @see #getHitRatio() + */ + public long getAccessCount() { + return this.accessCount.get(); + } + + /** + * Returns the total number of times an access attempt successfully returned a cache entry. + * + * @return the total number of times an access attempt successfully returned a cache entry. + * @see #getMissCount() + * @see #getHitRatio() + */ + public long getHitCount() { + return hitCount.get(); + } + + /** + * Returns the total number of times an access attempt did not return a cache entry. + * + * @return the total number of times an access attempt successfully returned a cache entry. + * @see #getHitCount() + * @see #getHitRatio() + */ + public long getMissCount() { + return missCount.get(); + } + + /** + * Returns the ratio of {@link #getHitCount() hitCount} to {@link #getAccessCount() accessCount}. The closer this + * number is to {@code 1.0}, the more effectively the cache is being used. The closer this number is to + * {code 0.0}, the less effectively the cache is being used. + * + * @return the ratio of {@link #getHitCount() hitCount} to {@link #getAccessCount() accessCount}. + */ + public double getHitRatio() { + double accessCount = (double) getAccessCount(); + if (accessCount > 0) { + double hitCount = (double) getHitCount(); + + return hitCount / accessCount; + } + return 0; + } + + /** + * Removes all entries from this cache. + */ + public void clear() { + map.clear(); + } + + /** + * Returns the total number of cache entries currently available in this cache. + * + * @return the total number of cache entries currently available in this cache. + */ + public int size() { + return map.size(); + } + + /** + * Returns this cache instance's name. + * + * @return this cache instance's name. + */ + public String getName() { + return this.name; + } + + public String toString() { + return new StringBuilder(" {\n \"name\": \"").append(name).append("\",\n") + .append(" \"size\": ").append(map.size()).append(",\n") + .append(" \"accessCount\": ").append(getAccessCount()).append(",\n") + .append(" \"hitCount\": ").append(getHitCount()).append(",\n") + .append(" \"missCount\": ").append(getMissCount()).append(",\n") + .append(" \"hitRatio\": ").append(getHitRatio()).append("\n") + .append(" }") + .toString(); + } + + /** + * An Entry is a wrapper that encapsulates the actual {@code value} stored in the cache as well as + * {@link #getCreationTimeMillis() creationTimeMillis} and {@link #getLastAccessTimeMillis() lastAccessTimeMillis} + * metadata about the entry itself. The {@code creationTimeMillis} and {@code lastAccessTimeMillis} values are used + * to support expunging cache entries based on + * {@link DefaultCache#getTimeToIdle() timeToIdle} and + * {@link DefaultCache#getTimeToLive() timeToLive} settings, respectively. + * + * @param the type of value that is stored in the cache. + */ + public static class Entry { + + private final V value; + private final long creationTimeMillis; + private volatile long lastAccessTimeMillis; + + /** + * Creates a new Entry instance wrapping the specified {@code value}, defaulting both the + * {@link #getCreationTimeMillis() creationTimeMillis} and the {@link #getLastAccessTimeMillis() lastAccessTimeMills} + * to the current timestamp (i.e. {@link System#currentTimeMillis()}). + * + * @param value the cache entry to store. + */ + public Entry(V value) { + this.value = value; + this.creationTimeMillis = System.currentTimeMillis(); + this.lastAccessTimeMillis = this.creationTimeMillis; + } + + /** + * Returns the actual value stored in the cache. + * + * @return the actual value stored in the cache. + */ + public V getValue() { + return value; + } + + /** + * Returns the creation time in millis since Epoch when this {@code Entry} instance was created. This is used to + * support expunging cache entries when this value is older than the cache's + * {@link DefaultCache#getTimeToLive() timeToLive} setting. + * + * @return the creation time in millis since Epoch when this {@code Entry} instance was created. + */ + public long getCreationTimeMillis() { + return creationTimeMillis; + } + + /** + * Returns the time in millis since Epoch when this {@code Entry} instance was last accessed. This is used to + * support expunging cache entries when this value is older than the cache's + * {@link DefaultCache#getTimeToIdle() timeToIdle} setting. + * + * @return the time in millis since Epoch when this {@code Entry} instance was last accessed. + */ + public long getLastAccessTimeMillis() { + return lastAccessTimeMillis; + } + } +} diff --git a/impl/src/main/java/com/okta/sdk/impl/cache/DefaultCacheConfiguration.java b/impl/src/main/java/com/okta/sdk/impl/cache/DefaultCacheConfiguration.java new file mode 100644 index 00000000000..4301d5433ff --- /dev/null +++ b/impl/src/main/java/com/okta/sdk/impl/cache/DefaultCacheConfiguration.java @@ -0,0 +1,71 @@ +/* + * Copyright 2014 Stormpath, Inc. + * Modifications Copyright 2018 Okta, Inc. + * + * 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 com.okta.sdk.impl.cache; + +import com.okta.commons.lang.Assert; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +/** + * @since 0.5.0 + */ +public class DefaultCacheConfiguration implements CacheConfiguration { + + private final String name; + private final Duration timeToLive; + private final Duration timeToIdle; + + public DefaultCacheConfiguration(String name, Duration timeToLive, Duration timeToIdle) { + Assert.hasText(name, "Cache Region name cannot be null or empty."); + this.name = name; + this.timeToLive = timeToLive; + this.timeToIdle = timeToIdle; + } + + static Duration toDuration(long value, TimeUnit tu) { + long timeInMillis = TimeUnit.MILLISECONDS.convert(value, tu); + if (timeInMillis > 0) { + return Duration.ofMillis(timeInMillis); + } + return null; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public Duration getTimeToLive() { + return this.timeToLive; + } + + @Override + public Duration getTimeToIdle() { + return this.timeToIdle; + } + + @Override + public String toString() { + return "DefaultCacheConfiguration{" + + "name='" + name + '\'' + + ", timeToLive=" + timeToLive + + ", timeToIdle=" + timeToIdle + + '}'; + } +} diff --git a/impl/src/main/java/com/okta/sdk/impl/cache/DefaultCacheConfigurationBuilder.java b/impl/src/main/java/com/okta/sdk/impl/cache/DefaultCacheConfigurationBuilder.java new file mode 100644 index 00000000000..b6a5353569b --- /dev/null +++ b/impl/src/main/java/com/okta/sdk/impl/cache/DefaultCacheConfigurationBuilder.java @@ -0,0 +1,66 @@ +/* + * Copyright 2014 Stormpath, Inc. + * Modifications Copyright 2018 Okta, Inc. + * + * 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 com.okta.sdk.impl.cache; + +import com.okta.commons.lang.Assert; +import com.okta.sdk.cache.CacheConfigurationBuilder; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +/** + * @since 0.5.0 + */ +public class DefaultCacheConfigurationBuilder implements CacheConfigurationBuilder { + + private final String name; + private Duration timeToLive; + private Duration timeToIdle; + + public DefaultCacheConfigurationBuilder(String name) { + Assert.hasText(name, "Cache Region name cannot be null or empty."); + this.name = name; + } + + @Override + public CacheConfigurationBuilder withTimeToLive(long ttl, TimeUnit ttlTimeUnit) { + this.timeToLive = DefaultCacheConfiguration.toDuration(ttl, ttlTimeUnit); + return this; + } + + @Override + public CacheConfigurationBuilder withTimeToIdle(long tti, TimeUnit ttiTimeUnit) { + this.timeToIdle = DefaultCacheConfiguration.toDuration(tti, ttiTimeUnit); + return this; + } + + public String getName() { + return name; + } + + public Duration getTimeToLive() { + return timeToLive; + } + + public Duration getTimeToIdle() { + return timeToIdle; + } + + public CacheConfiguration build() { + return new DefaultCacheConfiguration(getName(), getTimeToLive(), getTimeToIdle()); + } +} diff --git a/impl/src/main/java/com/okta/sdk/impl/cache/DefaultCacheManager.java b/impl/src/main/java/com/okta/sdk/impl/cache/DefaultCacheManager.java new file mode 100644 index 00000000000..fb6eff33996 --- /dev/null +++ b/impl/src/main/java/com/okta/sdk/impl/cache/DefaultCacheManager.java @@ -0,0 +1,271 @@ +/* + * Copyright 2014 Stormpath, Inc. + * Modifications Copyright 2018 Okta, Inc. + * + * 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 com.okta.sdk.impl.cache; + +import com.okta.commons.lang.Assert; +import com.okta.sdk.cache.Cache; +import com.okta.sdk.cache.CacheManager; +import com.okta.sdk.impl.util.SoftHashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.util.Collection; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; + +/** + * Very simple default {@code CacheManager} implementation that retains all created {@link Cache Cache} instances in + * an in-memory {@link ConcurrentMap ConcurrentMap}. By default, this implementation creates thread-safe + * {@link DefaultCache} instances via the {@link #createCache(String) createCache(name)} method, but this can be overridden + * by subclasses that wish to provide different Cache implementations. + *

Clustering

+ * This implementation DOES NOT SUPPORT CLUSTERING. + *

+ * If your application is deployed on multiple hosts, it is + * strongly recommended that you configure the Okta SDK with a clustered {@code CacheManager} + * implementation so all of your application instances can utilize the same cache policy and see the same + * security/identity data. Some example clusterable caching projects: Hazelcast, Ehcache+Terracotta, Coherence, + * GigaSpaces, etc. + *

+ * This implementation is production-quality, but only recommended for single-node/single-JVM applications. + *

Time To Idle

+ * Time to Idle is the amount of time a cache entry may be idle - unused (not accessed) - before it will expire and + * no longer be available. If a cache entry is not accessed at all after this amount of time, it will be + * removed from the cache as soon as possible. + *

+ * This implementation's {@link #setDefaultTimeToIdle(Duration) defaultTimeToIdle} + * is {@code null}, which means that cache entries can potentially remain idle indefinitely. Note however that a + * cache entry can still be expunged due to other conditions (e.g. memory constraints, Time to Live setting, etc). + *

+ * The {@link #setDefaultTimeToIdle(Duration) defaultTimeToIdle} setting is only + * applied to newly created {@code Cache} instances. It does not affect already existing {@code Cache}s. + *

Time to Live

+ * Time to Live is the amount of time a cache entry may exist after first being created before it will expire and no + * longer be available. If a cache entry ever becomes older than this amount of time (regardless of how often + * it is accessed), it will be removed from the cache as soon as possible. + *

+ * This implementation's {@link #setDefaultTimeToLive(Duration) defaultTimeToLive} + * is {@code null}, which means that cache entries could potentially live indefinitely. Note however that a + * cache entry can still be expunged due to other conditions (e.g. memory constraints, Time to Idle setting, etc). + *

+ * The {@link #setDefaultTimeToLive(Duration) defaultTimeToLive} setting is only + * applied to newly created {@code Cache} instances. It does not affect already existing {@code Cache}s. + *

Thread Safety

+ * This implementation and the cache instances it creates are thread-safe and usable in concurrent environments. + * + * @see #setDefaultTimeToIdle(Duration) + * @see #setDefaultTimeToIdleSeconds(long) + * @see #setDefaultTimeToLive(Duration) + * @see #setDefaultTimeToLiveSeconds(long) + * @since 0.5.0 + */ +public class DefaultCacheManager implements CacheManager { + + private final Logger logger = LoggerFactory.getLogger(DefaultCacheManager.class); + /** + * Retains any region-specific configuration that might be used when creating Cache instances. + */ + private final ConcurrentMap configs; + + /** + * Retains all Cache objects maintained by this cache manager. + */ + protected final ConcurrentMap caches; + + private volatile Duration defaultTimeToLive; + private volatile Duration defaultTimeToIdle; + + /** + * Default no-arg constructor that instantiates an internal name-to-cache {@code ConcurrentMap}. + */ + public DefaultCacheManager() { + this.configs = new ConcurrentHashMap<>(); + this.caches = new ConcurrentHashMap<>(); + } + + /** + * Returns the default {@link DefaultCache#getTimeToLive() timeToLive} duration + * to apply to newly created {@link DefaultCache} instances. This setting does not affect existing + * {@link DefaultCache} instances. + * + * @return the default {@link DefaultCache#getTimeToLive() timeToLive} duration + * to apply to newly created {@link DefaultCache} instances. + * @see DefaultCache + * @see DefaultCache#getTimeToLive() + */ + public Duration getDefaultTimeToLive() { + return defaultTimeToLive; + } + + /** + * Sets the default {@link DefaultCache#getTimeToLive() timeToLive} duration + * to apply to newly created {@link DefaultCache} instances. This setting does not affect existing + * {@link DefaultCache} instances. + * + * @param defaultTimeToLive the default {@link DefaultCache#getTimeToLive() timeToLive} + * duration to apply to newly created {@link DefaultCache} instances. + */ + public void setDefaultTimeToLive(Duration defaultTimeToLive) { + DefaultCache.assertTtl(defaultTimeToLive); + this.defaultTimeToLive = defaultTimeToLive; + } + + /** + * Convenience method that sets the {@link #setDefaultTimeToLive(Duration) defaultTimeToLive} + * value using a {@code TimeUnit} of {@link TimeUnit#SECONDS}. + * + * @param seconds the {@link #setDefaultTimeToLive(Duration) defaultTimeToLive} value in seconds. + */ + public void setDefaultTimeToLiveSeconds(long seconds) { + setDefaultTimeToLive(Duration.ofSeconds(seconds)); + } + + /** + * Returns the default {@link DefaultCache#getTimeToIdle() timeToIdle} duration + * to apply to newly created {@link DefaultCache} instances. This setting does not affect existing + * {@link DefaultCache} instances. + * + * @return the default {@link DefaultCache#getTimeToIdle() timeToIdle} duration + * to apply to newly created {@link DefaultCache} instances. + */ + public Duration getDefaultTimeToIdle() { + return defaultTimeToIdle; + } + + /** + * Sets the default {@link DefaultCache#getTimeToIdle() timeToIdle} duration + * to apply to newly created {@link DefaultCache} instances. This setting does not affect existing + * {@link DefaultCache} instances. + * + * @param defaultTimeToIdle the default {@link DefaultCache#getTimeToIdle() timeToIdle} + * duration to apply to newly created {@link DefaultCache} instances. + */ + public void setDefaultTimeToIdle(Duration defaultTimeToIdle) { + DefaultCache.assertTti(defaultTimeToIdle); + this.defaultTimeToIdle = defaultTimeToIdle; + } + + /** + * Convenience method that sets the {@link #setDefaultTimeToIdle(Duration) defaultTimeToIdle} + * value using a {@code TimeUnit} of {@link TimeUnit#SECONDS}. + * + * @param seconds the {@link #setDefaultTimeToIdle(Duration) defaultTimeToIdle} value in seconds. + */ + public void setDefaultTimeToIdleSeconds(long seconds) { + setDefaultTimeToIdle(Duration.ofSeconds(seconds)); + } + + /** + * Sets cache-specific configuration entries, to be utilized when creating cache instances. + * + * @param configs cache-specific configuration entries, to be utilized when creating cache instances. + */ + public void setCacheConfigurations(Collection configs) { + Assert.notNull("Argument cannot be null. To remove all configuration, set an empty collection."); + this.configs.clear(); + + for (CacheConfiguration config : configs) { + this.configs.put(config.getName(), config); + } + } + + /** + * Returns the cache with the specified {@code name}. If the cache instance does not yet exist, it will be lazily + * created, retained for further access, and then returned. + * + * @param name the name of the cache to acquire. + * @return the cache with the specified {@code name}. + * @throws IllegalArgumentException if the {@code name} argument is {@code null} or does not contain text. + */ + public Cache getCache(String name) throws IllegalArgumentException { + Assert.hasText(name, "Cache name cannot be null or empty."); + + Cache cache; + + cache = caches.get(name); + if (cache == null) { + logger.debug("Creating cache '{}'", name); + + cache = createCache(name); + Cache existing = caches.putIfAbsent(name, cache); + if (existing != null) { + cache = existing; + } + } + + //noinspection unchecked + return cache; + } + + /** + * Creates a new {@code Cache} instance associated with the specified {@code name}. + * + * @param name the name of the cache to create + * @return a new {@code Cache} instance associated with the specified {@code name}. + */ + @SuppressWarnings("unchecked") + protected Cache createCache(String name) { + Duration ttl = this.defaultTimeToLive != null ? Duration.from(this.defaultTimeToLive) : null; + Duration tti = this.defaultTimeToIdle != null ? Duration.from(this.defaultTimeToIdle) : null; + + CacheConfiguration config = this.configs.get(name); + if (config != null) { + Duration d = config.getTimeToLive(); + if (d != null) { + ttl = d; + } + d = config.getTimeToIdle(); + if (d != null) { + tti = d; + } + } + + return new DefaultCache(name, new SoftHashMap(), ttl, tti); + } + + public String toString() { + Collection values = caches.values(); + StringBuilder sb = new StringBuilder() + .append("{\n") + .append(" \"cacheCount\": ").append(caches.size()).append(",\n") + .append(" \"defaultTimeToLive\": \"").append(toString(defaultTimeToLive)).append("\",\n") + .append(" \"defaultTimeToIdle\": \"").append(toString(defaultTimeToIdle)).append("\",\n") + .append(" \"caches\": ["); + + if (!caches.isEmpty()) { + sb.append("\n"); + int i = 0; + for (Cache cache : values) { + if (i > 0) { + sb.append(",\n"); + } + sb.append(cache.toString()); + i++; + } + sb.append("\n "); + } + + sb.append("]\n}"); + return sb.toString(); + } + + private String toString(Duration d) { + return d != null ? d.toString() : "indefinite"; + } +} diff --git a/impl/src/main/java/com/okta/sdk/impl/cache/DefaultCacheManagerBuilder.java b/impl/src/main/java/com/okta/sdk/impl/cache/DefaultCacheManagerBuilder.java new file mode 100644 index 00000000000..92dd0e38383 --- /dev/null +++ b/impl/src/main/java/com/okta/sdk/impl/cache/DefaultCacheManagerBuilder.java @@ -0,0 +1,82 @@ +/* + * Copyright 2014 Stormpath, Inc. + * Modifications Copyright 2018 Okta, Inc. + * + * 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 com.okta.sdk.impl.cache; + +import com.okta.commons.lang.Assert; +import com.okta.commons.lang.Collections; +import com.okta.sdk.cache.CacheConfigurationBuilder; +import com.okta.sdk.cache.CacheManager; +import com.okta.sdk.cache.CacheManagerBuilder; + +import java.time.Duration; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * @since 0.5.0 + */ +public class DefaultCacheManagerBuilder implements CacheManagerBuilder { + + private Duration defaultTimeToLive; + private Duration defaultTimeToIdle; + + private final Set configs = new LinkedHashSet(); + + @Override + public CacheManagerBuilder withDefaultTimeToLive(long ttl, TimeUnit timeUnit) { + this.defaultTimeToLive = DefaultCacheConfiguration.toDuration(ttl, timeUnit); + return this; + } + + @Override + public CacheManagerBuilder withDefaultTimeToIdle(long tti, TimeUnit timeUnit) { + this.defaultTimeToIdle = DefaultCacheConfiguration.toDuration(tti, timeUnit); + return this; + } + + @Override + public CacheManagerBuilder withCache(CacheConfigurationBuilder builder) { + Assert.isInstanceOf(DefaultCacheConfigurationBuilder.class, builder, + "This implementation only accepts " + DefaultCacheConfigurationBuilder.class.getName() + " instances."); + + DefaultCacheConfigurationBuilder b = (DefaultCacheConfigurationBuilder) builder; + + this.configs.add(b.build()); + + return this; + } + + @Override + public CacheManager build() { + DefaultCacheManager manager = new DefaultCacheManager(); + + if (this.defaultTimeToLive != null) { + manager.setDefaultTimeToLive(this.defaultTimeToLive); + } + + if (this.defaultTimeToIdle != null) { + manager.setDefaultTimeToIdle(this.defaultTimeToIdle); + } + + if (!Collections.isEmpty(configs)) { + manager.setCacheConfigurations(configs); + } + + return manager; + } +} diff --git a/impl/src/main/java/com/okta/sdk/impl/cache/DisabledCache.java b/impl/src/main/java/com/okta/sdk/impl/cache/DisabledCache.java new file mode 100644 index 00000000000..b4013daa16f --- /dev/null +++ b/impl/src/main/java/com/okta/sdk/impl/cache/DisabledCache.java @@ -0,0 +1,61 @@ +/* + * Copyright 2014 Stormpath, Inc. + * Modifications Copyright 2018 Okta, Inc. + * + * 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 com.okta.sdk.impl.cache; + +import com.okta.sdk.cache.Cache; + +/** + * A disabled implementation that does nothing. This is useful for a CacheManager implementation to return instead + * of retuning null. Non-null guarantees reduce a program's cyclomatic complexity. + * + * @since 0.5.0 + */ +public class DisabledCache implements Cache { + + /** + * This implementation does not do anything and always returns null. + * + * @return null always. + */ + @Override + public V get(K key) { + return null; + } + + /** + * This implementation does not do anything (no caching) and always returns null. + * + * @param key the key used to identify the object being stored. + * @param value the value to be stored in the cache. + * @return null always. + */ + @Override + public V put(K key, V value) { + return null; + } + + /** + * This implementation does not do anything (no caching) and always returns null. + * + * @param key the key used to identify the object being stored. + * @return null always. + */ + @Override + public V remove(K key) { + return null; + } +} diff --git a/impl/src/main/java/com/okta/sdk/impl/cache/DisabledCacheManager.java b/impl/src/main/java/com/okta/sdk/impl/cache/DisabledCacheManager.java new file mode 100644 index 00000000000..ad67d285106 --- /dev/null +++ b/impl/src/main/java/com/okta/sdk/impl/cache/DisabledCacheManager.java @@ -0,0 +1,42 @@ +/* + * Copyright 2014 Stormpath, Inc. + * Modifications Copyright 2018 Okta, Inc. + * + * 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 com.okta.sdk.impl.cache; + +import com.okta.sdk.cache.Cache; +import com.okta.sdk.cache.CacheManager; + +/** + * A disabled implementation that does nothing. This alleviates a CacheManager user (component) from ever needing to + * check for null. Non-null guarantees reduce a program's cyclomatic complexity and simplify testing. + * + * @since 0.5.0 + */ +public class DisabledCacheManager implements CacheManager { + + private static final Cache CACHE_INSTANCE = new DisabledCache(); + + /** + * Always returns a {@link DisabledCache} instance to ensure non-null guarantees. + * + * @return returns a {@link DisabledCache} instance to ensure non-null guarantees. + */ + @SuppressWarnings("unchecked") + @Override + public Cache getCache(String name) { + return CACHE_INSTANCE; + } +} diff --git a/impl/src/main/java/com/okta/sdk/impl/client/DefaultClientBuilder.java b/impl/src/main/java/com/okta/sdk/impl/client/DefaultClientBuilder.java index f8f86d3a17c..4a7b0456203 100644 --- a/impl/src/main/java/com/okta/sdk/impl/client/DefaultClientBuilder.java +++ b/impl/src/main/java/com/okta/sdk/impl/client/DefaultClientBuilder.java @@ -24,8 +24,13 @@ import com.okta.commons.configcheck.ConfigurationValidator; import com.okta.commons.http.config.Proxy; import com.okta.commons.lang.Assert; +import com.okta.commons.lang.Classes; import com.okta.commons.lang.Strings; import com.okta.sdk.authc.credentials.ClientCredentials; +import com.okta.sdk.cache.CacheConfigurationBuilder; +import com.okta.sdk.cache.CacheManager; +import com.okta.sdk.cache.CacheManagerBuilder; +import com.okta.sdk.cache.Caches; import com.okta.sdk.client.AuthenticationScheme; import com.okta.sdk.client.AuthorizationMode; import com.okta.sdk.client.ClientBuilder; @@ -62,6 +67,8 @@ import org.openapitools.client.ApiClient; import org.openapitools.client.model.UserProfile; import org.openapitools.jackson.nullable.JsonNullableModule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.converter.HttpMessageConverter; @@ -91,6 +98,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; /** *

The default {@link ClientBuilder} implementation. This looks for configuration files @@ -113,12 +121,15 @@ */ public class DefaultClientBuilder implements ClientBuilder { + private static final Logger log = LoggerFactory.getLogger(DefaultClientBuilder.class); + private static final String ENVVARS_TOKEN = "envvars"; private static final String SYSPROPS_TOKEN = "sysprops"; private static final String OKTA_CONFIG_CP = "com/okta/sdk/config/"; private static final String OKTA_YAML = "okta.yaml"; private static final String OKTA_PROPERTIES = "okta.properties"; + private CacheManager cacheManager; private ClientCredentials clientCredentials; private boolean allowNonHttpsForTesting = false; @@ -169,6 +180,38 @@ else if (SYSPROPS_TOKEN.equalsIgnoreCase(location)) { clientConfig.setApiToken(props.get(DEFAULT_CLIENT_API_TOKEN_PROPERTY_NAME)); } + if (Strings.hasText(props.get(DEFAULT_CLIENT_CACHE_ENABLED_PROPERTY_NAME))) { + clientConfig.setCacheManagerEnabled(Boolean.parseBoolean(props.get(DEFAULT_CLIENT_CACHE_ENABLED_PROPERTY_NAME))); + } + + if (Strings.hasText(props.get(DEFAULT_CLIENT_CACHE_TTL_PROPERTY_NAME))) { + clientConfig.setCacheManagerTtl(Long.parseLong(props.get(DEFAULT_CLIENT_CACHE_TTL_PROPERTY_NAME))); + } + + if (Strings.hasText(props.get(DEFAULT_CLIENT_CACHE_TTI_PROPERTY_NAME))) { + clientConfig.setCacheManagerTti(Long.parseLong(props.get(DEFAULT_CLIENT_CACHE_TTI_PROPERTY_NAME))); + } + + for (String prop : props.keySet()) { + boolean isPrefix = prop.length() == DEFAULT_CLIENT_CACHE_CACHES_PROPERTY_NAME.length(); + if (!isPrefix && prop.startsWith(DEFAULT_CLIENT_CACHE_CACHES_PROPERTY_NAME)) { + // get class from prop name + String cacheClass = prop.substring(DEFAULT_CLIENT_CACHE_CACHES_PROPERTY_NAME.length() + 1, prop.length() - 4); + String cacheTti = props.get(DEFAULT_CLIENT_CACHE_CACHES_PROPERTY_NAME + "." + cacheClass + ".tti"); + String cacheTtl = props.get(DEFAULT_CLIENT_CACHE_CACHES_PROPERTY_NAME + "." + cacheClass + ".ttl"); + CacheConfigurationBuilder cacheBuilder = Caches.forResource(Classes.forName(cacheClass)); + if (Strings.hasText(cacheTti)) { + cacheBuilder.withTimeToIdle(Long.parseLong(cacheTti), TimeUnit.SECONDS); + } + if (Strings.hasText(cacheTtl)) { + cacheBuilder.withTimeToLive(Long.parseLong(cacheTtl), TimeUnit.SECONDS); + } + if (!clientConfig.getCacheManagerCaches().containsKey(cacheClass)) { + clientConfig.getCacheManagerCaches().put(cacheClass, cacheBuilder); + } + } + } + if (Strings.hasText(props.get(DEFAULT_CLIENT_TESTING_DISABLE_HTTPS_CHECK_PROPERTY_NAME))) { allowNonHttpsForTesting = Boolean.parseBoolean(props.get(DEFAULT_CLIENT_TESTING_DISABLE_HTTPS_CHECK_PROPERTY_NAME)); } @@ -247,6 +290,12 @@ public ClientBuilder setProxy(Proxy proxy) { return this; } + @Override + public ClientBuilder setCacheManager(CacheManager cacheManager) { + this.cacheManager = cacheManager; + return this; + } + @Override public ClientBuilder setConnectionTimeout(int timeout) { Assert.isTrue(timeout >= 0, "Timeout cannot be a negative number."); @@ -276,12 +325,30 @@ public ClientBuilder setRetryMaxAttempts(int maxAttempts) { @Override public ApiClient build() { + if (!this.clientConfig.isCacheManagerEnabled()) { + log.debug("CacheManager disabled. Defaulting to DisabledCacheManager"); + this.cacheManager = Caches.newDisabledCacheManager(); + } else if (this.cacheManager == null) { + log.debug("No CacheManager configured. Defaulting to in-memory CacheManager with default TTL and TTI of five minutes."); + + CacheManagerBuilder cacheManagerBuilder = Caches.newCacheManager() + .withDefaultTimeToIdle(this.clientConfig.getCacheManagerTti(), TimeUnit.SECONDS) + .withDefaultTimeToLive(this.clientConfig.getCacheManagerTtl(), TimeUnit.SECONDS); + if (this.clientConfig.getCacheManagerCaches().size() > 0) { + for (CacheConfigurationBuilder builder : this.clientConfig.getCacheManagerCaches().values()) { + cacheManagerBuilder.withCache(builder); + } + } + + this.cacheManager = cacheManagerBuilder.build(); + } + if (this.clientConfig.getBaseUrlResolver() == null) { ConfigurationValidator.validateOrgUrl(this.clientConfig.getBaseUrl(), allowNonHttpsForTesting); this.clientConfig.setBaseUrlResolver(new DefaultBaseUrlResolver(this.clientConfig.getBaseUrl())); } - ApiClient apiClient = new ApiClient(restTemplate(this.clientConfig)); + ApiClient apiClient = new ApiClient(restTemplate(this.clientConfig), this.cacheManager); if (!isOAuth2Flow()) { if (this.clientConfig.getClientCredentialsResolver() == null && this.clientCredentials != null) { @@ -337,24 +404,25 @@ private RestTemplate restTemplate(ClientConfiguration clientConfig) { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.registerModule(new JsonNullableModule()); + + SimpleModule module = new SimpleModule(); + module.addSerializer(UserProfile.class, new UserProfileSerializer()); + module.addDeserializer(UserProfile.class, new UserProfileDeserializer()); + objectMapper.registerModule(module); - MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter(objectMapper); - ObjectMapper mapper = messageConverter.getObjectMapper(); - messageConverter.setSupportedMediaTypes(Arrays.asList( + MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = + new MappingJackson2HttpMessageConverter(objectMapper); + + mappingJackson2HttpMessageConverter.setSupportedMediaTypes(Arrays.asList( MediaType.APPLICATION_JSON, MediaType.parseMediaType("application/x-pem-file"), MediaType.parseMediaType("application/x-x509-ca-cert"), MediaType.parseMediaType("application/pkix-cert"))); - mapper.registerModule(new JavaTimeModule()); - mapper.registerModule(new JsonNullableModule()); - - SimpleModule module = new SimpleModule(); - module.addSerializer(UserProfile.class, new UserProfileSerializer()); - module.addDeserializer(UserProfile.class, new UserProfileDeserializer()); - mapper.registerModule(module); List> messageConverters = new ArrayList<>(); - messageConverters.add(messageConverter); + messageConverters.add(mappingJackson2HttpMessageConverter); RestTemplate restTemplate = new RestTemplate(messageConverters); restTemplate.setErrorHandler(new ErrorHandler()); @@ -364,6 +432,7 @@ private RestTemplate restTemplate(ClientConfiguration clientConfig) { } private HttpComponentsClientHttpRequestFactory requestFactory(ClientConfiguration clientConfig) { + final HttpClientBuilder clientBuilder = HttpClientBuilder.create(); if (clientConfig.getProxy() != null) { diff --git a/impl/src/main/java/com/okta/sdk/impl/config/ClientConfiguration.java b/impl/src/main/java/com/okta/sdk/impl/config/ClientConfiguration.java index 214e6c48483..3a3fb663ef2 100644 --- a/impl/src/main/java/com/okta/sdk/impl/config/ClientConfiguration.java +++ b/impl/src/main/java/com/okta/sdk/impl/config/ClientConfiguration.java @@ -19,11 +19,14 @@ import com.okta.commons.http.config.BaseUrlResolver; import com.okta.commons.http.config.HttpClientConfiguration; import com.okta.commons.lang.Strings; +import com.okta.sdk.cache.CacheConfigurationBuilder; import com.okta.sdk.client.AuthenticationScheme; import com.okta.sdk.client.AuthorizationMode; import com.okta.sdk.impl.api.ClientCredentialsResolver; import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.Set; /** @@ -39,6 +42,10 @@ public class ClientConfiguration extends HttpClientConfiguration { private String apiToken; private ClientCredentialsResolver clientCredentialsResolver; + private boolean cacheManagerEnabled; + private long cacheManagerTtl; + private long cacheManagerTti; + private Map cacheManagerCaches = new LinkedHashMap<>(); private AuthenticationScheme authenticationScheme; private BaseUrlResolver baseUrlResolver; private AuthorizationMode authorizationMode; @@ -111,6 +118,22 @@ public void setPrivateKey(String privateKey) { this.privateKey = privateKey; } + public boolean isCacheManagerEnabled() { + return cacheManagerEnabled; + } + + public void setCacheManagerEnabled(boolean cacheManagerEnabled) { + this.cacheManagerEnabled = cacheManagerEnabled; + } + + public Map getCacheManagerCaches() { + return cacheManagerCaches; + } + + public void setCacheManagerCaches(Map cacheManagerCaches) { + this.cacheManagerCaches = cacheManagerCaches; + } + public String getKid() { return kid; } @@ -119,6 +142,38 @@ public void setKid(String kid) { this.kid = kid; } + /** + * Time to idle for cache manager in seconds + * @return seconds until time to idle expires + */ + public long getCacheManagerTti() { + return cacheManagerTti; + } + + /** + * The cache manager's time to idle in seconds + * @param cacheManagerTti the time to idle in seconds + */ + public void setCacheManagerTti(long cacheManagerTti) { + this.cacheManagerTti = cacheManagerTti; + } + + /** + * Time to live for cache manager in seconds + * @return seconds until time to live expires + */ + public long getCacheManagerTtl() { + return cacheManagerTtl; + } + + /** + * The cache manager's time to live in seconds + * @param cacheManagerTtl the time to live in seconds + */ + public void setCacheManagerTtl(long cacheManagerTtl) { + this.cacheManagerTtl = cacheManagerTtl; + } + @Override public String getBaseUrl() { String baseUrl = super.getBaseUrl(); @@ -132,11 +187,14 @@ public String getBaseUrl() { @Override public String toString() { - return "ClientConfiguration { baseUrl='" + getBaseUrl() + '\'' + + return "ClientConfiguration {cacheManagerTtl=" + cacheManagerTtl + + ", cacheManagerTti=" + cacheManagerTti + + ", cacheManagerCaches=" + cacheManagerCaches + + ", baseUrl='" + getBaseUrl() + '\'' + ", authorizationMode=" + getAuthorizationMode() + ", clientId=" + getClientId() + ", scopes=" + getScopes() + - ", privateKey=" + ((getPrivateKey() != null) ? "*****" : null) + + ", privateKey=" + ((getPrivateKey() != null) ? "xxxxx" : null) + ", connectionTimeout=" + getConnectionTimeout() + ", requestAuthenticator=" + getRequestAuthenticator() + ", retryMaxElapsed=" + getRetryMaxElapsed() + diff --git a/impl/src/main/java/com/okta/sdk/impl/config/DefaultEnvVarNameConverter.java b/impl/src/main/java/com/okta/sdk/impl/config/DefaultEnvVarNameConverter.java index 79aa4c7ee57..a999c424d3e 100644 --- a/impl/src/main/java/com/okta/sdk/impl/config/DefaultEnvVarNameConverter.java +++ b/impl/src/main/java/com/okta/sdk/impl/config/DefaultEnvVarNameConverter.java @@ -32,6 +32,8 @@ public DefaultEnvVarNameConverter() { // this dependency on ClientBuilder isn't great, in the future we can change the API to ONLY support a one way conversion this.envToDotPropMap = buildReverseLookupToMap( + ClientBuilder.DEFAULT_CLIENT_CACHE_TTL_PROPERTY_NAME, + ClientBuilder.DEFAULT_CLIENT_CACHE_TTI_PROPERTY_NAME, ClientBuilder.DEFAULT_CLIENT_ORG_URL_PROPERTY_NAME, ClientBuilder.DEFAULT_CLIENT_CONNECTION_TIMEOUT_PROPERTY_NAME, ClientBuilder.DEFAULT_CLIENT_AUTHENTICATION_SCHEME_PROPERTY_NAME, diff --git a/impl/src/test/groovy/com/okta/sdk/impl/cache/CachesTest.groovy b/impl/src/test/groovy/com/okta/sdk/impl/cache/CachesTest.groovy new file mode 100644 index 00000000000..f5fbc8eee49 --- /dev/null +++ b/impl/src/test/groovy/com/okta/sdk/impl/cache/CachesTest.groovy @@ -0,0 +1,76 @@ +/* + * Copyright 2014 Stormpath, Inc. + * Modifications Copyright 2018 Okta, Inc. + * + * 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 com.okta.sdk.impl.cache + +import com.okta.sdk.cache.CacheManager +import org.testng.annotations.Test + +import java.time.Duration +import java.util.concurrent.TimeUnit + +import static com.okta.sdk.cache.Caches.* +import static org.testng.Assert.* + +/** + * + * @since 0.5.0 + */ +class CachesTest { + + @Test + void testBuild() { + + Duration defaultTtl = Duration.ofMinutes(30) + Duration defaultTti = Duration.ofHours(5) + + CacheManager m = newCacheManager() + .withDefaultTimeToLive(defaultTtl.getSeconds(), TimeUnit.SECONDS) + .withDefaultTimeToIdle(defaultTti.getSeconds(), TimeUnit.SECONDS) + .withCache(named('foo').withTimeToLive(20, TimeUnit.MINUTES).withTimeToIdle(15, TimeUnit.MINUTES)) + .withCache(named('bar').withTimeToLive(-1, TimeUnit.HOURS).withTimeToIdle(0, TimeUnit.HOURS)) + .build() + + assertNotNull m + assertTrue m instanceof DefaultCacheManager + DefaultCacheManager manager = (DefaultCacheManager)m + + assertEquals manager.defaultTimeToLive, defaultTtl + assertEquals manager.defaultTimeToIdle, defaultTti + + def c = manager.getCache('foo') + assertNotNull c + assertTrue c instanceof DefaultCache + DefaultCache cache = (DefaultCache)c + + assertEquals cache.timeToLive, Duration.ofMinutes(20) + assertEquals cache.timeToIdle, Duration.ofMinutes(15) + + cache = (DefaultCache)manager.getCache('bar') + + assertEquals cache.timeToLive, defaultTtl + assertEquals cache.timeToIdle, defaultTti + } + + @Test + void testNewDisabledCacheManager() { + + CacheManager cm = newDisabledCacheManager() + + assertNotNull cm + assertTrue cm instanceof DisabledCacheManager + } +} diff --git a/impl/src/test/groovy/com/okta/sdk/impl/cache/DefaultCacheManagerTest.groovy b/impl/src/test/groovy/com/okta/sdk/impl/cache/DefaultCacheManagerTest.groovy new file mode 100644 index 00000000000..43a25a71f63 --- /dev/null +++ b/impl/src/test/groovy/com/okta/sdk/impl/cache/DefaultCacheManagerTest.groovy @@ -0,0 +1,136 @@ +/* + * Copyright 2014 Stormpath, Inc. + * Modifications Copyright 2018 Okta, Inc. + * + * 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 com.okta.sdk.impl.cache + +import com.okta.sdk.cache.Cache +import groovy.json.JsonSlurper +import org.testng.annotations.BeforeTest +import org.testng.annotations.Test + +import java.time.Duration + +import static org.testng.Assert.* + +/** + * @since 0.5.0 + */ +class DefaultCacheManagerTest { + + private DefaultCacheManager mgr; + + @BeforeTest + void setUp() { + this.mgr = new DefaultCacheManager(); + } + + @Test + void testGetCache() { + def cache = mgr.getCache('foo'); + assertNotNull cache + assertEquals 'foo', cache.name + assertSame cache, mgr.getCache('foo') + } + + @Test + void testGetCachePutIfAbsent() { + mgr = new DefaultCacheManager() { + @Override + protected Cache createCache(String name) { + Cache first = super.createCache(name) + + //simulate something else putting a cache with the same name in + //we should see this cache and not the first one created: + Cache second = super.createCache(name); + second.put('key', 'value'); + caches.put(name, second); + + return first; + } + } + + def cache = mgr.getCache('foo') + assertNotNull cache + assertEquals 'foo', cache.name + assertEquals 'value', cache.get('key') + } + + @Test + void testDefaultTtl() { + mgr = new DefaultCacheManager() + Duration ttl = Duration.ofSeconds(30) + mgr.setDefaultTimeToLive(ttl) + + def cache = mgr.getCache('foo') + assertEquals cache.timeToLive, Duration.ofSeconds(30) + } + + @Test + void testDefaultTtlSeconds() { + mgr.setDefaultTimeToLiveSeconds(30) + assertEquals mgr.defaultTimeToLive, Duration.ofSeconds(30) + } + + @Test + void testDefaultTti() { + Duration tti = Duration.ofSeconds(20) + mgr.setDefaultTimeToIdle(tti) + + def cache = mgr.getCache('foo') + assertEquals cache.timeToIdle.seconds, tti.getSeconds() + } + + @Test + void testDefaultTtiSeconds() { + mgr.setDefaultTimeToIdleSeconds(30) + assertEquals Duration.ofSeconds(30), mgr.defaultTimeToIdle + } + + @Test + void testToString() { + + mgr = new DefaultCacheManager() + + mgr.getCache('foo') + mgr.getCache('bar') + + def string = mgr.toString() + def json = new JsonSlurper().parseText(string) + + assertEquals json.cacheCount, 2 + assertEquals json.caches.size(), 2 + assertEquals json.defaultTimeToLive, 'indefinite' + assertEquals json.defaultTimeToIdle, 'indefinite' + + def names = ['foo', 'bar'] + + def caches = [:] + for( def cache : json.caches) { + caches.put(cache.name, cache) + } + + for(def name : names) { + def cache = caches.get(name) + assertEquals cache.name, name + assertEquals cache.size, 0 + assertEquals cache.accessCount, 0 + assertEquals cache.hitCount, 0 + assertEquals cache.missCount, 0 + assertEquals cache.hitRatio, 0.0 + } + } + +} diff --git a/impl/src/test/groovy/com/okta/sdk/impl/cache/DefaultCacheTest.groovy b/impl/src/test/groovy/com/okta/sdk/impl/cache/DefaultCacheTest.groovy new file mode 100644 index 00000000000..30a12e2f6f9 --- /dev/null +++ b/impl/src/test/groovy/com/okta/sdk/impl/cache/DefaultCacheTest.groovy @@ -0,0 +1,266 @@ +/* + * Copyright 2014 Stormpath, Inc. + * Modifications Copyright 2018 Okta, Inc. + * + * 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 com.okta.sdk.impl.cache + +import groovy.json.JsonSlurper +import org.testng.annotations.Test + +import java.time.Duration + +import static org.testng.Assert.* + +/** + * @since 0.5.0 + */ +class DefaultCacheTest { + + @Test + void testDefault() { + def cache = new DefaultCache('foo') + assertEquals 'foo', cache.name + } + + @Test + void testGetReturningNull() { + def cache = new DefaultCache('foo'); + assertNull cache.get('key') + } + + @Test + void testSetTtl() { + def cache = new DefaultCache('foo') + def ttl = Duration.ofMillis(30) + cache.setTimeToLive(ttl) + assertSame cache.timeToLive, ttl + } + + @Test(expectedExceptions = IllegalArgumentException) + void testSetNegativeTtl() { + def cache = new DefaultCache('foo') + def ttl = Duration.ofMillis(-30) + cache.setTimeToLive(ttl) + } + + @Test(expectedExceptions = IllegalArgumentException) + void testSetZeroTtl() { + def cache = new DefaultCache('foo') + def ttl = Duration.ofMillis(0) + cache.setTimeToLive(ttl) + } + + @Test + void testSetTti() { + def cache = new DefaultCache('foo') + def tti = Duration.ofMillis(30) + cache.setTimeToIdle(tti) + assertSame cache.timeToIdle, tti + } + + @Test(expectedExceptions = IllegalArgumentException) + void testSetNegativeTti() { + def cache = new DefaultCache('foo') + def tti = Duration.ofMillis(-30) + cache.setTimeToIdle(tti) + } + + @Test(expectedExceptions = IllegalArgumentException) + void testSetZeroTti() { + def cache = new DefaultCache('foo') + def tti = Duration.ofMillis(0) + cache.setTimeToIdle(tti) + } + + @Test + void testRemove() { + def cache = new DefaultCache('foo') + def existing = cache.put('key', 'value') + assertNull existing + + existing = cache.remove('key') + assertEquals existing, 'value' + + assertEquals cache.accessCount, 1 + assertEquals cache.hitCount, 1 + assertEquals cache.missCount, 0 + assertEquals cache.hitRatio, 1.0d + + def value = cache.get('key') + assertNull value + + assertEquals cache.accessCount, 2 + assertEquals cache.hitCount, 1 + assertEquals cache.missCount, 1 + def ratio = cache.hitRatio + assertEquals ratio, 0.5d + + value = cache.remove('key') + assertNull value + + assertEquals cache.accessCount, 3 + assertEquals cache.hitCount, 1 + assertEquals cache.missCount, 2 + ratio = cache.hitRatio + assertEquals ratio, (1d/3d) + } + + + @Test + void testClear() { + def cache = new DefaultCache('foo') + cache.put('key', 'value') + assertEquals cache.size(), 1 + cache.clear() + assertEquals cache.size(), 0 + } + + @Test + void testToString() { + def cache = new DefaultCache('foo') + def json = new JsonSlurper().parseText(cache.toString()) + + assertEquals json.name, 'foo' + assertEquals json.size, 0 + assertEquals json.accessCount, 0 + assertEquals json.hitCount, 0 + assertEquals json.missCount, 0 + assertEquals json.hitRatio, 0.0 + + cache.put('key', 'value') + def value = cache.get('key') + assertEquals value, 'value' + + json = new JsonSlurper().parseText(cache.toString()) + + assertEquals json.name, 'foo' + assertEquals json.size, 1 + assertEquals json.accessCount, 1 + assertEquals json.hitCount, 1 + assertEquals json.missCount, 0 + assertEquals json.hitRatio, 1.0 + } + + @Test + void testPutReplacesPreviousValue() { + def cache = new DefaultCache('foo', [:], null, null); + + def key = 'key' + def value1 = 'value1' + def value2 = 'value2' + + assertNull cache.put(key, value1) + + def prev = cache.put(key, value2) + + assertEquals prev, value1 + assertEquals cache.get(key), value2 + } + + @Test(enabled = false) // TODO: flaky test, need to fix + void testTimeToLive() { + + def cache = new DefaultCache('foo', [:], Duration.ofMillis(10), null) + + def key = 'key' + def value = 'value' + + def prev = cache.put(key, value) + assertNull prev + + def found = cache.get(key) + assertEquals value, found + assertEquals cache.size(), 1 + + Thread.sleep(15) + + found = cache.get(key) + + assertNull found + assertEquals cache.size(), 0 + } + + @Test(enabled = false) // TODO: flaky test, need to fix + void testTimeToIdle() { + + def cache = new DefaultCache('foo', [:], null, Duration.ofMillis(50)) + + def key = 'key' + def value = 'value' + + def prev = cache.put(key, value) + assertNull prev + + def found = cache.get(key) + assertEquals found, value + assertEquals cache.size(), 1 + + Thread.sleep(5) + + found = cache.get(key) + assertEquals found, value + assertEquals cache.size(), 1 + + Thread.sleep(300) + + found = cache.get(key) + assertNull found + assertEquals cache.size(), 0 + } + + @Test(enabled = false) // TODO: flaky test, need to fix + void testTimeToLiveAndTimeToIdle() { + + def cache = new DefaultCache('foo', [:], Duration.ofMillis(100), Duration.ofMillis(40)) + + def key = 'key' + def value = 'value' + + def prev = cache.put(key, value) + assertNull prev + + def found = cache.get(key) + assertEquals found, value + assertEquals 1, cache.size() + + //each time we access after sleeping 15 seconds, we should always acquire the value since the last + //access timestamp is being updated, preventing expunging due to idle. + + Thread.sleep(20) + found = cache.get(key) + assertEquals(found, value) + assertEquals 1, cache.size() + + Thread.sleep(20) + found = cache.get(key) + assertEquals(found, value) + assertEquals 1, cache.size() + + Thread.sleep(20) + found = cache.get(key) + assertEquals(found, value) + assertEquals 1, cache.size() + + //Now we need to ensure that no matter how frequently the value is used (not idle), we still need to remove + //the value if older than the TTL + + //20 + 20 + 20 = 60. Add another 50 millis, and we'll be ~ 110 millis, which is older than the TTL of 100 above. + Thread.sleep(50) + + found = cache.get(key) + assertNull found + assertEquals 0, cache.size() + } +} diff --git a/impl/src/test/groovy/com/okta/sdk/impl/cache/DisabledCacheManagerTest.groovy b/impl/src/test/groovy/com/okta/sdk/impl/cache/DisabledCacheManagerTest.groovy new file mode 100644 index 00000000000..2c956252a8b --- /dev/null +++ b/impl/src/test/groovy/com/okta/sdk/impl/cache/DisabledCacheManagerTest.groovy @@ -0,0 +1,42 @@ +/* + * Copyright 2014 Stormpath, Inc. + * Modifications Copyright 2018 Okta, Inc. + * + * 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 com.okta.sdk.impl.cache + +import org.testng.annotations.Test + +import static org.testng.Assert.assertSame +import static org.testng.Assert.assertTrue + +/** + * + * @since 0.5.0 + */ +class DisabledCacheManagerTest { + + @Test + void testDefault() { + + DisabledCacheManager cacheManager = new DisabledCacheManager(); + + def foo = cacheManager.getCache('foo') + assertTrue foo instanceof DisabledCache + assertSame foo, DisabledCacheManager.CACHE_INSTANCE + + def bar = cacheManager.getCache('bar') + assertSame bar, DisabledCacheManager.CACHE_INSTANCE + } +} diff --git a/impl/src/test/groovy/com/okta/sdk/impl/cache/DisabledCacheTest.groovy b/impl/src/test/groovy/com/okta/sdk/impl/cache/DisabledCacheTest.groovy new file mode 100644 index 00000000000..ab11d05a647 --- /dev/null +++ b/impl/src/test/groovy/com/okta/sdk/impl/cache/DisabledCacheTest.groovy @@ -0,0 +1,42 @@ +/* + * Copyright 2014 Stormpath, Inc. + * Modifications Copyright 2018 Okta, Inc. + * + * 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 com.okta.sdk.impl.cache + +import org.testng.annotations.Test + +import static org.testng.Assert.assertNull + +/** + * @since 0.5.0 + */ +class DisabledCacheTest { + + @Test + void testDefault() { + + DisabledCache cache = new DisabledCache(); + + def returned = cache.put('foo', 'bar') + assertNull returned + + returned = cache.get('foo') + assertNull returned + + cache.put('foo', 'bar') + assertNull cache.remove('foo') + } +} diff --git a/pom.xml b/pom.xml index 98883f0d4ea..f6e8f7ee838 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ com.okta okta-parent - 23 + 24 com.okta.sdk @@ -34,12 +34,12 @@ 2017 - 2.13.3 - 1.30 + 2.14.0-rc1 + 1.33 1.70 0.11.5 - 8.2.1 - 1.3.0 + 10.0.0-beta + 1.3.2 okta/okta-sdk-java @@ -292,8 +292,8 @@ true - false - true + false + false ${root.dir}/src/japicmp/postAnalysisScript.groovy