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.junit.jupiter.api.Assertions.*;
21  
22  import java.util.*;
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 Stringifier} functional interface.
30   *
31   * <p>This test class verifies functional interface compliance, lambda compatibility,
32   * and edge case handling for Stringifier implementations.</p>
33   */
34  class Stringifier_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  			Stringifier<String> stringifier = (converter, obj) -> "STRINGIFIED:" + obj;
48  
49  			assertNotNull(stringifier);
50  			assertTrue(stringifier instanceof BiFunction);
51  			assertTrue(stringifier instanceof Stringifier);
52  		}
53  
54  		@Test
55  		void a02_lambdaExpressionCompatibility() {
56  			// Test lambda expression usage
57  			Stringifier<Integer> lambda = (converter, num) -> "NUMBER:" + num;
58  
59  			var converter = BasicBeanConverter.DEFAULT;
60  			var result = lambda.apply(converter, 42);
61  
62  			assertEquals("NUMBER:42", result);
63  		}
64  
65  		@Test
66  		void a03_methodReferenceCompatibility() {
67  			// Test method reference usage
68  			Stringifier<String> methodRef = StringifierMethods::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  			Stringifier<String> stringifier = (converter, str) -> str.toUpperCase();
80  
81  			// Test BiFunction.apply method
82  			var converter = BasicBeanConverter.DEFAULT;
83  			var result = stringifier.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  			Stringifier<String> base = (converter, str) -> str.toLowerCase();
98  			Function<String, String> postProcessor = s -> "[" + s + "]";
99  
100 			BiFunction<BeanConverter, String, String> 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_functionalComposition() {
110 			// Compose multiple stringification steps
111 			Stringifier<String> upperCase = (converter, str) -> str.toUpperCase();
112 			Stringifier<String> prefixed = (converter, str) -> "PROCESSED:" + str;
113 
114 			BeanConverter 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 			Stringifier<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_emptyStringHandling() {
146 			Stringifier<String> emptyHandler = (converter, str) -> {
147 				if (str.isEmpty()) return "EMPTY_STRING";
148 				return str;
149 			};
150 
151 			var converter = BasicBeanConverter.DEFAULT;
152 			var result = emptyHandler.apply(converter, "");
153 
154 			assertEquals("EMPTY_STRING", result);
155 		}
156 
157 		@Test
158 		void c03_exceptionHandling() {
159 			Stringifier<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_specialCharacterHandling() {
178 			Stringifier<String> specialHandler = (converter, str) -> {
179 				return str.replace("\n", "\\n")
180 					.replace("\t", "\\t")
181 					.replace("\r", "\\r");
182 			};
183 
184 			var converter = BasicBeanConverter.DEFAULT;
185 			var result = specialHandler.apply(converter, "line1\nline2\tcolumn");
186 
187 			assertEquals("line1\\nline2\\tcolumn", result);
188 		}
189 
190 		@Test
191 		void c05_unicodeHandling() {
192 			Stringifier<String> unicodeHandler = (converter, str) -> "UNICODE:" + str;
193 
194 			var converter = BasicBeanConverter.DEFAULT;
195 			var result = unicodeHandler.apply(converter, "测试 🎉 ñoël");
196 
197 			assertEquals("UNICODE:测试 🎉 ñoël", result);
198 		}
199 	}
200 
201 	// ====================================================================================================
202 	// Type-Specific Tests
203 	// ====================================================================================================
204 
205 	@Nested
206 	class D_typeSpecific extends TestBase {
207 
208 		@Test
209 		void d01_numberStringification() {
210 			Stringifier<Number> numberFormatter = (converter, num) -> {
211 				if (num instanceof Integer) return "INT:" + num;
212 				if (num instanceof Double) return "DOUBLE:" + String.format("%.2f", num);
213 				return "NUMBER:" + num;
214 			};
215 
216 			var converter = BasicBeanConverter.DEFAULT;
217 
218 			assertEquals("INT:42", numberFormatter.apply(converter, 42));
219 			assertEquals("DOUBLE:3.14", numberFormatter.apply(converter, 3.14159));
220 			assertEquals("NUMBER:123", numberFormatter.apply(converter, 123L));
221 		}
222 
223 		@Test
224 		void d02_collectionStringification() {
225 			Stringifier<Collection<?>> collectionFormatter = (converter, coll) -> {
226 				if (coll.isEmpty()) return "EMPTY_COLLECTION";
227 				return "COLLECTION[" + coll.size() + "]:" +
228 				coll.stream().map(Object::toString).reduce("", (a, b) -> a + "," + b);
229 			};
230 
231 			var converter = BasicBeanConverter.DEFAULT;
232 
233 			assertEquals("EMPTY_COLLECTION", collectionFormatter.apply(converter, l()));
234 			assertEquals("COLLECTION[3]:,a,b,c", collectionFormatter.apply(converter, l("a", "b", "c")));
235 		}
236 
237 		@Test
238 		void d03_booleanStringification() {
239 			Stringifier<Boolean> booleanFormatter = (converter, bool) -> bool ? "YES" : "NO";
240 
241 			var converter = BasicBeanConverter.DEFAULT;
242 
243 			assertEquals("YES", booleanFormatter.apply(converter, true));
244 			assertEquals("NO", booleanFormatter.apply(converter, false));
245 		}
246 
247 		@Test
248 		void d04_customObjectStringification() {
249 			Stringifier<TestPerson> personFormatter = (converter, person) -> String.format("Person{name='%s', age=%d}", person.name, person.age);
250 
251 			var converter = BasicBeanConverter.DEFAULT;
252 			var person = new TestPerson("Alice", 30);
253 			var result = personFormatter.apply(converter, person);
254 
255 			assertEquals("Person{name='Alice', age=30}", result);
256 		}
257 	}
258 
259 	// ====================================================================================================
260 	// Integration Tests
261 	// ====================================================================================================
262 
263 	@Nested
264 	class E_integration extends TestBase {
265 
266 		@Test
267 		void e01_converterIntegration() {
268 			// Test integration with custom converter
269 			var customConverter = BasicBeanConverter.builder()
270 				.defaultSettings()
271 				.addStringifier(TestPerson.class, (converter, person) ->
272 				"CUSTOM:" + person.name + ":" + person.age)
273 				.build();
274 
275 			var person = new TestPerson("Bob", 25);
276 			var result = customConverter.stringify(person);
277 
278 			assertEquals("CUSTOM:Bob:25", result);
279 		}
280 
281 		@Test
282 		void e02_multipleStringifierRegistration() {
283 			var customConverter = BasicBeanConverter.builder()
284 				.defaultSettings()
285 				.addStringifier(String.class, (converter, str) -> "STR:" + str)
286 				.addStringifier(Integer.class, (converter, num) -> "INT:" + num)
287 				.build();
288 
289 			// Test string stringifier
290 			assertEquals("STR:test", customConverter.stringify("test"));
291 
292 			// Test integer stringifier
293 			assertEquals("INT:42", customConverter.stringify(42));
294 		}
295 
296 		@Test
297 		void e03_converterPassthrough() {
298 			// Test that converter parameter is properly passed
299 			Stringifier<List<?>> listStringifier = (converter, list) -> {
300 				// Use the converter parameter to stringify elements
301 				var sb = new StringBuilder("[");
302 				for (var i = 0; i < list.size(); i++) {
303 					if (i > 0) sb.append(",");
304 					sb.append(converter.stringify(list.get(i)));
305 				}
306 				sb.append("]");
307 				return sb.toString();
308 			};
309 
310 			var converter = BasicBeanConverter.DEFAULT;
311 			var testList = l("a", 42, true);
312 			var result = listStringifier.apply(converter, testList);
313 
314 			assertEquals("[a,42,true]", result);
315 		}
316 
317 		@Test
318 		void e04_nestedConverterCalls() {
319 			// Test stringifier that makes nested converter calls
320 			Stringifier<TestContainer> containerStringifier = (converter, container) -> {
321 				String itemsStr = converter.stringify(container.items);
322 				return "Container{items=" + itemsStr + ", count=" + container.items.size() + "}";
323 			};
324 
325 			var converter = BasicBeanConverter.DEFAULT;
326 			var container = new TestContainer(l("x", "y", "z"));
327 			var result = containerStringifier.apply(converter, container);
328 
329 			assertTrue(result.contains("Container{items="));
330 			assertTrue(result.contains("count=3"));
331 		}
332 	}
333 
334 	// ====================================================================================================
335 	// Performance Tests
336 	// ====================================================================================================
337 
338 	@Nested
339 	class F_performance extends TestBase {
340 
341 		@Test
342 		void f01_performanceWithLargeStrings() {
343 			Stringifier<String> largeStringHandler = (converter, str) -> {
344 				var sb = new StringBuilder();
345 				for (var i = 0; i < 1000; i++) {
346 					sb.append(str).append("_").append(i);
347 					if (i < 999) sb.append(",");
348 				}
349 				return sb.toString();
350 			};
351 
352 			var converter = BasicBeanConverter.DEFAULT;
353 
354 			var start = System.currentTimeMillis();
355 			var result = largeStringHandler.apply(converter, "test");
356 			var end = System.currentTimeMillis();
357 
358 			assertTrue(result.length() > 8000, "Should generate a large string (actual: " + result.length() + ")");
359 			assertTrue(end - start < 1000, "Should complete within 1 second");
360 		}
361 
362 		@Test
363 		void f02_memoryEfficiency() {
364 			Stringifier<Integer> memoryTest = (converter, num) -> {
365 				// Create a reasonably large string
366 				var sb = new StringBuilder();
367 				for (var i = 0; i < num; i++) {
368 					sb.append("item_").append(i);
369 					if (i < num - 1) sb.append(",");
370 				}
371 				return sb.toString();
372 			};
373 
374 			var converter = BasicBeanConverter.DEFAULT;
375 			var result = memoryTest.apply(converter, 100);
376 
377 			assertTrue(result.startsWith("item_0"));
378 			assertTrue(result.endsWith("item_99"));
379 			assertTrue(result.contains(","));
380 		}
381 	}
382 
383 	// ====================================================================================================
384 	// Helper Classes and Methods
385 	// ====================================================================================================
386 
387 	static class StringifierMethods {
388 		static String addPrefix(BeanConverter converter, String str) {
389 			return "PREFIX:" + str;
390 		}
391 	}
392 
393 	static class TestPerson {
394 		final String name;
395 		final int age;
396 
397 		TestPerson(String name, int age) {
398 			this.name = name;
399 			this.age = age;
400 		}
401 	}
402 
403 	static class TestContainer {
404 		final List<String> items;
405 
406 		TestContainer(List<String> items) {
407 			this.items = items;
408 		}
409 	}
410 }