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.junit.bct.Utils.*;
20  import static org.junit.jupiter.api.Assertions.*;
21  
22  import java.util.*;
23  
24  import org.apache.juneau.*;
25  import org.junit.jupiter.api.*;
26  
27  /**
28   * Unit tests for the {@link BeanConverter} interface and its contract compliance.
29   *
30   * <p>This test class verifies interface implementations, contract compliance,
31   * and edge case handling for all BeanConverter implementations.</p>
32   */
33  class BeanConverter_Test extends TestBase {
34  
35  	// ====================================================================================================
36  	// Contract Compliance Tests
37  	// ====================================================================================================
38  
39  	@Nested
40  	class A_contractCompliance extends TestBase {
41  
42  		@Test
43  		void a01_basicContractVerification() {
44  			var converter = BasicBeanConverter.DEFAULT;
45  
46  			// Verify basic contract requirements
47  			assertNotNull(converter, "Default converter should not be null");
48  			assertNotNull(converter.stringify(null), "stringify(null) should return non-null");
49  			assertThrows(IllegalArgumentException.class, () -> converter.listify(null), "listify(null) should throw IllegalArgumentException");
50  			assertNotNull(converter.getNested(new Object(), tokenize("toString").get(0)), "getNested should return non-null");
51  		}
52  
53  		@Test
54  		void a02_stringifyContract() {
55  			var converter = BasicBeanConverter.DEFAULT;
56  
57  			// Test basic stringify contract
58  			assertEquals("<null>", converter.stringify(null));
59  			assertEquals("test", converter.stringify("test"));
60  			assertEquals("42", converter.stringify(42));
61  			assertEquals("true", converter.stringify(true));
62  
63  			// Test collection stringify
64  			var list = Arrays.asList("a", "b", "c");
65  			var result = converter.stringify(list);
66  			assertTrue(result.contains("a") && result.contains("b") && result.contains("c"));
67  		}
68  
69  		@Test
70  		void a03_listifyContract() {
71  			var converter = BasicBeanConverter.DEFAULT;
72  
73  			// Test basic listify contract
74  			assertThrows(IllegalArgumentException.class, () -> converter.listify(null));
75  			assertThrows(IllegalArgumentException.class, () -> converter.listify("single")); // Strings are not listifiable by default
76  
77  			// Test collection listify
78  			var input = Arrays.asList("a", "b", "c");
79  			var result = converter.listify(input);
80  			assertEquals(3, result.size());
81  			assertEquals("a", result.get(0));
82  			assertEquals("b", result.get(1));
83  			assertEquals("c", result.get(2));
84  
85  			// Test array listify
86  			String[] array = {"x", "y", "z"};
87  			var arrayResult = converter.listify(array);
88  			assertEquals(3, arrayResult.size());
89  			assertEquals("x", arrayResult.get(0));
90  		}
91  
92  		@Test
93  		void a04_getNestedContract() {
94  			var converter = BasicBeanConverter.DEFAULT;
95  
96  			// Test basic property access
97  			var bean = new TestBean("test", 42);
98  			assertEquals("test", converter.getNested(bean, tokenize("name").get(0)));
99  			assertEquals("42", converter.getNested(bean, tokenize("value").get(0)));
100 
101 			// Test toString access
102 			assertEquals(bean.toString(), converter.getNested(bean, tokenize("toString").get(0)));
103 
104 			// Test class access
105 			assertEquals("{"+bean.getClass().getSimpleName()+"}", converter.getNested(bean, tokenize("class{simpleName}").get(0)));
106 		}
107 	}
108 
109 	// ====================================================================================================
110 	// Edge Case Tests
111 	// ====================================================================================================
112 
113 	@Nested
114 	class B_edgeCases extends TestBase {
115 
116 		@Test
117 		void b01_nullInputHandling() {
118 			var converter = BasicBeanConverter.DEFAULT;
119 
120 			// All methods should handle null gracefully - except listify
121 			assertNotNull(converter.stringify(null));
122 			assertThrows(IllegalArgumentException.class, () -> converter.listify(null));
123 			assertFalse(converter.canListify(null));
124 			assertNotNull(converter.getNested(null, tokenize("anyProperty").get(0)));
125 
126 			// Null token should throw IllegalArgumentException
127 			assertThrows(IllegalArgumentException.class, () -> converter.getNested(new Object(), null));
128 		}
129 
130 		@Test
131 		void b02_emptyInputHandling() {
132 			var converter = BasicBeanConverter.DEFAULT;
133 
134 			// Empty collections
135 			assertEquals("[]", converter.stringify(new ArrayList<>()));
136 			assertEquals(0, converter.listify(new ArrayList<>()).size());
137 
138 			// Empty strings
139 			assertEquals("", converter.stringify(""));
140 			assertThrows(IllegalArgumentException.class, () -> converter.listify("")); // Strings are not listifiable by default
141 
142 			// Empty arrays
143 			assertEquals("[]", converter.stringify(new Object[0]));
144 			assertEquals(0, converter.listify(new Object[0]).size());
145 		}
146 
147 		@Test
148 		void b03_largeInputHandling() {
149 			var converter = BasicBeanConverter.DEFAULT;
150 
151 			// Large collection
152 			var largeList = new ArrayList<Integer>();
153 			for (int i = 0; i < 1000; i++) {
154 				largeList.add(i);
155 			}
156 
157 			var stringResult = converter.stringify(largeList);
158 			assertNotNull(stringResult);
159 			assertTrue(stringResult.length() > 100);
160 
161 			var listResult = converter.listify(largeList);
162 			assertEquals(1000, listResult.size());
163 		}
164 
165 		@Test
166 		void b04_specialCharacterHandling() {
167 			var converter = BasicBeanConverter.DEFAULT;
168 
169 			// Unicode characters
170 			var unicode = "测试 🎉 ñoël";
171 			assertEquals(unicode, converter.stringify(unicode));
172 
173 			// Special control characters
174 			String special = "tab\there\nnewline\rcarriage";
175 			String result = converter.stringify(special);
176 			assertNotNull(result);
177 
178 			// Null character
179 			String nullChar = "before\u0000after";
180 			assertNotNull(converter.stringify(nullChar));
181 		}
182 
183 		@Test
184 		void b05_circularReferenceHandling() {
185 			var converter = BasicBeanConverter.DEFAULT;
186 
187 			// Create circular reference
188 			var parent = new CircularTestBean("parent");
189 			var child = new CircularTestBean("child");
190 			parent.child = child;
191 			child.parent = parent;
192 
193 			// Should not cause stack overflow
194 			var result = converter.stringify(parent);
195 			assertNotNull(result);
196 
197 			// Should handle nested access on circular objects
198 			assertEquals("{child}", converter.getNested(parent, tokenize("child{name}").get(0)));
199 		}
200 	}
201 
202 	// ====================================================================================================
203 	// Implementation Verification Tests
204 	// ====================================================================================================
205 
206 	@Nested
207 	class C_implementationVerification extends TestBase {
208 
209 		@Test
210 		void c01_builderPatternVerification() {
211 			// Verify builder creates valid converter
212 			var custom = BasicBeanConverter.builder()
213 				.defaultSettings()
214 				.addSetting("nullValue", "<empty>")
215 				.build();
216 
217 			assertNotNull(custom);
218 			assertEquals("<empty>", custom.stringify(null));
219 		}
220 
221 		@Test
222 		void c02_defaultConverterImmutability() {
223 			var defaultConverter = BasicBeanConverter.DEFAULT;
224 
225 			// Multiple calls should return same instance
226 			var another = BasicBeanConverter.DEFAULT;
227 			assertSame(defaultConverter, another);
228 		}
229 
230 		@Test
231 		void c03_customSettingsPersistence() {
232 			var custom = BasicBeanConverter.builder()
233 				.defaultSettings()
234 				.addSetting("nullValue", "CUSTOM_NULL")
235 				.addSetting("selfValue", "CUSTOM_SELF")
236 				.build();
237 
238 			assertEquals("CUSTOM_NULL", custom.stringify(null));
239 
240 			// Settings should persist across calls
241 			assertEquals("CUSTOM_NULL", custom.stringify(null));
242 			assertEquals("CUSTOM_NULL", custom.stringify(null));
243 		}
244 
245 		@Test
246 		void c04_extensibilityVerification() {
247 			// Verify custom stringifiers work
248 			var custom = BasicBeanConverter.builder()
249 				.defaultSettings()
250 				.addStringifier(TestBean.class, (converter, bean) -> "CUSTOM:" + bean.name)
251 				.build();
252 
253 			var bean = new TestBean("test", 42);
254 			assertEquals("CUSTOM:test", custom.stringify(bean));
255 		}
256 	}
257 
258 	// ====================================================================================================
259 	// Error Handling Tests
260 	// ====================================================================================================
261 
262 	@Nested
263 	class D_errorHandling extends TestBase {
264 
265 		@Test
266 		void d01_invalidPropertyAccess_throwsPropertyNotFoundException() {
267 			var converter = BasicBeanConverter.DEFAULT;
268 			var bean = new TestBean("test", 42);
269 
270 			// Non-existent property should throw PropertyNotFoundException with descriptive message
271 			var ex = assertThrows(PropertyNotFoundException.class, () ->
272 				converter.getNested(bean, tokenize("nonExistentProperty").get(0)));
273 
274 			// Verify the exception message contains useful information
275 			assertTrue(ex.getMessage().contains("nonExistentProperty"));
276 			assertTrue(ex.getMessage().contains("TestBean"));
277 		}
278 
279 		@Test
280 		void d02_exceptionInPropertyAccess() {
281 			var converter = BasicBeanConverter.DEFAULT;
282 			var bean = new ExceptionThrowingBean();
283 
284 			// Exception in getter should propagate as RuntimeException (wrapped InvocationTargetException)
285 			assertThrows(RuntimeException.class, () ->
286 				converter.getNested(bean, tokenize("throwingProperty").get(0)));
287 		}
288 
289 		@Test
290 		void d03_malformedPropertyPath() {
291 			var converter = BasicBeanConverter.DEFAULT;
292 			var bean = new TestBean("test", 42);
293 
294 			// Invalid property should throw PropertyNotFoundException
295 			assertThrows(PropertyNotFoundException.class, () -> converter.getNested(bean, tokenize("invalidProperty").get(0)));
296 			// Test null token handling
297 			assertThrows(IllegalArgumentException.class, () -> converter.getNested(bean, null));
298 		}
299 
300 		@Test
301 		void d04_typeConversionErrors() {
302 			var converter = BasicBeanConverter.DEFAULT;
303 
304 			// Objects that can't be easily converted should still work
305 			var problematic = new Object() {
306 				@Override
307 				public String toString() {
308 					throw new RuntimeException("Cannot convert to string");
309 				}
310 			};
311 
312 			// Should handle toString exceptions gracefully using safeToString
313 			String result = converter.stringify(problematic);
314 			assertNotNull(result);
315 			assertTrue(result.contains("RuntimeException"));
316 			assertTrue(result.contains("Cannot convert to string"));
317 		}
318 	}
319 
320 	// ====================================================================================================
321 	// Helper Classes
322 	// ====================================================================================================
323 
324 	static class TestBean {
325 		final String name;
326 		final int value;
327 
328 		TestBean(String name, int value) {
329 			this.name = name;
330 			this.value = value;
331 		}
332 
333 		String getName() { return name; }
334 		int getValue() { return value; }
335 
336 		@Override
337 		public String toString() {
338 			return "TestBean(name=" + name + ", value=" + value + ")";
339 		}
340 	}
341 
342 	static class CircularTestBean {
343 		final String name;
344 		CircularTestBean parent;
345 		CircularTestBean child;
346 
347 		CircularTestBean(String name) {
348 			this.name = name;
349 		}
350 
351 		String getName() { return name; }
352 		CircularTestBean getParent() { return parent; }
353 		CircularTestBean getChild() { return child; }
354 	}
355 
356 	static class ExceptionThrowingBean {
357 		public String getThrowingProperty() {
358 			throw new RuntimeException("Intentional test exception");
359 		}
360 	}
361 }