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.function.*;
23  
24  import org.apache.juneau.*;
25  import org.junit.jupiter.api.*;
26  
27  /**
28   * Unit tests for the {@link Stringifier} functional interface.
29   *
30   * <p>This test class verifies functional interface compliance, lambda compatibility,
31   * and edge case handling for Stringifier implementations.</p>
32   */
33  class Stringifier_Test extends TestBase {
34  
35  	// ====================================================================================================
36  	// Functional Interface Compliance Tests
37  	// ====================================================================================================
38  
39  	@Nested
40  	class A_functionalInterfaceCompliance extends TestBase {
41  
42  		@SuppressWarnings("cast")
43  		@Test
44  		void a01_functionalInterfaceContract() {
45  			// Verify it's a proper functional interface
46  			Stringifier<String> stringifier = (converter, obj) -> "STRINGIFIED:" + obj;
47  
48  			assertNotNull(stringifier);
49  			assertTrue(stringifier instanceof BiFunction);
50  			assertTrue(stringifier instanceof Stringifier);
51  		}
52  
53  		@Test
54  		void a02_lambdaExpressionCompatibility() {
55  			// Test lambda expression usage
56  			Stringifier<Integer> lambda = (converter, num) -> "NUMBER:" + num;
57  
58  			var converter = BasicBeanConverter.DEFAULT;
59  			var result = lambda.apply(converter, 42);
60  
61  			assertEquals("NUMBER:42", result);
62  		}
63  
64  		@Test
65  		void a03_methodReferenceCompatibility() {
66  			// Test method reference usage
67  			Stringifier<String> methodRef = StringifierMethods::addPrefix;
68  
69  			var converter = BasicBeanConverter.DEFAULT;
70  			var result = methodRef.apply(converter, "test");
71  
72  			assertEquals("PREFIX:test", result);
73  		}
74  
75  		@Test
76  		void a04_biFunctionInheritance() {
77  			// Verify BiFunction methods are inherited
78  			Stringifier<String> stringifier = (converter, str) -> str.toUpperCase();
79  
80  			// Test BiFunction.apply method
81  			var converter = BasicBeanConverter.DEFAULT;
82  			var result = stringifier.apply(converter, "test");
83  			assertEquals("TEST", result);
84  		}
85  	}
86  
87  	// ====================================================================================================
88  	// Lambda Composition Tests
89  	// ====================================================================================================
90  
91  	@Nested
92  	class B_lambdaComposition extends TestBase {
93  
94  		@Test
95  		void b01_andThenComposition() {
96  			Stringifier<String> base = (converter, str) -> str.toLowerCase();
97  			Function<String, String> postProcessor = s -> "[" + s + "]";
98  
99  			BiFunction<BeanConverter, String, String> composed = base.andThen(postProcessor);
100 
101 			var converter = BasicBeanConverter.DEFAULT;
102 			var result = composed.apply(converter, "TEST");
103 
104 			assertEquals("[test]", result);
105 		}
106 
107 		@Test
108 		void b02_functionalComposition() {
109 			// Compose multiple stringification steps
110 			Stringifier<String> upperCase = (converter, str) -> str.toUpperCase();
111 			Stringifier<String> prefixed = (converter, str) -> "PROCESSED:" + str;
112 
113 			BeanConverter converter = BasicBeanConverter.DEFAULT;
114 
115 			var upperResult = upperCase.apply(converter, "test");
116 			assertEquals("TEST", upperResult);
117 
118 			var prefixedResult = prefixed.apply(converter, "value");
119 			assertEquals("PROCESSED:value", prefixedResult);
120 		}
121 	}
122 
123 	// ====================================================================================================
124 	// Edge Case Tests
125 	// ====================================================================================================
126 
127 	@Nested
128 	class C_edgeCases extends TestBase {
129 
130 		@Test
131 		void c01_nullInputHandling() {
132 			Stringifier<String> nullSafe = (converter, str) -> {
133 				if (str == null) return "NULL_INPUT";
134 				return str;
135 			};
136 
137 			var converter = BasicBeanConverter.DEFAULT;
138 			var result = nullSafe.apply(converter, null);
139 
140 			assertEquals("NULL_INPUT", result);
141 		}
142 
143 		@Test
144 		void c02_emptyStringHandling() {
145 			Stringifier<String> emptyHandler = (converter, str) -> {
146 				if (str.isEmpty()) return "EMPTY_STRING";
147 				return str;
148 			};
149 
150 			var converter = BasicBeanConverter.DEFAULT;
151 			var result = emptyHandler.apply(converter, "");
152 
153 			assertEquals("EMPTY_STRING", result);
154 		}
155 
156 		@Test
157 		void c03_exceptionHandling() {
158 			Stringifier<String> throwing = (converter, str) -> {
159 				if ("ERROR".equals(str)) {
160 					throw new RuntimeException("Intentional test exception");
161 				}
162 				return str;
163 			};
164 
165 			var converter = BasicBeanConverter.DEFAULT;
166 
167 			// Normal case should work
168 			var normalResult = throwing.apply(converter, "normal");
169 			assertEquals("normal", normalResult);
170 
171 			// Exception case should throw
172 			assertThrows(RuntimeException.class, () -> throwing.apply(converter, "ERROR"));
173 		}
174 
175 		@Test
176 		void c04_specialCharacterHandling() {
177 			Stringifier<String> specialHandler = (converter, str) -> {
178 				return str.replace("\n", "\\n")
179 					.replace("\t", "\\t")
180 					.replace("\r", "\\r");
181 			};
182 
183 			var converter = BasicBeanConverter.DEFAULT;
184 			var result = specialHandler.apply(converter, "line1\nline2\tcolumn");
185 
186 			assertEquals("line1\\nline2\\tcolumn", result);
187 		}
188 
189 		@Test
190 		void c05_unicodeHandling() {
191 			Stringifier<String> unicodeHandler = (converter, str) -> "UNICODE:" + str;
192 
193 			var converter = BasicBeanConverter.DEFAULT;
194 			var result = unicodeHandler.apply(converter, "测试 🎉 ñoël");
195 
196 			assertEquals("UNICODE:测试 🎉 ñoël", result);
197 		}
198 	}
199 
200 	// ====================================================================================================
201 	// Type-Specific Tests
202 	// ====================================================================================================
203 
204 	@Nested
205 	class D_typeSpecific extends TestBase {
206 
207 		@Test
208 		void d01_numberStringification() {
209 			Stringifier<Number> numberFormatter = (converter, num) -> {
210 				if (num instanceof Integer) return "INT:" + num;
211 				if (num instanceof Double) return "DOUBLE:" + String.format("%.2f", num);
212 				return "NUMBER:" + num;
213 			};
214 
215 			var converter = BasicBeanConverter.DEFAULT;
216 
217 			assertEquals("INT:42", numberFormatter.apply(converter, 42));
218 			assertEquals("DOUBLE:3.14", numberFormatter.apply(converter, 3.14159));
219 			assertEquals("NUMBER:123", numberFormatter.apply(converter, 123L));
220 		}
221 
222 		@Test
223 		void d02_collectionStringification() {
224 			Stringifier<Collection<?>> collectionFormatter = (converter, coll) -> {
225 				if (coll.isEmpty()) return "EMPTY_COLLECTION";
226 				return "COLLECTION[" + coll.size() + "]:" +
227 				coll.stream().map(Object::toString).reduce("", (a, b) -> a + "," + b);
228 			};
229 
230 			var converter = BasicBeanConverter.DEFAULT;
231 
232 			assertEquals("EMPTY_COLLECTION", collectionFormatter.apply(converter, Arrays.asList()));
233 			assertEquals("COLLECTION[3]:,a,b,c", collectionFormatter.apply(converter, Arrays.asList("a", "b", "c")));
234 		}
235 
236 		@Test
237 		void d03_booleanStringification() {
238 			Stringifier<Boolean> booleanFormatter = (converter, bool) -> bool ? "YES" : "NO";
239 
240 			var converter = BasicBeanConverter.DEFAULT;
241 
242 			assertEquals("YES", booleanFormatter.apply(converter, true));
243 			assertEquals("NO", booleanFormatter.apply(converter, false));
244 		}
245 
246 		@Test
247 		void d04_customObjectStringification() {
248 			Stringifier<TestPerson> personFormatter = (converter, person) -> String.format("Person{name='%s', age=%d}", person.name, person.age);
249 
250 			var converter = BasicBeanConverter.DEFAULT;
251 			var person = new TestPerson("Alice", 30);
252 			var result = personFormatter.apply(converter, person);
253 
254 			assertEquals("Person{name='Alice', age=30}", result);
255 		}
256 	}
257 
258 	// ====================================================================================================
259 	// Integration Tests
260 	// ====================================================================================================
261 
262 	@Nested
263 	class E_integration extends TestBase {
264 
265 		@Test
266 		void e01_converterIntegration() {
267 			// Test integration with custom converter
268 			var customConverter = BasicBeanConverter.builder()
269 				.defaultSettings()
270 				.addStringifier(TestPerson.class, (converter, person) ->
271 				"CUSTOM:" + person.name + ":" + person.age)
272 				.build();
273 
274 			var person = new TestPerson("Bob", 25);
275 			var result = customConverter.stringify(person);
276 
277 			assertEquals("CUSTOM:Bob:25", result);
278 		}
279 
280 		@Test
281 		void e02_multipleStringifierRegistration() {
282 			var customConverter = BasicBeanConverter.builder()
283 				.defaultSettings()
284 				.addStringifier(String.class, (converter, str) -> "STR:" + str)
285 				.addStringifier(Integer.class, (converter, num) -> "INT:" + num)
286 				.build();
287 
288 			// Test string stringifier
289 			assertEquals("STR:test", customConverter.stringify("test"));
290 
291 			// Test integer stringifier
292 			assertEquals("INT:42", customConverter.stringify(42));
293 		}
294 
295 		@Test
296 		void e03_converterPassthrough() {
297 			// Test that converter parameter is properly passed
298 			Stringifier<List<?>> listStringifier = (converter, list) -> {
299 				// Use the converter parameter to stringify elements
300 				StringBuilder sb = new StringBuilder("[");
301 				for (int i = 0; i < list.size(); i++) {
302 					if (i > 0) sb.append(",");
303 					sb.append(converter.stringify(list.get(i)));
304 				}
305 				sb.append("]");
306 				return sb.toString();
307 			};
308 
309 			var converter = BasicBeanConverter.DEFAULT;
310 			var testList = Arrays.asList("a", 42, true);
311 			var result = listStringifier.apply(converter, testList);
312 
313 			assertEquals("[a,42,true]", result);
314 		}
315 
316 		@Test
317 		void e04_nestedConverterCalls() {
318 			// Test stringifier that makes nested converter calls
319 			Stringifier<TestContainer> containerStringifier = (converter, container) -> {
320 				String itemsStr = converter.stringify(container.items);
321 				return "Container{items=" + itemsStr + ", count=" + container.items.size() + "}";
322 			};
323 
324 			var converter = BasicBeanConverter.DEFAULT;
325 			var container = new TestContainer(Arrays.asList("x", "y", "z"));
326 			var result = containerStringifier.apply(converter, container);
327 
328 			assertTrue(result.contains("Container{items="));
329 			assertTrue(result.contains("count=3"));
330 		}
331 	}
332 
333 	// ====================================================================================================
334 	// Performance Tests
335 	// ====================================================================================================
336 
337 	@Nested
338 	class F_performance extends TestBase {
339 
340 		@Test
341 		void f01_performanceWithLargeStrings() {
342 			Stringifier<String> largeStringHandler = (converter, str) -> {
343 				StringBuilder sb = new StringBuilder();
344 				for (int i = 0; i < 1000; i++) {
345 					sb.append(str).append("_").append(i);
346 					if (i < 999) sb.append(",");
347 				}
348 				return sb.toString();
349 			};
350 
351 			var converter = BasicBeanConverter.DEFAULT;
352 
353 			var start = System.currentTimeMillis();
354 			var result = largeStringHandler.apply(converter, "test");
355 			var end = System.currentTimeMillis();
356 
357 			assertTrue(result.length() > 8000, "Should generate a large string (actual: " + result.length() + ")");
358 			assertTrue(end - start < 1000, "Should complete within 1 second");
359 		}
360 
361 		@Test
362 		void f02_memoryEfficiency() {
363 			Stringifier<Integer> memoryTest = (converter, num) -> {
364 				// Create a reasonably large string
365 				StringBuilder sb = new StringBuilder();
366 				for (int i = 0; i < num; i++) {
367 					sb.append("item_").append(i);
368 					if (i < num - 1) sb.append(",");
369 				}
370 				return sb.toString();
371 			};
372 
373 			var converter = BasicBeanConverter.DEFAULT;
374 			var result = memoryTest.apply(converter, 100);
375 
376 			assertTrue(result.startsWith("item_0"));
377 			assertTrue(result.endsWith("item_99"));
378 			assertTrue(result.contains(","));
379 		}
380 	}
381 
382 	// ====================================================================================================
383 	// Helper Classes and Methods
384 	// ====================================================================================================
385 
386 	static class StringifierMethods {
387 		static String addPrefix(BeanConverter converter, String str) {
388 			return "PREFIX:" + str;
389 		}
390 	}
391 
392 	static class TestPerson {
393 		final String name;
394 		final int age;
395 
396 		TestPerson(String name, int age) {
397 			this.name = name;
398 			this.age = age;
399 		}
400 	}
401 
402 	static class TestContainer {
403 		final List<String> items;
404 
405 		TestContainer(List<String> items) {
406 			this.items = items;
407 		}
408 	}
409 }