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.junit.bct;
18  
19  import static org.junit.jupiter.api.Assertions.*;
20  
21  import java.util.*;
22  import java.util.concurrent.*;
23  import java.util.function.*;
24  
25  import org.apache.juneau.*;
26  import org.junit.jupiter.api.*;
27  
28  /**
29   * Unit tests for the {@link Swapper} functional interface.
30   *
31   * <p>This test class verifies functional interface compliance, lambda compatibility,
32   * BiFunction inheritance, and integration with BasicBeanConverter.</p>
33   */
34  class Swapper_Test extends TestBase {
35  
36  	// ====================================================================================================
37  	// Functional Interface Compliance Tests
38  	// ====================================================================================================
39  
40  	@Nested
41  	class A_functionalInterfaceCompliance extends TestBase {
42  
43  		@SuppressWarnings("cast")
44  		@Test
45  		void a01_functionalInterfaceContract() {
46  			// Verify it's a proper functional interface
47  			Swapper<String> swapper = (converter, str) -> "SWAPPED:" + str;
48  
49  			assertNotNull(swapper);
50  			assertTrue(swapper instanceof BiFunction);
51  			assertTrue(swapper instanceof Swapper);
52  		}
53  
54  		@Test
55  		void a02_lambdaExpressionCompatibility() {
56  			// Test lambda expression usage
57  			Swapper<Integer> lambda = (converter, num) -> num * 2;
58  
59  			var converter = BasicBeanConverter.DEFAULT;
60  			var result = lambda.apply(converter, 21);
61  
62  			assertEquals(42, result);
63  		}
64  
65  		@Test
66  		void a03_methodReferenceCompatibility() {
67  			// Test method reference usage
68  			Swapper<String> methodRef = SwapperMethods::addPrefix;
69  
70  			var converter = BasicBeanConverter.DEFAULT;
71  			var result = methodRef.apply(converter, "test");
72  
73  			assertEquals("PREFIX:test", result);
74  		}
75  
76  		@Test
77  		void a04_biFunctionInheritance() {
78  			// Verify BiFunction methods are inherited
79  			Swapper<String> swapper = (converter, str) -> str.toUpperCase();
80  
81  			// Test BiFunction.apply method
82  			var converter = BasicBeanConverter.DEFAULT;
83  			var result = swapper.apply(converter, "test");
84  			assertEquals("TEST", result);
85  		}
86  	}
87  
88  	// ====================================================================================================
89  	// Lambda Composition Tests
90  	// ====================================================================================================
91  
92  	@Nested
93  	class B_lambdaComposition extends TestBase {
94  
95  		@Test
96  		void b01_andThenComposition() {
97  			Swapper<String> base = (converter, str) -> str.toLowerCase();
98  			Function<Object, String> postProcessor = obj -> "[" + obj.toString() + "]";
99  
100 			var composed = base.andThen(postProcessor);
101 
102 			var converter = BasicBeanConverter.DEFAULT;
103 			var result = composed.apply(converter, "TEST");
104 
105 			assertEquals("[test]", result);
106 		}
107 
108 		@Test
109 		void b02_chainedSwapping() {
110 			// Compose multiple swapping steps
111 			Swapper<String> upperCase = (converter, str) -> str.toUpperCase();
112 			Swapper<String> prefixed = (converter, str) -> "PROCESSED:" + str;
113 
114 			var converter = BasicBeanConverter.DEFAULT;
115 
116 			var upperResult = upperCase.apply(converter, "test");
117 			assertEquals("TEST", upperResult);
118 
119 			var prefixedResult = prefixed.apply(converter, "value");
120 			assertEquals("PROCESSED:value", prefixedResult);
121 		}
122 	}
123 
124 	// ====================================================================================================
125 	// Edge Case Tests
126 	// ====================================================================================================
127 
128 	@Nested
129 	class C_edgeCases extends TestBase {
130 
131 		@Test
132 		void c01_nullInputHandling() {
133 			Swapper<String> nullSafe = (converter, str) -> {
134 				if (str == null) return "NULL_INPUT";
135 				return str;
136 			};
137 
138 			var converter = BasicBeanConverter.DEFAULT;
139 			var result = nullSafe.apply(converter, null);
140 
141 			assertEquals("NULL_INPUT", result);
142 		}
143 
144 		@Test
145 		void c02_nullOutputHandling() {
146 			Swapper<String> nullReturning = (converter, str) -> {
147 				if ("return_null".equals(str)) return null;
148 				return str;
149 			};
150 
151 			var converter = BasicBeanConverter.DEFAULT;
152 
153 			assertNull(nullReturning.apply(converter, "return_null"));
154 			assertEquals("keep", nullReturning.apply(converter, "keep"));
155 		}
156 
157 		@Test
158 		void c03_exceptionHandling() {
159 			Swapper<String> throwing = (converter, str) -> {
160 				if ("ERROR".equals(str)) {
161 					throw new RuntimeException("Intentional test exception");
162 				}
163 				return str;
164 			};
165 
166 			var converter = BasicBeanConverter.DEFAULT;
167 
168 			// Normal case should work
169 			var normalResult = throwing.apply(converter, "normal");
170 			assertEquals("normal", normalResult);
171 
172 			// Exception case should throw
173 			assertThrows(RuntimeException.class, () -> throwing.apply(converter, "ERROR"));
174 		}
175 
176 		@Test
177 		void c04_typeTransformation() {
178 			Swapper<Number> numberSwapper = (converter, num) -> {
179 				if (num instanceof Integer) return num.doubleValue();
180 				if (num instanceof Double) return num.intValue();
181 				return num;
182 			};
183 
184 			var converter = BasicBeanConverter.DEFAULT;
185 
186 			assertEquals(42.0, numberSwapper.apply(converter, 42));
187 			assertEquals(3, numberSwapper.apply(converter, 3.14));
188 			assertEquals(123L, numberSwapper.apply(converter, 123L));
189 		}
190 	}
191 
192 	// ====================================================================================================
193 	// Type-Specific Tests
194 	// ====================================================================================================
195 
196 	@Nested
197 	class D_typeSpecific extends TestBase {
198 
199 		@Test
200 		void d01_optionalSwapping() {
201 			Swapper<Optional<String>> optionalSwapper = (converter, opt) -> opt.orElse("EMPTY");
202 
203 			var converter = BasicBeanConverter.DEFAULT;
204 
205 			assertEquals("value", optionalSwapper.apply(converter, Optional.of("value")));
206 			assertEquals("EMPTY", optionalSwapper.apply(converter, Optional.empty()));
207 		}
208 
209 		@Test
210 		void d02_collectionSwapping() {
211 			Swapper<Collection<?>> collectionSwapper = (converter, coll) -> {
212 				if (coll.isEmpty()) return "EMPTY_COLLECTION";
213 				return "COLLECTION[" + coll.size() + "]:" + coll.iterator().next();
214 			};
215 
216 			var converter = BasicBeanConverter.DEFAULT;
217 
218 			assertEquals("EMPTY_COLLECTION", collectionSwapper.apply(converter, Arrays.asList()));
219 			assertEquals("COLLECTION[3]:a", collectionSwapper.apply(converter, Arrays.asList("a", "b", "c")));
220 		}
221 
222 		@Test
223 		void d03_supplierSwapping() {
224 			Swapper<Supplier<?>> supplierSwapper = (converter, supplier) -> {
225 				try {
226 					return supplier.get();
227 				} catch (Exception e) {
228 					return "SUPPLIER_ERROR:" + e.getMessage();
229 				}
230 			};
231 
232 			var converter = BasicBeanConverter.DEFAULT;
233 
234 			assertEquals("success", supplierSwapper.apply(converter, () -> "success"));
235 			assertEquals("SUPPLIER_ERROR:test error",
236 				supplierSwapper.apply(converter, () -> { throw new RuntimeException("test error"); }));
237 		}
238 
239 		@Test
240 		void d04_futureSwapping() {
241 			Swapper<CompletableFuture<?>> futureSwapper = (converter, future) -> {
242 				try {
243 					if (future.isDone()) {
244 						return future.get();
245 					} else {
246 						return "<pending>";
247 					}
248 				} catch (Exception e) {
249 					return "<error:" + e.getMessage() + ">";
250 				}
251 			};
252 
253 			var converter = BasicBeanConverter.DEFAULT;
254 
255 			// Test completed future
256 			var completedFuture = CompletableFuture.completedFuture("done");
257 			assertEquals("done", futureSwapper.apply(converter, completedFuture));
258 
259 			// Test pending future
260 			var pendingFuture = new CompletableFuture<String>();
261 			assertEquals("<pending>", futureSwapper.apply(converter, pendingFuture));
262 
263 			// Test failed future
264 			var failedFuture = new CompletableFuture<String>();
265 			failedFuture.completeExceptionally(new RuntimeException("failed"));
266 			var result = (String) futureSwapper.apply(converter, failedFuture);
267 			assertTrue(result.startsWith("<error:"));
268 			assertTrue(result.contains("failed"));
269 		}
270 
271 		@Test
272 		void d05_customObjectSwapping() {
273 			Swapper<TestWrapper> wrapperSwapper = (converter, wrapper) -> wrapper.getValue();
274 
275 			var converter = BasicBeanConverter.DEFAULT;
276 			var wrapper = new TestWrapper("inner_value");
277 			var result = wrapperSwapper.apply(converter, wrapper);
278 
279 			assertEquals("inner_value", result);
280 		}
281 	}
282 
283 	// ====================================================================================================
284 	// Integration Tests
285 	// ====================================================================================================
286 
287 	@Nested
288 	class E_integration extends TestBase {
289 
290 		@Test
291 		void e01_converterIntegration() {
292 			// Test integration with custom converter
293 			var customConverter = BasicBeanConverter.builder()
294 				.defaultSettings()
295 				.addSwapper(TestWrapper.class, (converter, wrapper) -> wrapper.getValue())
296 				.build();
297 
298 			var wrapper = new TestWrapper("swapped_content");
299 			var result = customConverter.stringify(wrapper);
300 
301 			assertEquals("swapped_content", result);
302 		}
303 
304 		@Test
305 		void e02_multipleSwapperRegistration() {
306 			var customConverter = BasicBeanConverter.builder()
307 				.defaultSettings()
308 				.addSwapper(TestWrapper.class, (converter, wrapper) -> "WRAPPER:" + wrapper.getValue())
309 				.addSwapper(TestContainer.class, (converter, container) -> "CONTAINER:" + container.content)
310 				.build();
311 
312 			// Test wrapper swapper
313 			var wrapper = new TestWrapper("test");
314 			assertEquals("WRAPPER:test", customConverter.stringify(wrapper));
315 
316 			// Test container swapper
317 			var container = new TestContainer("content");
318 			assertEquals("CONTAINER:content", customConverter.stringify(container));
319 		}
320 
321 		@Test
322 		void e03_converterPassthrough() {
323 			// Test that converter parameter is properly passed
324 			Swapper<TestContainer> containerSwapper = (converter, container) -> {
325 				// Use the converter parameter to stringify the content
326 				return "CONTAINER[" + converter.stringify(container.content) + "]";
327 			};
328 
329 			var converter = BasicBeanConverter.DEFAULT;
330 			var container = new TestContainer("inner_content");
331 			var result = containerSwapper.apply(converter, container);
332 
333 			assertEquals("CONTAINER[inner_content]", result);
334 		}
335 
336 		@Test
337 		void e04_nestedConverterCalls() {
338 			// Test swapper that makes nested converter calls
339 			Swapper<TestComplexObject> complexSwapper = (converter, obj) -> {
340 				var itemsStr = converter.stringify(obj.items);
341 				return "COMPLEX{items=" + itemsStr + ", count=" + obj.items.size() + "}";
342 			};
343 
344 			var converter = BasicBeanConverter.DEFAULT;
345 			var complex = new TestComplexObject(Arrays.asList("x", "y", "z"));
346 			var result = complexSwapper.apply(converter, complex);
347 
348 			assertTrue(result.toString().contains("COMPLEX{items="));
349 			assertTrue(result.toString().contains("count=3"));
350 		}
351 
352 		@Test
353 		void e05_swapperChaining() {
354 			// Test that swapped objects can be swapped again
355 			var converter = BasicBeanConverter.builder()
356 				.defaultSettings()
357 				.addSwapper(TestWrapper.class, (conv, wrapper) -> new TestContainer(wrapper.getValue()))
358 				.addSwapper(TestContainer.class, (conv, container) -> "CHAINED:" + container.content)
359 				.build();
360 
361 			var wrapper = new TestWrapper("hello");
362 			var result = converter.stringify(wrapper);
363 
364 			// Should unwrap the wrapper, create a container, then process the container
365 			assertEquals("CHAINED:hello", result);
366 		}
367 
368 		@Test
369 		void e06_swapperWithListification() {
370 			// Test swapper integration with listification
371 			var converter = BasicBeanConverter.builder()
372 				.defaultSettings()
373 				.addSwapper(TestContainer.class, (conv, container) -> Arrays.asList(container.content, "extra"))
374 				.build();
375 
376 			var container = new TestContainer("test");
377 			var list = converter.listify(container);
378 
379 			assertEquals(2, list.size());
380 			assertEquals("test", list.get(0));
381 			assertEquals("extra", list.get(1));
382 		}
383 	}
384 
385 	// ====================================================================================================
386 	// Performance Tests
387 	// ====================================================================================================
388 
389 	@Nested
390 	class F_performance extends TestBase {
391 
392 		@Test
393 		void f01_simpleSwapperPerformance() {
394 			Swapper<String> simpleSwapper = (converter, str) -> str.toUpperCase();
395 			var converter = BasicBeanConverter.DEFAULT;
396 
397 			// Test that simple swappers execute quickly
398 			var start = System.nanoTime();
399 			for (int i = 0; i < 1000; i++) {
400 				simpleSwapper.apply(converter, "test" + i);
401 			}
402 			var end = System.nanoTime();
403 
404 			var durationMs = (end - start) / 1_000_000;
405 			assertTrue(durationMs < 100, "Simple swapper should execute quickly, took: " + durationMs + "ms");
406 		}
407 
408 		@Test
409 		void f02_complexSwapperPerformance() {
410 			Swapper<List<String>> complexSwapper = (converter, list) -> {
411 				return list.stream()
412 					.map(String::toUpperCase)
413 					.sorted()
414 					.toList();
415 			};
416 
417 			var converter = BasicBeanConverter.DEFAULT;
418 			var testList = Arrays.asList("z", "a", "m", "c", "x");
419 
420 			var start = System.nanoTime();
421 			for (int i = 0; i < 100; i++) {
422 				complexSwapper.apply(converter, testList);
423 			}
424 			var end = System.nanoTime();
425 
426 			var durationMs = (end - start) / 1_000_000;
427 			assertTrue(durationMs < 1000, "Complex swapper should complete reasonably quickly, took: " + durationMs + "ms");
428 		}
429 	}
430 
431 	// ====================================================================================================
432 	// Helper Classes and Methods
433 	// ====================================================================================================
434 
435 	static class SwapperMethods {
436 		static String addPrefix(BeanConverter converter, String str) {
437 			return "PREFIX:" + str;
438 		}
439 	}
440 
441 	static class TestWrapper {
442 		final String value;
443 
444 		TestWrapper(String value) {
445 			this.value = value;
446 		}
447 
448 		String getValue() { return value; }
449 	}
450 
451 	static class TestContainer {
452 		final String content;
453 
454 		TestContainer(String content) {
455 			this.content = content;
456 		}
457 	}
458 
459 	static class TestComplexObject {
460 		final List<String> items;
461 
462 		TestComplexObject(List<String> items) {
463 			this.items = items;
464 		}
465 	}
466 }