diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java
index 18dae2a..e3e82ce 100644
--- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java
@@ -1,9 +1,5 @@
package org.keycloak.authorization.policy.provider.js;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-
import org.keycloak.Config;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
@@ -24,7 +20,7 @@ import org.keycloak.scripting.ScriptingProvider;
public class JSPolicyProviderFactory implements PolicyProviderFactory<JSPolicyRepresentation> {
private final JSPolicyProvider provider = new JSPolicyProvider(this::getEvaluatableScript);
- private final Map<String, EvaluatableScriptAdapter> scripts = Collections.synchronizedMap(new HashMap<>());
+ private ScriptCache scriptCache;
@Override
public String getName() {
@@ -74,12 +70,14 @@ public class JSPolicyProviderFactory implements PolicyProviderFactory<JSPolicyRe
@Override
public void onRemove(final Policy policy, final AuthorizationProvider authorization) {
- scripts.remove(policy.getId());
+ scriptCache.remove(policy.getId());
}
@Override
public void init(Config.Scope config) {
-
+ int maxEntries = Integer.parseInt(config.get("cache-max-entries", "100"));
+ int maxAge = Integer.parseInt(config.get("cache-entry-max-age", "-1"));
+ scriptCache = new ScriptCache(maxEntries, maxAge);
}
@Override
@@ -98,7 +96,7 @@ public class JSPolicyProviderFactory implements PolicyProviderFactory<JSPolicyRe
}
private EvaluatableScriptAdapter getEvaluatableScript(final AuthorizationProvider authz, final Policy policy) {
- return scripts.computeIfAbsent(policy.getId(), id -> {
+ return scriptCache.computeIfAbsent(policy.getId(), id -> {
final ScriptingProvider scripting = authz.getKeycloakSession().getProvider(ScriptingProvider.class);
ScriptModel script = getScriptModel(policy, authz.getRealm(), scripting);
return scripting.prepareEvaluatableScript(script);
@@ -115,6 +113,7 @@ public class JSPolicyProviderFactory implements PolicyProviderFactory<JSPolicyRe
}
private void updatePolicy(Policy policy, String code) {
+ scriptCache.remove(policy.getId());
policy.putConfig("code", code);
}
}
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/ScriptCache.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/ScriptCache.java
new file mode 100644
index 0000000..4180db6
--- /dev/null
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/ScriptCache.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * 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 org.keycloak.authorization.policy.provider.js;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.LockSupport;
+import java.util.function.Function;
+
+import org.keycloak.scripting.EvaluatableScriptAdapter;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ScriptCache {
+
+ /**
+ * The load factor.
+ */
+ private static final float DEFAULT_LOAD_FACTOR = 0.75f;
+
+ private final Map<String, CacheEntry> cache;
+
+ private final AtomicBoolean writing = new AtomicBoolean(false);
+
+ private final long maxAge;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param maxEntries the maximum number of entries to keep in the cache
+ */
+ public ScriptCache(int maxEntries) {
+ this(maxEntries, -1);
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ * @param maxEntries the maximum number of entries to keep in the cache
+ * @param maxAge the time in milliseconds that an entry can stay in the cache. If {@code -1}, entries never expire
+ */
+ public ScriptCache(final int maxEntries, long maxAge) {
+ cache = new LinkedHashMap<String, CacheEntry>(16, DEFAULT_LOAD_FACTOR, true) {
+ @Override
+ protected boolean removeEldestEntry(Map.Entry eldest) {
+ return cache.size() > maxEntries;
+ }
+ };
+ this.maxAge = maxAge;
+ }
+
+ public EvaluatableScriptAdapter computeIfAbsent(String id, Function<String, EvaluatableScriptAdapter> function) {
+ try {
+ if (parkForWriteAndCheckInterrupt()) {
+ return null;
+ }
+
+ CacheEntry entry = cache.computeIfAbsent(id, key -> new CacheEntry(key, function.apply(id), maxAge));
+
+ if (entry != null) {
+ return entry.value();
+ }
+
+ return null;
+ } finally {
+ writing.lazySet(false);
+ }
+ }
+
+ public EvaluatableScriptAdapter get(String uri) {
+ if (parkForReadAndCheckInterrupt()) {
+ return null;
+ }
+
+ CacheEntry cached = cache.get(uri);
+
+ if (cached != null) {
+ return removeIfExpired(cached);
+ }
+
+ return null;
+ }
+
+ public void remove(String key) {
+ try {
+ if (parkForWriteAndCheckInterrupt()) {
+ return;
+ }
+
+ cache.remove(key);
+ } finally {
+ writing.lazySet(false);
+ }
+ }
+
+ private EvaluatableScriptAdapter removeIfExpired(CacheEntry cached) {
+ if (cached == null) {
+ return null;
+ }
+
+ if (cached.isExpired()) {
+ remove(cached.key());
+ return null;
+ }
+
+ return cached.value();
+ }
+
+ private boolean parkForWriteAndCheckInterrupt() {
+ while (!writing.compareAndSet(false, true)) {
+ LockSupport.parkNanos(1L);
+ if (Thread.interrupted()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean parkForReadAndCheckInterrupt() {
+ while (writing.get()) {
+ LockSupport.parkNanos(1L);
+ if (Thread.interrupted()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static final class CacheEntry {
+
+ final String key;
+ final EvaluatableScriptAdapter value;
+ final long expiration;
+
+ CacheEntry(String key, EvaluatableScriptAdapter value, long maxAge) {
+ this.key = key;
+ this.value = value;
+ if(maxAge == -1) {
+ expiration = -1;
+ } else {
+ expiration = System.currentTimeMillis() + maxAge;
+ }
+ }
+
+ String key() {
+ return key;
+ }
+
+ EvaluatableScriptAdapter value() {
+ return value;
+ }
+
+ boolean isExpired() {
+ return expiration != -1 ? System.currentTimeMillis() > expiration : false;
+ }
+ }
+}