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 Cache3_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 = Cache3.of(String.class, String.class, Integer.class, String.class)
38  			.supplier((k1, k2, k3) -> {
39  				callCount.incrementAndGet();
40  				return k1 + ":" + k2 + ":" + k3;
41  			})
42  			.build();
43  
44  		var result1 = x.get("en", "US", 1);
45  		var result2 = x.get("en", "US", 1); // Cache hit
46  
47  		assertEquals("en:US:1", result1);
48  		assertEquals("en:US:1", result2);
49  		assertEquals(1, callCount.get());
50  		assertEquals(1, x.getCacheHits());
51  	}
52  
53  	@Test
54  	void a02_overrideSupplier() {
55  		var x = Cache3.of(String.class, String.class, Integer.class, String.class)
56  			.supplier((k1, k2, k3) -> "DEFAULT")
57  			.build();
58  
59  		var result = x.get("en", "US", 1, () -> "OVERRIDE");
60  
61  		assertEquals("OVERRIDE", result);
62  	}
63  
64  	@Test
65  	void a03_nullKeys() {
66  		var x = Cache3.of(String.class, String.class, Integer.class, String.class)
67  			.supplier((k1, k2, k3) -> "value-" + k1 + "-" + k2 + "-" + k3)
68  			.build();
69  
70  		// Null keys are now allowed
71  		assertEquals("value-null-US-1", x.get(null, "US", 1));
72  		assertEquals("value-en-null-1", x.get("en", null, 1));
73  		assertEquals("value-en-US-null", x.get("en", "US", null));
74  		assertEquals("value-null-null-null", x.get(null, null, null));
75  
76  		// Cached values should be returned on subsequent calls
77  		assertEquals("value-null-US-1", x.get(null, "US", 1));
78  		assertEquals("value-en-null-1", x.get("en", null, 1));
79  	}
80  
81  	@Test
82  	void a04_disableCaching() {
83  		var callCount = new AtomicInteger();
84  		var x = Cache3.of(String.class, String.class, Integer.class, String.class)
85  			.cacheMode(NONE)
86  			.supplier((k1, k2, k3) -> {
87  				callCount.incrementAndGet();
88  				return "value";
89  			})
90  			.build();
91  
92  		x.get("en", "US", 1);
93  		x.get("en", "US", 1);
94  
95  		assertEquals(2, callCount.get()); // Called twice
96  		assertEmpty(x);
97  	}
98  
99  	@Test
100 	void a04b_weakMode_basicCaching() {
101 		var callCount = new AtomicInteger();
102 		var x = Cache3.of(String.class, String.class, Integer.class, String.class)
103 			.cacheMode(WEAK)
104 			.supplier((k1, k2, k3) -> {
105 				callCount.incrementAndGet();
106 				return k1 + ":" + k2 + ":" + k3;
107 			})
108 			.build();
109 
110 		// First call - cache miss
111 		var result1 = x.get("en", "US", 1);
112 
113 		// Second call - cache hit
114 		var result2 = x.get("en", "US", 1);
115 
116 		assertEquals("en:US:1", result1);
117 		assertEquals("en:US:1", result2);
118 		assertSame(result1, result2);
119 		assertEquals(1, callCount.get()); // Supplier only called once
120 		assertSize(1, x);
121 		assertEquals(1, x.getCacheHits());
122 	}
123 
124 	@Test
125 	void a04c_weakMode_multipleKeys() {
126 		var x = Cache3.of(String.class, String.class, Integer.class, String.class)
127 			.cacheMode(WEAK)
128 			.supplier((k1, k2, k3) -> k1 + ":" + k2 + ":" + k3)
129 			.build();
130 
131 		x.get("en", "US", 1);
132 		x.get("fr", "FR", 2);
133 		x.get("de", "DE", 3);
134 
135 		assertSize(3, x);
136 		assertEquals(0, x.getCacheHits());
137 
138 		// Verify all cached
139 		assertEquals("en:US:1", x.get("en", "US", 1));
140 		assertEquals("fr:FR:2", x.get("fr", "FR", 2));
141 		assertEquals("de:DE:3", x.get("de", "DE", 3));
142 		assertEquals(3, x.getCacheHits());
143 	}
144 
145 	@Test
146 	void a04d_weakMode_clear() {
147 		var x = Cache3.of(String.class, String.class, Integer.class, String.class)
148 			.cacheMode(WEAK)
149 			.supplier((k1, k2, k3) -> "value")
150 			.build();
151 
152 		x.get("en", "US", 1);
153 		x.get("fr", "FR", 2);
154 		assertSize(2, x);
155 
156 		x.clear();
157 		assertEmpty(x);
158 	}
159 
160 	@Test
161 	void a04e_weakMode_maxSize() {
162 		var x = Cache3.of(String.class, String.class, Integer.class, String.class)
163 			.cacheMode(WEAK)
164 			.maxSize(2)
165 			.supplier((k1, k2, k3) -> "value")
166 			.build();
167 
168 		x.get("en", "US", 1);
169 		x.get("fr", "FR", 2);
170 		assertSize(2, x);
171 
172 		// 3rd item doesn't trigger eviction yet
173 		x.get("de", "DE", 3);
174 		assertSize(3, x);
175 
176 		// 4th item triggers eviction
177 		x.get("es", "ES", 4);
178 		assertSize(1, x);
179 	}
180 
181 	@Test
182 	void a04f_weakMethod_basicCaching() {
183 		// Test the weak() convenience method
184 		var callCount = new AtomicInteger();
185 		var x = Cache3.of(String.class, String.class, Integer.class, String.class)
186 			.weak()
187 			.supplier((k1, k2, k3) -> {
188 				callCount.incrementAndGet();
189 				return k1 + ":" + k2 + ":" + k3;
190 			})
191 			.build();
192 
193 		// First call - cache miss
194 		var result1 = x.get("en", "US", 1);
195 
196 		// Second call - cache hit
197 		var result2 = x.get("en", "US", 1);
198 
199 		assertEquals("en:US:1", result1);
200 		assertEquals("en:US:1", result2);
201 		assertSame(result1, result2);
202 		assertEquals(1, callCount.get()); // Supplier only called once
203 		assertSize(1, x);
204 		assertEquals(1, x.getCacheHits());
205 	}
206 
207 	@Test
208 	void a04g_weakMethod_chaining() {
209 		// Test that weak() can be chained with other builder methods
210 		var x = Cache3.of(String.class, String.class, Integer.class, String.class)
211 			.weak()
212 			.maxSize(100)
213 			.supplier((k1, k2, k3) -> k1 + ":" + k2 + ":" + k3)
214 			.build();
215 
216 		var result = x.get("en", "US", 1);
217 		assertEquals("en:US:1", result);
218 		assertSize(1, x);
219 	}
220 
221 	@Test
222 	void a05_maxSize() {
223 		var x = Cache3.of(String.class, String.class, Integer.class, String.class)
224 			.maxSize(2)
225 			.supplier((k1, k2, k3) -> "value")
226 			.build();
227 
228 		x.get("en", "US", 1);
229 		x.get("fr", "FR", 2);
230 		assertSize(2, x);
231 
232 		x.get("de", "DE", 3);
233 		assertSize(3, x);
234 
235 		x.get("es", "ES", 4); // This exceeds max (3 > 2)
236 		assertSize(1, x); // Cleared
237 	}
238 
239 	@Test
240 	void a06_cacheHitsTracking() {
241 		var x = Cache3.of(String.class, String.class, Integer.class, String.class)
242 			.supplier((k1, k2, k3) -> "value")
243 			.build();
244 
245 		x.get("en", "US", 1); // Miss
246 		assertEquals(0, x.getCacheHits());
247 
248 		x.get("en", "US", 1); // Hit
249 		x.get("en", "US", 1); // Hit
250 		assertEquals(2, x.getCacheHits());
251 	}
252 
253 	//====================================================================================================
254 	// b - put(), isEmpty(), containsKey()
255 	//====================================================================================================
256 
257 	@Test
258 	void b01_put() {
259 		var x = Cache3.of(String.class, String.class, Integer.class, String.class).build();
260 		var previous = x.put("en", "US", 1, "value");
261 		assertNull(previous);
262 		assertEquals("value", x.get("en", "US", 1, () -> "should not be called"));
263 	}
264 
265 	@Test
266 	void b01b_put_withNullValue() {
267 		var x = Cache3.of(String.class, String.class, Integer.class, String.class).build();
268 		x.put("en", "US", 1, "value1");
269 		var previous = x.put("en", "US", 1, null);
270 		assertEquals("value1", previous);
271 		assertFalse(x.containsKey("en", "US", 1));
272 	}
273 
274 	@Test
275 	void b02_isEmpty() {
276 		var x = Cache3.of(String.class, String.class, Integer.class, String.class).build();
277 		assertTrue(x.isEmpty());
278 		x.put("en", "US", 1, "value");
279 		assertFalse(x.isEmpty());
280 		x.clear();
281 		assertTrue(x.isEmpty());
282 	}
283 
284 	@Test
285 	void b03_containsKey() {
286 		var x = Cache3.of(String.class, String.class, Integer.class, String.class).build();
287 		assertFalse(x.containsKey("en", "US", 1));
288 		x.put("en", "US", 1, "value");
289 		assertTrue(x.containsKey("en", "US", 1));
290 		assertFalse(x.containsKey("fr", "FR", 2));
291 	}
292 
293 	//====================================================================================================
294 	// c - create(), disableCaching(), null values
295 	//====================================================================================================
296 
297 	@Test
298 	void c01_create() {
299 		var x = Cache3.<String, String, Integer, String>create()
300 			.supplier((k1, k2, k3) -> k1 + ":" + k2 + ":" + k3)
301 			.build();
302 		var result = x.get("en", "US", 1);
303 		assertEquals("en:US:1", result);
304 	}
305 
306 	@Test
307 	void c02_disableCaching() {
308 		var callCount = new AtomicInteger();
309 		var x = Cache3.of(String.class, String.class, Integer.class, String.class)
310 			.cacheMode(NONE)
311 			.supplier((k1, k2, k3) -> {
312 				callCount.incrementAndGet();
313 				return "value";
314 			})
315 			.build();
316 		x.get("en", "US", 1);
317 		x.get("en", "US", 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 = Cache3.of(String.class, String.class, Integer.class, String.class).build();
326 		x.get("en", "US", 1, () -> {
327 			callCount.incrementAndGet();
328 			return null;
329 		});
330 		x.get("en", "US", 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 = Cache3.of(String.class, String.class, Integer.class, String.class).build();
345 		x.put("en", "US", 1, "value1");
346 		var removed = x.remove("en", "US", 1);
347 		assertEquals("value1", removed);
348 		assertFalse(x.containsKey("en", "US", 1));
349 	}
350 
351 	@Test
352 	void d02_containsValue() {
353 		var x = Cache3.of(String.class, String.class, Integer.class, String.class).build();
354 		x.put("en", "US", 1, "value1");
355 		assertTrue(x.containsValue("value1"));
356 		assertFalse(x.containsValue("value2"));
357 	}
358 
359 	@Test
360 	void d03_containsValue_nullValue() {
361 		var x = Cache3.of(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", 1, () -> null);
364 		assertFalse(x.containsValue(null));
365 		// Also test with empty cache
366 		var x2 = Cache3.of(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 = Cache3.of(String.class, String.class, Integer.class, String.class)
377 			.logOnExit("TestCache3")
378 			.supplier((k1, k2, k3) -> k1 + ":" + k2 + ":" + k3)
379 			.build();
380 		x.get("en", "US", 1);
381 		assertSize(1, x);
382 	}
383 
384 	@Test
385 	void e02_logOnExit_withBoolean() {
386 		var x = Cache3.of(String.class, String.class, Integer.class, String.class)
387 			.logOnExit(true, "MyCache3")
388 			.supplier((k1, k2, k3) -> k1 + ":" + k2 + ":" + k3)
389 			.build();
390 		x.get("en", "US", 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 = Cache3.of(String.class, String.class, Integer.class, String.class)
402 			.threadLocal()
403 			.supplier((k1, k2, k3) -> {
404 				callCount.incrementAndGet();
405 				return k1 + ":" + k2 + ":" + k3;
406 			})
407 			.build();
408 
409 		// First call - cache miss
410 		var result1 = x.get("en", "US", 1);
411 
412 		// Second call - cache hit
413 		var result2 = x.get("en", "US", 1);
414 
415 		assertEquals("en:US:1", result1);
416 		assertEquals("en:US: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 = Cache3.of(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", 1) with its own value
432 		var future1 = java.util.concurrent.CompletableFuture.runAsync(() -> {
433 			var value = x.get("en", "US", 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", 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 = Cache3.of(String.class, String.class, Integer.class, String.class)
455 			.threadLocal()
456 			.supplier((k1, k2, k3) -> k1 + ":" + k2 + ":" + k3)
457 			.build();
458 
459 		x.get("en", "US", 1);
460 		x.get("fr", "FR", 2);
461 		x.get("de", "DE", 3);
462 
463 		assertSize(3, x);
464 		assertEquals(0, x.getCacheHits());
465 
466 		// Verify all cached
467 		assertEquals("en:US:1", x.get("en", "US", 1));
468 		assertEquals("fr:FR:2", x.get("fr", "FR", 2));
469 		assertEquals("de:DE:3", x.get("de", "DE", 3));
470 		assertEquals(3, x.getCacheHits());
471 	}
472 
473 	@Test
474 	void f04_threadLocal_clear() {
475 		var x = Cache3.of(String.class, String.class, Integer.class, String.class)
476 			.threadLocal()
477 			.supplier((k1, k2, k3) -> "value")
478 			.build();
479 
480 		x.get("en", "US", 1);
481 		x.get("fr", "FR", 2);
482 		assertSize(2, x);
483 
484 		x.clear();
485 		assertEmpty(x);
486 	}
487 
488 	@Test
489 	void f05_threadLocal_maxSize() {
490 		var x = Cache3.of(String.class, String.class, Integer.class, String.class)
491 			.threadLocal()
492 			.maxSize(2)
493 			.supplier((k1, k2, k3) -> "value")
494 			.build();
495 
496 		x.get("en", "US", 1);
497 		x.get("fr", "FR", 2);
498 		assertSize(2, x);
499 
500 		// 3rd item doesn't trigger eviction yet
501 		x.get("de", "DE", 3);
502 		assertSize(3, x);
503 
504 		// 4th item triggers eviction
505 		x.get("es", "ES", 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 = Cache3.of(String.class, String.class, Integer.class, String.class)
517 			.threadLocal()
518 			.cacheMode(WEAK)
519 			.supplier((k1, k2, k3) -> {
520 				callCount.incrementAndGet();
521 				return k1 + ":" + k2 + ":" + k3;
522 			})
523 			.build();
524 
525 		// First call - cache miss
526 		var result1 = x.get("en", "US", 1);
527 
528 		// Second call - cache hit
529 		var result2 = x.get("en", "US", 1);
530 
531 		assertEquals("en:US:1", result1);
532 		assertEquals("en:US: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 = Cache3.of(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", 1) with its own value
549 		var future1 = java.util.concurrent.CompletableFuture.runAsync(() -> {
550 			var value = x.get("en", "US", 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", 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 = Cache3.of(String.class, String.class, Integer.class, String.class)
572 			.threadLocal()
573 			.cacheMode(WEAK)
574 			.supplier((k1, k2, k3) -> "value")
575 			.build();
576 
577 		x.get("en", "US", 1);
578 		x.get("fr", "FR", 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 = Cache3.of(String.class, String.class, Integer.class, String.class)
588 			.threadLocal()
589 			.cacheMode(WEAK)
590 			.maxSize(2)
591 			.supplier((k1, k2, k3) -> "value")
592 			.build();
593 
594 		x.get("en", "US", 1);
595 		x.get("fr", "FR", 2);
596 		assertSize(2, x);
597 
598 		// 3rd item doesn't trigger eviction yet
599 		x.get("de", "DE", 3);
600 		assertSize(3, x);
601 
602 		// 4th item triggers eviction
603 		x.get("es", "ES", 4);
604 		assertSize(1, x);
605 	}
606 }
607