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