View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.juneau.commons.collections;
18  
19  import static org.junit.jupiter.api.Assertions.*;
20  import static org.apache.juneau.commons.collections.CacheMode.*;
21  import static org.apache.juneau.junit.bct.BctAssertions.*;
22  
23  import java.util.concurrent.atomic.*;
24  
25  import org.apache.juneau.*;
26  import org.junit.jupiter.api.*;
27  
28  class Cache4_Test extends TestBase {
29  
30  	//====================================================================================================
31  	// a - Basic cache operations
32  	//====================================================================================================
33  
34  	@Test
35  	void a01_defaultSupplier_basic() {
36  		var callCount = new AtomicInteger();
37  		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class)
38  			.supplier((k1, k2, k3, k4) -> {
39  				callCount.incrementAndGet();
40  				return k1 + ":" + k2 + ":" + k3 + ":" + k4;
41  			})
42  			.build();
43  
44  		var result1 = x.get("en", "US", "formal", 1);
45  		var result2 = x.get("en", "US", "formal", 1); // Cache hit
46  
47  		assertEquals("en:US:formal:1", result1);
48  		assertEquals("en:US:formal:1", result2);
49  		assertEquals(1, callCount.get());
50  		assertEquals(1, x.getCacheHits());
51  	}
52  
53  	@Test
54  	void a02_overrideSupplier() {
55  		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class)
56  			.supplier((k1, k2, k3, k4) -> "DEFAULT")
57  			.build();
58  
59  		var result = x.get("en", "US", "formal", 1, () -> "OVERRIDE");
60  
61  		assertEquals("OVERRIDE", result);
62  	}
63  
64  	@Test
65  	void a03_nullKeys() {
66  		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class)
67  			.supplier((k1, k2, k3, k4) -> "value-" + k1 + "-" + k2 + "-" + k3 + "-" + k4)
68  			.build();
69  
70  		// Null keys are now allowed
71  		assertEquals("value-null-US-formal-1", x.get(null, "US", "formal", 1));
72  		assertEquals("value-en-null-formal-1", x.get("en", null, "formal", 1));
73  		assertEquals("value-en-US-null-1", x.get("en", "US", null, 1));
74  		assertEquals("value-en-US-formal-null", x.get("en", "US", "formal", null));
75  		assertEquals("value-null-null-null-null", x.get(null, null, null, null));
76  
77  		// Cached values should be returned on subsequent calls
78  		assertEquals("value-null-US-formal-1", x.get(null, "US", "formal", 1));
79  		assertEquals("value-en-null-formal-1", x.get("en", null, "formal", 1));
80  	}
81  
82  	@Test
83  	void a04_disableCaching() {
84  		var callCount = new AtomicInteger();
85  		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class)
86  			.cacheMode(NONE)
87  			.supplier((k1, k2, k3, k4) -> {
88  				callCount.incrementAndGet();
89  				return "value";
90  			})
91  			.build();
92  
93  		x.get("en", "US", "formal", 1);
94  		x.get("en", "US", "formal", 1);
95  
96  		assertEquals(2, callCount.get()); // Called twice
97  		assertEmpty(x);
98  	}
99  
100 	@Test
101 	void a04b_weakMode_basicCaching() {
102 		var callCount = new AtomicInteger();
103 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class)
104 			.cacheMode(WEAK)
105 			.supplier((k1, k2, k3, k4) -> {
106 				callCount.incrementAndGet();
107 				return k1 + ":" + k2 + ":" + k3 + ":" + k4;
108 			})
109 			.build();
110 
111 		// First call - cache miss
112 		var result1 = x.get("en", "US", "formal", 1);
113 
114 		// Second call - cache hit
115 		var result2 = x.get("en", "US", "formal", 1);
116 
117 		assertEquals("en:US:formal:1", result1);
118 		assertEquals("en:US:formal:1", result2);
119 		assertSame(result1, result2);
120 		assertEquals(1, callCount.get()); // Supplier only called once
121 		assertSize(1, x);
122 		assertEquals(1, x.getCacheHits());
123 	}
124 
125 	@Test
126 	void a04c_weakMode_multipleKeys() {
127 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class)
128 			.cacheMode(WEAK)
129 			.supplier((k1, k2, k3, k4) -> k1 + ":" + k2 + ":" + k3 + ":" + k4)
130 			.build();
131 
132 		x.get("en", "US", "formal", 1);
133 		x.get("fr", "FR", "formal", 2);
134 		x.get("de", "DE", "formal", 3);
135 
136 		assertSize(3, x);
137 		assertEquals(0, x.getCacheHits());
138 
139 		// Verify all cached
140 		assertEquals("en:US:formal:1", x.get("en", "US", "formal", 1));
141 		assertEquals("fr:FR:formal:2", x.get("fr", "FR", "formal", 2));
142 		assertEquals("de:DE:formal:3", x.get("de", "DE", "formal", 3));
143 		assertEquals(3, x.getCacheHits());
144 	}
145 
146 	@Test
147 	void a04d_weakMode_clear() {
148 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class)
149 			.cacheMode(WEAK)
150 			.supplier((k1, k2, k3, k4) -> "value")
151 			.build();
152 
153 		x.get("en", "US", "formal", 1);
154 		x.get("fr", "FR", "formal", 2);
155 		assertSize(2, x);
156 
157 		x.clear();
158 		assertEmpty(x);
159 	}
160 
161 	@Test
162 	void a04e_weakMode_maxSize() {
163 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class)
164 			.cacheMode(WEAK)
165 			.maxSize(2)
166 			.supplier((k1, k2, k3, k4) -> "value")
167 			.build();
168 
169 		x.get("en", "US", "formal", 1);
170 		x.get("fr", "FR", "formal", 2);
171 		assertSize(2, x);
172 
173 		// 3rd item doesn't trigger eviction yet
174 		x.get("de", "DE", "formal", 3);
175 		assertSize(3, x);
176 
177 		// 4th item triggers eviction
178 		x.get("es", "ES", "formal", 4);
179 		assertSize(1, x);
180 	}
181 
182 	@Test
183 	void a04f_weakMethod_basicCaching() {
184 		// Test the weak() convenience method
185 		var callCount = new AtomicInteger();
186 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class)
187 			.weak()
188 			.supplier((k1, k2, k3, k4) -> {
189 				callCount.incrementAndGet();
190 				return k1 + ":" + k2 + ":" + k3 + ":" + k4;
191 			})
192 			.build();
193 
194 		// First call - cache miss
195 		var result1 = x.get("en", "US", "formal", 1);
196 
197 		// Second call - cache hit
198 		var result2 = x.get("en", "US", "formal", 1);
199 
200 		assertEquals("en:US:formal:1", result1);
201 		assertEquals("en:US:formal:1", result2);
202 		assertSame(result1, result2);
203 		assertEquals(1, callCount.get()); // Supplier only called once
204 		assertSize(1, x);
205 		assertEquals(1, x.getCacheHits());
206 	}
207 
208 	@Test
209 	void a04g_weakMethod_chaining() {
210 		// Test that weak() can be chained with other builder methods
211 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class)
212 			.weak()
213 			.maxSize(100)
214 			.supplier((k1, k2, k3, k4) -> k1 + ":" + k2 + ":" + k3 + ":" + k4)
215 			.build();
216 
217 		var result = x.get("en", "US", "formal", 1);
218 		assertEquals("en:US:formal:1", result);
219 		assertSize(1, x);
220 	}
221 
222 	@Test
223 	void a05_maxSize() {
224 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class)
225 			.maxSize(2)
226 			.supplier((k1, k2, k3, k4) -> "value")
227 			.build();
228 
229 		x.get("en", "US", "formal", 1);
230 		x.get("fr", "FR", "formal", 2);
231 		assertSize(2, x);
232 
233 		x.get("de", "DE", "formal", 3); // Doesn't exceed yet
234 		assertSize(3, x);
235 
236 		x.get("es", "ES", "formal", 4); // Exceeds max (3 > 2), triggers clear
237 		assertSize(1, x); // Cleared
238 	}
239 
240 	@Test
241 	void a06_cacheHitsTracking() {
242 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class)
243 			.supplier((k1, k2, k3, k4) -> "value")
244 			.build();
245 
246 		x.get("en", "US", "formal", 1); // Miss
247 		assertEquals(0, x.getCacheHits());
248 
249 		x.get("en", "US", "formal", 1); // Hit
250 		x.get("en", "US", "formal", 1); // Hit
251 		assertEquals(2, x.getCacheHits());
252 	}
253 
254 	//====================================================================================================
255 	// b - put(), isEmpty(), containsKey()
256 	//====================================================================================================
257 
258 	@Test
259 	void b01_put() {
260 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class).build();
261 		var previous = x.put("en", "US", "formal", 1, "value");
262 		assertNull(previous);
263 		assertEquals("value", x.get("en", "US", "formal", 1, () -> "should not be called"));
264 	}
265 
266 	@Test
267 	void b01b_put_withNullValue() {
268 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class).build();
269 		x.put("en", "US", "formal", 1, "value1");
270 		var previous = x.put("en", "US", "formal", 1, null);
271 		assertEquals("value1", previous);
272 		assertFalse(x.containsKey("en", "US", "formal", 1));
273 	}
274 
275 	@Test
276 	void b02_isEmpty() {
277 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class).build();
278 		assertTrue(x.isEmpty());
279 		x.put("en", "US", "formal", 1, "value");
280 		assertFalse(x.isEmpty());
281 		x.clear();
282 		assertTrue(x.isEmpty());
283 	}
284 
285 	@Test
286 	void b03_containsKey() {
287 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class).build();
288 		assertFalse(x.containsKey("en", "US", "formal", 1));
289 		x.put("en", "US", "formal", 1, "value");
290 		assertTrue(x.containsKey("en", "US", "formal", 1));
291 	}
292 
293 	//====================================================================================================
294 	// c - create(), disableCaching(), null values
295 	//====================================================================================================
296 
297 	@Test
298 	void c01_create() {
299 		var x = Cache4.<String, String, String, Integer, String>create()
300 			.supplier((k1, k2, k3, k4) -> k1 + ":" + k2 + ":" + k3 + ":" + k4)
301 			.build();
302 		var result = x.get("en", "US", "formal", 1);
303 		assertEquals("en:US:formal:1", result);
304 	}
305 
306 	@Test
307 	void c02_disableCaching() {
308 		var callCount = new AtomicInteger();
309 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class)
310 			.cacheMode(NONE)
311 			.supplier((k1, k2, k3, k4) -> {
312 				callCount.incrementAndGet();
313 				return "value";
314 			})
315 			.build();
316 		x.get("en", "US", "formal", 1);
317 		x.get("en", "US", "formal", 1);
318 		assertEquals(2, callCount.get());
319 		assertTrue(x.isEmpty());
320 	}
321 
322 	@Test
323 	void c03_nullValue_notCached() {
324 		var callCount = new AtomicInteger();
325 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class).build();
326 		x.get("en", "US", "formal", 1, () -> {
327 			callCount.incrementAndGet();
328 			return null;
329 		});
330 		x.get("en", "US", "formal", 1, () -> {
331 			callCount.incrementAndGet();
332 			return null;
333 		});
334 		assertEquals(2, callCount.get());
335 		assertTrue(x.isEmpty());
336 	}
337 
338 	//====================================================================================================
339 	// d - remove() and containsValue()
340 	//====================================================================================================
341 
342 	@Test
343 	void d01_remove() {
344 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class).build();
345 		x.put("en", "US", "formal", 1, "value1");
346 		var removed = x.remove("en", "US", "formal", 1);
347 		assertEquals("value1", removed);
348 		assertFalse(x.containsKey("en", "US", "formal", 1));
349 	}
350 
351 	@Test
352 	void d02_containsValue() {
353 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class).build();
354 		x.put("en", "US", "formal", 1, "value1");
355 		assertTrue(x.containsValue("value1"));
356 		assertFalse(x.containsValue("value2"));
357 	}
358 
359 	@Test
360 	void d03_containsValue_nullValue() {
361 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class).build();
362 		// Null values can't be cached, so containsValue(null) should return false
363 		x.get("en", "US", "formal", 1, () -> null);
364 		assertFalse(x.containsValue(null));
365 		// Also test with empty cache
366 		var x2 = Cache4.of(String.class, String.class, String.class, Integer.class, String.class).build();
367 		assertFalse(x2.containsValue(null));
368 	}
369 
370 	//====================================================================================================
371 	// e - logOnExit() builder methods
372 	//====================================================================================================
373 
374 	@Test
375 	void e01_logOnExit_withStringId() {
376 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class)
377 			.logOnExit("TestCache4")
378 			.supplier((k1, k2, k3, k4) -> k1 + ":" + k2 + ":" + k3 + ":" + k4)
379 			.build();
380 		x.get("en", "US", "formal", 1);
381 		assertSize(1, x);
382 	}
383 
384 	@Test
385 	void e02_logOnExit_withBoolean() {
386 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class)
387 			.logOnExit(true, "MyCache4")
388 			.supplier((k1, k2, k3, k4) -> k1 + ":" + k2 + ":" + k3 + ":" + k4)
389 			.build();
390 		x.get("en", "US", "formal", 1);
391 		assertSize(1, x);
392 	}
393 
394 	//====================================================================================================
395 	// f - Thread-local cache mode
396 	//====================================================================================================
397 
398 	@Test
399 	void f01_threadLocal_basicCaching() {
400 		var callCount = new AtomicInteger();
401 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class)
402 			.threadLocal()
403 			.supplier((k1, k2, k3, k4) -> {
404 				callCount.incrementAndGet();
405 				return k1 + ":" + k2 + ":" + k3 + ":" + k4;
406 			})
407 			.build();
408 
409 		// First call - cache miss
410 		var result1 = x.get("en", "US", "formal", 1);
411 
412 		// Second call - cache hit
413 		var result2 = x.get("en", "US", "formal", 1);
414 
415 		assertEquals("en:US:formal:1", result1);
416 		assertEquals("en:US:formal:1", result2);
417 		assertSame(result1, result2);
418 		assertEquals(1, callCount.get()); // Supplier only called once
419 		assertSize(1, x);
420 		assertEquals(1, x.getCacheHits());
421 	}
422 
423 	@Test
424 	void f02_threadLocal_eachThreadHasOwnCache() throws Exception {
425 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class)
426 			.threadLocal()
427 			.build();
428 		var executor = java.util.concurrent.Executors.newFixedThreadPool(2);
429 		var threadValues = new java.util.concurrent.ConcurrentHashMap<Thread, String>();
430 
431 		// Each thread caches ("en", "US", "formal", 1) with its own value
432 		var future1 = java.util.concurrent.CompletableFuture.runAsync(() -> {
433 			var value = x.get("en", "US", "formal", 1, () -> "thread1-value");
434 			threadValues.put(Thread.currentThread(), value);
435 		}, executor);
436 
437 		var future2 = java.util.concurrent.CompletableFuture.runAsync(() -> {
438 			var value = x.get("en", "US", "formal", 1, () -> "thread2-value");
439 			threadValues.put(Thread.currentThread(), value);
440 		}, executor);
441 
442 		java.util.concurrent.CompletableFuture.allOf(future1, future2).get(5, java.util.concurrent.TimeUnit.SECONDS);
443 
444 		// Verify both threads cached their own values
445 		assertEquals(2, threadValues.size());
446 		assertTrue(threadValues.containsValue("thread1-value"));
447 		assertTrue(threadValues.containsValue("thread2-value"));
448 
449 		executor.shutdown();
450 	}
451 
452 	@Test
453 	void f03_threadLocal_multipleKeys() {
454 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class)
455 			.threadLocal()
456 			.supplier((k1, k2, k3, k4) -> k1 + ":" + k2 + ":" + k3 + ":" + k4)
457 			.build();
458 
459 		x.get("en", "US", "formal", 1);
460 		x.get("fr", "FR", "informal", 2);
461 		x.get("de", "DE", "formal", 3);
462 
463 		assertSize(3, x);
464 		assertEquals(0, x.getCacheHits());
465 
466 		// Verify all cached
467 		assertEquals("en:US:formal:1", x.get("en", "US", "formal", 1));
468 		assertEquals("fr:FR:informal:2", x.get("fr", "FR", "informal", 2));
469 		assertEquals("de:DE:formal:3", x.get("de", "DE", "formal", 3));
470 		assertEquals(3, x.getCacheHits());
471 	}
472 
473 	@Test
474 	void f04_threadLocal_clear() {
475 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class)
476 			.threadLocal()
477 			.supplier((k1, k2, k3, k4) -> "value")
478 			.build();
479 
480 		x.get("en", "US", "formal", 1);
481 		x.get("fr", "FR", "informal", 2);
482 		assertSize(2, x);
483 
484 		x.clear();
485 		assertEmpty(x);
486 	}
487 
488 	@Test
489 	void f05_threadLocal_maxSize() {
490 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class)
491 			.threadLocal()
492 			.maxSize(2)
493 			.supplier((k1, k2, k3, k4) -> "value")
494 			.build();
495 
496 		x.get("en", "US", "formal", 1);
497 		x.get("fr", "FR", "informal", 2);
498 		assertSize(2, x);
499 
500 		// 3rd item doesn't trigger eviction yet
501 		x.get("de", "DE", "formal", 3);
502 		assertSize(3, x);
503 
504 		// 4th item triggers eviction
505 		x.get("es", "ES", "informal", 4);
506 		assertSize(1, x);
507 	}
508 
509 	//====================================================================================================
510 	// g - Thread-local + weak mode combination
511 	//====================================================================================================
512 
513 	@Test
514 	void g01_threadLocal_weakMode_basicCaching() {
515 		var callCount = new AtomicInteger();
516 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class)
517 			.threadLocal()
518 			.cacheMode(WEAK)
519 			.supplier((k1, k2, k3, k4) -> {
520 				callCount.incrementAndGet();
521 				return k1 + ":" + k2 + ":" + k3 + ":" + k4;
522 			})
523 			.build();
524 
525 		// First call - cache miss
526 		var result1 = x.get("en", "US", "formal", 1);
527 
528 		// Second call - cache hit
529 		var result2 = x.get("en", "US", "formal", 1);
530 
531 		assertEquals("en:US:formal:1", result1);
532 		assertEquals("en:US:formal:1", result2);
533 		assertSame(result1, result2);
534 		assertEquals(1, callCount.get()); // Supplier only called once
535 		assertSize(1, x);
536 		assertEquals(1, x.getCacheHits());
537 	}
538 
539 	@Test
540 	void g02_threadLocal_weakMode_eachThreadHasOwnCache() throws Exception {
541 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class)
542 			.threadLocal()
543 			.cacheMode(WEAK)
544 			.build();
545 		var executor = java.util.concurrent.Executors.newFixedThreadPool(2);
546 		var threadValues = new java.util.concurrent.ConcurrentHashMap<Thread, String>();
547 
548 		// Each thread caches ("en", "US", "formal", 1) with its own value
549 		var future1 = java.util.concurrent.CompletableFuture.runAsync(() -> {
550 			var value = x.get("en", "US", "formal", 1, () -> "thread1-value");
551 			threadValues.put(Thread.currentThread(), value);
552 		}, executor);
553 
554 		var future2 = java.util.concurrent.CompletableFuture.runAsync(() -> {
555 			var value = x.get("en", "US", "formal", 1, () -> "thread2-value");
556 			threadValues.put(Thread.currentThread(), value);
557 		}, executor);
558 
559 		java.util.concurrent.CompletableFuture.allOf(future1, future2).get(5, java.util.concurrent.TimeUnit.SECONDS);
560 
561 		// Verify both threads cached their own values
562 		assertEquals(2, threadValues.size());
563 		assertTrue(threadValues.containsValue("thread1-value"));
564 		assertTrue(threadValues.containsValue("thread2-value"));
565 
566 		executor.shutdown();
567 	}
568 
569 	@Test
570 	void g03_threadLocal_weakMode_clear() {
571 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class)
572 			.threadLocal()
573 			.cacheMode(WEAK)
574 			.supplier((k1, k2, k3, k4) -> "value")
575 			.build();
576 
577 		x.get("en", "US", "formal", 1);
578 		x.get("fr", "FR", "informal", 2);
579 		assertSize(2, x);
580 
581 		x.clear();
582 		assertEmpty(x);
583 	}
584 
585 	@Test
586 	void g04_threadLocal_weakMode_maxSize() {
587 		var x = Cache4.of(String.class, String.class, String.class, Integer.class, String.class)
588 			.threadLocal()
589 			.cacheMode(WEAK)
590 			.maxSize(2)
591 			.supplier((k1, k2, k3, k4) -> "value")
592 			.build();
593 
594 		x.get("en", "US", "formal", 1);
595 		x.get("fr", "FR", "informal", 2);
596 		assertSize(2, x);
597 
598 		// 3rd item doesn't trigger eviction yet
599 		x.get("de", "DE", "formal", 3);
600 		assertSize(3, x);
601 
602 		// 4th item triggers eviction
603 		x.get("es", "ES", "informal", 4);
604 		assertSize(1, x);
605 	}
606 }
607