1 /*
2  * Copyright (c) 2002, 2021, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package sun.security.util;
27 
28 import java.util.*;
29 import java.lang.ref.*;
30 
31 /**
32  * Abstract base class and factory for caches. A cache is a key-value mapping.
33  * It has properties that make it more suitable for caching than a Map.
34  *
35  * The factory methods can be used to obtain two different implementations.
36  * They have the following properties:
37  *
38  *  . keys and values reside in memory
39  *
40  *  . keys and values must be non-null
41  *
42  *  . maximum size. Replacements are made in LRU order.
43  *
44  *  . optional lifetime, specified in seconds.
45  *
46  *  . safe for concurrent use by multiple threads
47  *
48  *  . values are held by either standard references or via SoftReferences.
49  *    SoftReferences have the advantage that they are automatically cleared
50  *    by the garbage collector in response to memory demand. This makes it
51  *    possible to simple set the maximum size to a very large value and let
52  *    the GC automatically size the cache dynamically depending on the
53  *    amount of available memory.
54  *
55  * However, note that because of the way SoftReferences are implemented in
56  * HotSpot at the moment, this may not work perfectly as it clears them fairly
57  * eagerly. Performance may be improved if the Java heap size is set to larger
58  * value using e.g. java -ms64M -mx128M foo.Test
59  *
60  * Cache sizing: the memory cache is implemented on top of a LinkedHashMap.
61  * In its current implementation, the number of buckets (NOT entries) in
62  * (Linked)HashMaps is always a power of two. It is recommended to set the
63  * maximum cache size to value that uses those buckets fully. For example,
64  * if a cache with somewhere between 500 and 1000 entries is desired, a
65  * maximum size of 750 would be a good choice: try 1024 buckets, with a
66  * load factor of 0.75f, the number of entries can be calculated as
67  * buckets / 4 * 3. As mentioned above, with a SoftReference cache, it is
68  * generally reasonable to set the size to a fairly large value.
69  *
70  * @author Andreas Sterbenz
71  */
72 public abstract class Cache<K,V> {
73 
Cache()74     protected Cache() {
75         // empty
76     }
77 
78     /**
79      * Return the number of currently valid entries in the cache.
80      */
size()81     public abstract int size();
82 
83     /**
84      * Remove all entries from the cache.
85      */
clear()86     public abstract void clear();
87 
88     /**
89      * Add an entry to the cache.
90      */
put(K key, V value)91     public abstract void put(K key, V value);
92 
93     /**
94      * Get a value from the cache.
95      */
get(Object key)96     public abstract V get(Object key);
97 
98     /**
99      * Remove an entry from the cache.
100      */
remove(Object key)101     public abstract void remove(Object key);
102 
103     /**
104      * Pull an entry from the cache.
105      */
pull(Object key)106     public abstract V pull(Object key);
107 
108     /**
109      * Set the maximum size.
110      */
setCapacity(int size)111     public abstract void setCapacity(int size);
112 
113     /**
114      * Set the timeout(in seconds).
115      */
setTimeout(int timeout)116     public abstract void setTimeout(int timeout);
117 
118     /**
119      * accept a visitor
120      */
accept(CacheVisitor<K,V> visitor)121     public abstract void accept(CacheVisitor<K,V> visitor);
122 
123     /**
124      * Return a new memory cache with the specified maximum size, unlimited
125      * lifetime for entries, with the values held by SoftReferences.
126      */
newSoftMemoryCache(int size)127     public static <K,V> Cache<K,V> newSoftMemoryCache(int size) {
128         return new MemoryCache<>(true, size);
129     }
130 
131     /**
132      * Return a new memory cache with the specified maximum size, the
133      * specified maximum lifetime (in seconds), with the values held
134      * by SoftReferences.
135      */
newSoftMemoryCache(int size, int timeout)136     public static <K,V> Cache<K,V> newSoftMemoryCache(int size, int timeout) {
137         return new MemoryCache<>(true, size, timeout);
138     }
139 
140     /**
141      * Return a new memory cache with the specified maximum size, unlimited
142      * lifetime for entries, with the values held by standard references.
143      */
newHardMemoryCache(int size)144     public static <K,V> Cache<K,V> newHardMemoryCache(int size) {
145         return new MemoryCache<>(false, size);
146     }
147 
148     /**
149      * Return a dummy cache that does nothing.
150      */
151     @SuppressWarnings("unchecked")
newNullCache()152     public static <K,V> Cache<K,V> newNullCache() {
153         return (Cache<K,V>) NullCache.INSTANCE;
154     }
155 
156     /**
157      * Return a new memory cache with the specified maximum size, the
158      * specified maximum lifetime (in seconds), with the values held
159      * by standard references.
160      */
newHardMemoryCache(int size, int timeout)161     public static <K,V> Cache<K,V> newHardMemoryCache(int size, int timeout) {
162         return new MemoryCache<>(false, size, timeout);
163     }
164 
165     /**
166      * Utility class that wraps a byte array and implements the equals()
167      * and hashCode() contract in a way suitable for Maps and caches.
168      */
169     public static class EqualByteArray {
170 
171         private final byte[] b;
172         private int hash;
173 
EqualByteArray(byte[] b)174         public EqualByteArray(byte[] b) {
175             this.b = b;
176         }
177 
hashCode()178         public int hashCode() {
179             int h = hash;
180             if (h == 0 && b.length > 0) {
181                 hash = h = Arrays.hashCode(b);
182             }
183             return h;
184         }
185 
equals(Object obj)186         public boolean equals(Object obj) {
187             if (this == obj) {
188                 return true;
189             }
190             if (obj instanceof EqualByteArray == false) {
191                 return false;
192             }
193             EqualByteArray other = (EqualByteArray)obj;
194             return Arrays.equals(this.b, other.b);
195         }
196     }
197 
198     public interface CacheVisitor<K,V> {
visit(Map<K,V> map)199         public void visit(Map<K,V> map);
200     }
201 
202 }
203 
204 class NullCache<K,V> extends Cache<K,V> {
205 
206     static final Cache<Object,Object> INSTANCE = new NullCache<>();
207 
NullCache()208     private NullCache() {
209         // empty
210     }
211 
size()212     public int size() {
213         return 0;
214     }
215 
clear()216     public void clear() {
217         // empty
218     }
219 
put(K key, V value)220     public void put(K key, V value) {
221         // empty
222     }
223 
get(Object key)224     public V get(Object key) {
225         return null;
226     }
227 
remove(Object key)228     public void remove(Object key) {
229         // empty
230     }
231 
pull(Object key)232     public V pull(Object key) {
233         return null;
234     }
235 
setCapacity(int size)236     public void setCapacity(int size) {
237         // empty
238     }
239 
setTimeout(int timeout)240     public void setTimeout(int timeout) {
241         // empty
242     }
243 
accept(CacheVisitor<K,V> visitor)244     public void accept(CacheVisitor<K,V> visitor) {
245         // empty
246     }
247 
248 }
249 
250 class MemoryCache<K,V> extends Cache<K,V> {
251 
252     private static final float LOAD_FACTOR = 0.75f;
253 
254     // XXXX
255     private static final boolean DEBUG = false;
256 
257     private final Map<K, CacheEntry<K,V>> cacheMap;
258     private int maxSize;
259     private long lifetime;
260     private long nextExpirationTime = Long.MAX_VALUE;
261 
262     // ReferenceQueue is of type V instead of Cache<K,V>
263     // to allow SoftCacheEntry to extend SoftReference<V>
264     private final ReferenceQueue<V> queue;
265 
MemoryCache(boolean soft, int maxSize)266     public MemoryCache(boolean soft, int maxSize) {
267         this(soft, maxSize, 0);
268     }
269 
MemoryCache(boolean soft, int maxSize, int lifetime)270     public MemoryCache(boolean soft, int maxSize, int lifetime) {
271         this.maxSize = maxSize;
272         this.lifetime = lifetime * 1000;
273         if (soft)
274             this.queue = new ReferenceQueue<>();
275         else
276             this.queue = null;
277 
278         cacheMap = new LinkedHashMap<>(1, LOAD_FACTOR, true);
279     }
280 
281     /**
282      * Empty the reference queue and remove all corresponding entries
283      * from the cache.
284      *
285      * This method should be called at the beginning of each public
286      * method.
287      */
emptyQueue()288     private void emptyQueue() {
289         if (queue == null) {
290             return;
291         }
292         int startSize = cacheMap.size();
293         while (true) {
294             @SuppressWarnings("unchecked")
295             CacheEntry<K,V> entry = (CacheEntry<K,V>)queue.poll();
296             if (entry == null) {
297                 break;
298             }
299             K key = entry.getKey();
300             if (key == null) {
301                 // key is null, entry has already been removed
302                 continue;
303             }
304             CacheEntry<K,V> currentEntry = cacheMap.remove(key);
305             // check if the entry in the map corresponds to the expired
306             // entry. If not, readd the entry
307             if ((currentEntry != null) && (entry != currentEntry)) {
308                 cacheMap.put(key, currentEntry);
309             }
310         }
311         if (DEBUG) {
312             int endSize = cacheMap.size();
313             if (startSize != endSize) {
314                 System.out.println("*** Expunged " + (startSize - endSize)
315                         + " entries, " + endSize + " entries left");
316             }
317         }
318     }
319 
320     /**
321      * Scan all entries and remove all expired ones.
322      */
expungeExpiredEntries()323     private void expungeExpiredEntries() {
324         emptyQueue();
325         if (lifetime == 0) {
326             return;
327         }
328         int cnt = 0;
329         long time = System.currentTimeMillis();
330         if (nextExpirationTime > time) {
331             return;
332         }
333         nextExpirationTime = Long.MAX_VALUE;
334         for (Iterator<CacheEntry<K,V>> t = cacheMap.values().iterator();
335                 t.hasNext(); ) {
336             CacheEntry<K,V> entry = t.next();
337             if (entry.isValid(time) == false) {
338                 t.remove();
339                 cnt++;
340             } else if (nextExpirationTime > entry.getExpirationTime()) {
341                 nextExpirationTime = entry.getExpirationTime();
342             }
343         }
344         if (DEBUG) {
345             if (cnt != 0) {
346                 System.out.println("Removed " + cnt
347                         + " expired entries, remaining " + cacheMap.size());
348             }
349         }
350     }
351 
size()352     public synchronized int size() {
353         expungeExpiredEntries();
354         return cacheMap.size();
355     }
356 
clear()357     public synchronized void clear() {
358         if (queue != null) {
359             // if this is a SoftReference cache, first invalidate() all
360             // entries so that GC does not have to enqueue them
361             for (CacheEntry<K,V> entry : cacheMap.values()) {
362                 entry.invalidate();
363             }
364             while (queue.poll() != null) {
365                 // empty
366             }
367         }
368         cacheMap.clear();
369     }
370 
put(K key, V value)371     public synchronized void put(K key, V value) {
372         emptyQueue();
373         long expirationTime = (lifetime == 0) ? 0 :
374                                         System.currentTimeMillis() + lifetime;
375         if (expirationTime < nextExpirationTime) {
376             nextExpirationTime = expirationTime;
377         }
378         CacheEntry<K,V> newEntry = newEntry(key, value, expirationTime, queue);
379         CacheEntry<K,V> oldEntry = cacheMap.put(key, newEntry);
380         if (oldEntry != null) {
381             oldEntry.invalidate();
382             return;
383         }
384         if (maxSize > 0 && cacheMap.size() > maxSize) {
385             expungeExpiredEntries();
386             if (cacheMap.size() > maxSize) { // still too large?
387                 Iterator<CacheEntry<K,V>> t = cacheMap.values().iterator();
388                 CacheEntry<K,V> lruEntry = t.next();
389                 if (DEBUG) {
390                     System.out.println("** Overflow removal "
391                         + lruEntry.getKey() + " | " + lruEntry.getValue());
392                 }
393                 t.remove();
394                 lruEntry.invalidate();
395             }
396         }
397     }
398 
get(Object key)399     public synchronized V get(Object key) {
400         emptyQueue();
401         CacheEntry<K,V> entry = cacheMap.get(key);
402         if (entry == null) {
403             return null;
404         }
405         long time = (lifetime == 0) ? 0 : System.currentTimeMillis();
406         if (entry.isValid(time) == false) {
407             if (DEBUG) {
408                 System.out.println("Ignoring expired entry");
409             }
410             cacheMap.remove(key);
411             return null;
412         }
413         return entry.getValue();
414     }
415 
remove(Object key)416     public synchronized void remove(Object key) {
417         emptyQueue();
418         CacheEntry<K,V> entry = cacheMap.remove(key);
419         if (entry != null) {
420             entry.invalidate();
421         }
422     }
423 
pull(Object key)424     public synchronized V pull(Object key) {
425         emptyQueue();
426         CacheEntry<K,V> entry = cacheMap.remove(key);
427         if (entry == null) {
428             return null;
429         }
430 
431         long time = (lifetime == 0) ? 0 : System.currentTimeMillis();
432         if (entry.isValid(time)) {
433             V value = entry.getValue();
434             entry.invalidate();
435             return value;
436         } else {
437             if (DEBUG) {
438                 System.out.println("Ignoring expired entry");
439             }
440             return null;
441         }
442     }
443 
setCapacity(int size)444     public synchronized void setCapacity(int size) {
445         expungeExpiredEntries();
446         if (size > 0 && cacheMap.size() > size) {
447             Iterator<CacheEntry<K,V>> t = cacheMap.values().iterator();
448             for (int i = cacheMap.size() - size; i > 0; i--) {
449                 CacheEntry<K,V> lruEntry = t.next();
450                 if (DEBUG) {
451                     System.out.println("** capacity reset removal "
452                         + lruEntry.getKey() + " | " + lruEntry.getValue());
453                 }
454                 t.remove();
455                 lruEntry.invalidate();
456             }
457         }
458 
459         maxSize = size > 0 ? size : 0;
460 
461         if (DEBUG) {
462             System.out.println("** capacity reset to " + size);
463         }
464     }
465 
setTimeout(int timeout)466     public synchronized void setTimeout(int timeout) {
467         emptyQueue();
468         lifetime = timeout > 0 ? timeout * 1000L : 0L;
469 
470         if (DEBUG) {
471             System.out.println("** lifetime reset to " + timeout);
472         }
473     }
474 
475     // it is a heavyweight method.
accept(CacheVisitor<K,V> visitor)476     public synchronized void accept(CacheVisitor<K,V> visitor) {
477         expungeExpiredEntries();
478         Map<K,V> cached = getCachedEntries();
479 
480         visitor.visit(cached);
481     }
482 
getCachedEntries()483     private Map<K,V> getCachedEntries() {
484         Map<K,V> kvmap = new HashMap<>(cacheMap.size());
485 
486         for (CacheEntry<K,V> entry : cacheMap.values()) {
487             kvmap.put(entry.getKey(), entry.getValue());
488         }
489 
490         return kvmap;
491     }
492 
newEntry(K key, V value, long expirationTime, ReferenceQueue<V> queue)493     protected CacheEntry<K,V> newEntry(K key, V value,
494             long expirationTime, ReferenceQueue<V> queue) {
495         if (queue != null) {
496             return new SoftCacheEntry<>(key, value, expirationTime, queue);
497         } else {
498             return new HardCacheEntry<>(key, value, expirationTime);
499         }
500     }
501 
502     private static interface CacheEntry<K,V> {
503 
isValid(long currentTime)504         boolean isValid(long currentTime);
505 
invalidate()506         void invalidate();
507 
getKey()508         K getKey();
509 
getValue()510         V getValue();
511 
getExpirationTime()512         long getExpirationTime();
513     }
514 
515     private static class HardCacheEntry<K,V> implements CacheEntry<K,V> {
516 
517         private K key;
518         private V value;
519         private long expirationTime;
520 
HardCacheEntry(K key, V value, long expirationTime)521         HardCacheEntry(K key, V value, long expirationTime) {
522             this.key = key;
523             this.value = value;
524             this.expirationTime = expirationTime;
525         }
526 
getKey()527         public K getKey() {
528             return key;
529         }
530 
getValue()531         public V getValue() {
532             return value;
533         }
534 
getExpirationTime()535         public long getExpirationTime() {
536             return expirationTime;
537         }
538 
isValid(long currentTime)539         public boolean isValid(long currentTime) {
540             boolean valid = (currentTime <= expirationTime);
541             if (valid == false) {
542                 invalidate();
543             }
544             return valid;
545         }
546 
invalidate()547         public void invalidate() {
548             key = null;
549             value = null;
550             expirationTime = -1;
551         }
552     }
553 
554     private static class SoftCacheEntry<K,V>
555             extends SoftReference<V>
556             implements CacheEntry<K,V> {
557 
558         private K key;
559         private long expirationTime;
560 
SoftCacheEntry(K key, V value, long expirationTime, ReferenceQueue<V> queue)561         SoftCacheEntry(K key, V value, long expirationTime,
562                 ReferenceQueue<V> queue) {
563             super(value, queue);
564             this.key = key;
565             this.expirationTime = expirationTime;
566         }
567 
getKey()568         public K getKey() {
569             return key;
570         }
571 
getValue()572         public V getValue() {
573             return get();
574         }
575 
getExpirationTime()576         public long getExpirationTime() {
577             return expirationTime;
578         }
579 
isValid(long currentTime)580         public boolean isValid(long currentTime) {
581             boolean valid = (currentTime <= expirationTime) && (get() != null);
582             if (valid == false) {
583                 invalidate();
584             }
585             return valid;
586         }
587 
invalidate()588         public void invalidate() {
589             clear();
590             key = null;
591             expirationTime = -1;
592         }
593     }
594 
595 }
596