001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest.server.auth.oauth;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.security.*;
022import java.time.*;
023import java.util.*;
024
025import org.apache.juneau.rest.auth.oauth.flow.*;
026
027/**
028 * Thread-safe bounded LRU implementation of {@link TokenCache}.
029 *
030 * <p>
031 * Backed by a {@link LinkedHashMap} configured with {@code accessOrder=true} so iteration order tracks
032 * most-recently-used.  Eviction policy is the conjunction of:
033 *
034 * <ul>
035 *    <li><b>Size cap</b> &mdash; when the entry count would exceed {@code maxEntries}, the least-recently-used
036 *       entry is removed before the new entry is inserted.  Default {@code maxEntries=1000}.
037 *    <li><b>TTL eviction</b> &mdash; on lookup, an entry whose stored expiry instant has passed (relative to
038 *       the supplied {@code now} clock) is removed and treated as a miss.
039 * </ul>
040 *
041 * @since 10.0.0
042 */
043public class BoundedLruTokenCache implements TokenCache {
044
045   /** Default maximum entry count. */
046   public static final int DEFAULT_MAX_ENTRIES = 1000;
047
048   /**
049    * Static creator with the default max-entries.
050    *
051    * @return A new cache.
052    */
053   public static BoundedLruTokenCache create() {
054      return new BoundedLruTokenCache(DEFAULT_MAX_ENTRIES);
055   }
056
057   /**
058    * Static creator with a custom max-entries.
059    *
060    * @param maxEntries The maximum entry count.  Must be positive.
061    * @return A new cache.
062    */
063   public static BoundedLruTokenCache create(int maxEntries) {
064      if (maxEntries <= 0)
065         throw iaex("maxEntries must be positive (was %s)", maxEntries);
066      return new BoundedLruTokenCache(maxEntries);
067   }
068
069   private final int maxEntries;
070   private final Map<String,Object> entries;
071   private final Object lock = new Object();
072
073   /**
074    * Constructor.
075    *
076    * @param maxEntries The maximum entry count.
077    */
078   protected BoundedLruTokenCache(int maxEntries) {
079      this.maxEntries = maxEntries;
080      this.entries = new LinkedHashMap<>(16, 0.75f, true) {
081         private static final long serialVersionUID = 1L;
082         @Override
083         protected boolean removeEldestEntry(Map.Entry<String,Object> eldest) {
084            return size() > BoundedLruTokenCache.this.maxEntries;
085         }
086      };
087   }
088
089   /**
090    * Returns the configured maximum entry count.
091    *
092    * @return The maximum entry count.
093    */
094   public int getMaxEntries() {
095      return maxEntries;
096   }
097
098   @Override /* Overridden from TokenCache */
099   public void putPrincipal(String key, Principal principal, Duration ttl) {
100      reqnb("key", key);
101      reqnn("principal", principal);
102      reqnn("ttl", ttl);
103      if (ttl.isZero() || ttl.isNegative())
104         throw iaex("ttl must be positive (was %s)", ttl);
105      var expiresAt = Instant.now().plus(ttl);
106      synchronized (lock) {
107         entries.put(key, new CachedPrincipal(principal, expiresAt));
108      }
109   }
110
111   @Override /* Overridden from TokenCache */
112   public Optional<Principal> getPrincipal(String key) {
113      reqnb("key", key);
114      synchronized (lock) {
115         var v = entries.get(key);
116         if (!(v instanceof CachedPrincipal v2))
117            return oe();
118         if (!Instant.now().isBefore(v2.expiresAt())) {
119            entries.remove(key);
120            return oe();
121         }
122         return o(v2.principal());
123      }
124   }
125
126   @Override /* Overridden from TokenCache */
127   public void putToken(String key, OAuthToken token) {
128      reqnb("key", key);
129      reqnn("token", token);
130      synchronized (lock) {
131         entries.put(key, token);
132      }
133   }
134
135   @Override /* Overridden from TokenCache */
136   public Optional<OAuthToken> getToken(String key, Instant now, Duration skew) {
137      reqnb("key", key);
138      reqnn("now", now);
139      reqnn("skew", skew);
140      if (skew.isNegative())
141         throw iaex("skew must be non-negative (was %s)", skew);
142      synchronized (lock) {
143         var v = entries.get(key);
144         if (!(v instanceof OAuthToken v2))
145            return oe();
146         if (v2.isExpired(now, skew)) {
147            entries.remove(key);
148            return oe();
149         }
150         return o(v2);
151      }
152   }
153
154   @Override /* Overridden from TokenCache */
155   public void invalidate(String key) {
156      reqnb("key", key);
157      synchronized (lock) {
158         entries.remove(key);
159      }
160   }
161
162   @Override /* Overridden from TokenCache */
163   public int size() {
164      synchronized (lock) {
165         return entries.size();
166      }
167   }
168}