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.lang.TriState.*;
20  import static org.apache.juneau.commons.utils.CollectionUtils.*;
21  import static org.apache.juneau.commons.utils.Utils.*;
22  import static org.apache.juneau.junit.bct.BasicBeanConverter.*;
23  import static org.apache.juneau.junit.bct.BctAssertions.*;
24  import static org.apache.juneau.junit.bct.BctUtils.*;
25  import static org.junit.jupiter.api.Assertions.*;
26  
27  import java.time.*;
28  import java.time.format.*;
29  import java.util.*;
30  import java.util.concurrent.*;
31  
32  import org.apache.juneau.*;
33  import org.apache.juneau.junit.bct.annotations.*;
34  import org.junit.jupiter.api.*;
35  
36  /**
37   * Unit tests for BasicBeanConverter.
38   */
39  @DisplayName("BasicBeanConverter")
40  class BasicBeanConverter_Test extends TestBase {
41  
42  	//------------------------------------------------------------------------------------------------------------------
43  	// Builder Tests
44  	//------------------------------------------------------------------------------------------------------------------
45  
46  	@Nested
47  	@DisplayName("Builder")
48  	class A_builderTest extends TestBase {
49  
50  		@Test
51  		@DisplayName("a01_builder() creates new Builder instance")
52  		void a01_builder_createsNewInstance() {
53  			var builder = BasicBeanConverter.builder();
54  			assertNotNull(builder);
55  			assertInstanceOf(BasicBeanConverter.Builder.class, builder);
56  		}
57  
58  		@Test
59  		@DisplayName("a02_build() creates BasicBeanConverter instance")
60  		void a02_build_createsBasicBeanConverter() {
61  			var converter = BasicBeanConverter.builder().build();
62  			assertNotNull(converter);
63  			assertInstanceOf(BasicBeanConverter.class, converter);
64  		}
65  
66  		@Test
67  		@DisplayName("a03_defaultSettings() applies default configuration")
68  		void a03_defaultSettings_appliesDefaults() {
69  			var converter = BasicBeanConverter.builder().defaultSettings().build();
70  
71  			// Test that default stringifiers work
72  			assertEquals("test", converter.stringify("test"));
73  			assertEquals("123", converter.stringify(123));
74  			assertEquals("true", converter.stringify(true));
75  			assertEquals("[1,2,3]", converter.stringify(l(1, 2, 3)));
76  
77  			// Test that default settings are applied
78  			assertEquals("<null>", converter.stringify(null));
79  		}
80  
81  		@Test
82  		@DisplayName("a04_addStringifier() adds custom stringifier")
83  		void a04_addStringifier_addsCustomStringifier() {
84  			var converter = BasicBeanConverter.builder().defaultSettings().addStringifier(LocalDate.class, (conv, date) -> date.format(DateTimeFormatter.ISO_LOCAL_DATE)).build();
85  
86  			var date = LocalDate.of(2023, 12, 25);
87  			assertEquals("2023-12-25", converter.stringify(date));
88  		}
89  
90  		@Test
91  		@DisplayName("a05_addListifier() adds custom listifier")
92  		void a05_addListifier_addsCustomListifier() {
93  			var converter = BasicBeanConverter.builder().defaultSettings().addListifier(String.class, (conv, str) -> l((Object[])str.split(","))).build();
94  
95  			var result = converter.listify("a,b,c");
96  			assertEquals(l("a", "b", "c"), result);
97  		}
98  
99  		@Test
100 		@DisplayName("a06_addSwapper() adds custom swapper")
101 		void a06_addSwapper_addsCustomSwapper() {
102 			var converter = BasicBeanConverter.builder().defaultSettings().addSwapper(Optional.class, (conv, opt) -> ((Optional<?>)opt).orElse(null)).build();
103 
104 			assertEquals("test", converter.stringify(opt("test")));
105 			assertEquals("<null>", converter.stringify(opte()));
106 		}
107 
108 		@Test
109 		@DisplayName("a07_addPropertyExtractor() adds custom property extractor")
110 		void a07_addPropertyExtractor_addsCustomExtractor() {
111 			var extractor = new PropertyExtractor() {
112 				@Override
113 				public boolean canExtract(BeanConverter converter, Object o, String name) {
114 					return o instanceof TestBean && "custom".equals(name);
115 				}
116 
117 				@Override
118 				public Object extract(BeanConverter converter, Object o, String name) {
119 					return "custom value";
120 				}
121 			};
122 
123 			var converter = BasicBeanConverter.builder().defaultSettings().addPropertyExtractor(extractor).build();
124 
125 			var bean = new TestBean("John", 30);
126 			assertEquals("custom value", converter.getProperty(bean, "custom"));
127 		}
128 
129 		@Test
130 		@DisplayName("a08_addSetting() adds custom setting")
131 		void a08_addSetting_addsCustomSetting() {
132 			var converter = BasicBeanConverter.builder().defaultSettings().addSetting(SETTING_nullValue, "<null>").build();
133 
134 			assertEquals("<null>", converter.stringify(null));
135 		}
136 	}
137 
138 	//------------------------------------------------------------------------------------------------------------------
139 	// Core Functionality Tests
140 	//------------------------------------------------------------------------------------------------------------------
141 
142 	@Nested
143 	@DisplayName("Core Functionality")
144 	class B_coreFunctionalityTest extends TestBase {
145 
146 		private BasicBeanConverter converter;
147 
148 		@BeforeEach
149 		void setUp() {
150 			converter = BasicBeanConverter.builder().defaultSettings().build();
151 		}
152 
153 		@Test
154 		@DisplayName("b01_stringify() handles null values")
155 		void b01_stringify_handlesNull() {
156 			assertEquals("<null>", converter.stringify(null));
157 		}
158 
159 		@Test
160 		@DisplayName("b02_stringify() handles primitive types")
161 		void b02_stringify_handlesPrimitives() {
162 			assertEquals("123", converter.stringify(123));
163 			assertEquals("true", converter.stringify(true));
164 			assertEquals("3.14", converter.stringify(3.14));
165 			assertEquals("c", converter.stringify('c'));
166 		}
167 
168 		@Test
169 		@DisplayName("b03_stringify() handles strings")
170 		void b03_stringify_handlesStrings() {
171 			assertEquals("hello", converter.stringify("hello"));
172 			assertEquals("", converter.stringify(""));
173 		}
174 
175 		@Test
176 		@DisplayName("b04_stringify() handles collections")
177 		@BctConfig(sortCollections = TRUE)
178 		void b04_stringify_handlesCollections() {
179 			assertEquals("[1,2,3]", converter.stringify(l(1, 2, 3)));
180 			assertEquals("[]", converter.stringify(Collections.emptyList()));
181 			// Set converted to TreeSet for deterministic ordering
182 			var setResult = converter.stringify(Set.of("z", "a", "m"));
183 			assertEquals("[a,m,z]", setResult); // TreeSet ensures natural order
184 		}
185 
186 		@Test
187 		@DisplayName("b05_stringify() handles maps")
188 		void b05_stringify_handlesMaps() {
189 			var map = m("name", "John", "age", 30);
190 			var result = converter.stringify(map);
191 			assertTrue(result.contains("name=John"));
192 			assertTrue(result.contains("age=30"));
193 			assertTrue(result.startsWith("{"));
194 			assertTrue(result.endsWith("}"));
195 		}
196 
197 		@Test
198 		@DisplayName("b06_stringify() handles arrays")
199 		void b06_stringify_handlesArrays() {
200 			assertEquals("[1,2,3]", converter.stringify(ints(1, 2, 3)));
201 			assertEquals("[a,b,c]", converter.stringify(a("a", "b", "c")));
202 			assertEquals("[]", converter.stringify(new Object[0]));
203 		}
204 
205 		@Test
206 		@DisplayName("b07_stringify() handles dates")
207 		void b07_stringify_handlesDates() {
208 			var date = Instant.parse("2023-12-25T10:15:30Z");
209 			var result = converter.stringify(date);
210 			assertEquals("2023-12-25T10:15:30Z", result);
211 		}
212 
213 		@Test
214 		@DisplayName("b08_listify() converts arrays to lists")
215 		void b08_listify_convertsArrays() {
216 			var result = converter.listify(a("a", "b", "c"));
217 			assertEquals(l("a", "b", "c"), result);
218 		}
219 
220 		@Test
221 		@DisplayName("b09_listify() handles collections")
222 		@BctConfig(sortCollections = TRUE)
223 		void b09_listify_handlesCollections() {
224 			var set = Set.of("z", "a", "m");
225 			var result = converter.listify(set);
226 			// TreeSet conversion ensures natural ordering
227 			assertList(result, "a", "m", "z");
228 		}
229 
230 		@Test
231 		@DisplayName("b10_listify() throws IllegalArgumentException for null")
232 		void b10_listify_throwsForNull() {
233 			assertThrows(IllegalArgumentException.class, () -> converter.listify(null));
234 		}
235 
236 		@Test
237 		@DisplayName("b11_canListify() returns correct values")
238 		void b11_canListify_returnsCorrectValues() {
239 			assertTrue(converter.canListify(l(1, 2, 3)));
240 			assertTrue(converter.canListify(ints(1, 2, 3)));
241 			assertTrue(converter.canListify(Set.of("a", "b")));
242 			assertFalse(converter.canListify("string"));
243 			assertFalse(converter.canListify(null));
244 		}
245 
246 		@Test
247 		@DisplayName("b12_swap() applies swappers")
248 		void b12_swap_appliesSwappers() {
249 			// Test with Future swapper (should be in default settings)
250 			var future = CompletableFuture.completedFuture("test");
251 			var result = converter.swap(future);
252 			// Future swapper should extract the completed value
253 			assertEquals("test", result);
254 		}
255 	}
256 
257 	//------------------------------------------------------------------------------------------------------------------
258 	// Property Access Tests
259 	//------------------------------------------------------------------------------------------------------------------
260 
261 	@Nested
262 	@DisplayName("Property Access")
263 	class C_propertyAccessTest extends TestBase {
264 
265 		private BasicBeanConverter converter;
266 
267 		@BeforeEach
268 		void setUp() {
269 			converter = BasicBeanConverter.builder().defaultSettings().build();
270 		}
271 
272 		@Test
273 		@DisplayName("c01_getProperty() accesses bean properties")
274 		void c01_getProperty_accessesBeanProperties() {
275 			var bean = new TestBean("John", 30);
276 
277 			assertEquals("John", converter.getProperty(bean, "name"));
278 			assertEquals(30, converter.getProperty(bean, "age"));
279 		}
280 
281 		@Test
282 		@DisplayName("c02_getProperty() handles null objects")
283 		void c02_getProperty_handlesNull() {
284 			assertNull(converter.getProperty(null, "name"));
285 		}
286 
287 		@Test
288 		@DisplayName("c03_getProperty() throws for unknown properties")
289 		void c03_getProperty_throwsForUnknownProperties() {
290 			var bean = new TestBean("John", 30);
291 
292 			assertThrows(RuntimeException.class, () -> converter.getProperty(bean, "unknown"));
293 		}
294 	}
295 
296 	//------------------------------------------------------------------------------------------------------------------
297 	// Settings Tests
298 	//------------------------------------------------------------------------------------------------------------------
299 
300 	@Nested
301 	@DisplayName("Settings")
302 	class D_settingsTest extends TestBase {
303 
304 		@Test
305 		@DisplayName("d01_nullValue setting changes null representation")
306 		void d01_nullValue_changesNullRepresentation() {
307 			var converter = BasicBeanConverter.builder().defaultSettings().addSetting(SETTING_nullValue, "<null>").build();
308 
309 			assertEquals("<null>", converter.stringify(null));
310 		}
311 
312 		@Test
313 		@DisplayName("d02_fieldSeparator setting changes delimiter")
314 		void d02_fieldSeparator_changesDelimiter() {
315 			var converter = BasicBeanConverter.builder().defaultSettings().addSetting(SETTING_fieldSeparator, " | ").build();
316 
317 			assertEquals("[1 | 2 | 3]", converter.stringify(l(1, 2, 3)));
318 		}
319 
320 		@Test
321 		@DisplayName("d03_collection prefix/suffix settings change brackets")
322 		void d03_collectionBrackets_changeBrackets() {
323 			var converter = BasicBeanConverter.builder().defaultSettings().addSetting(SETTING_collectionPrefix, "(").addSetting(SETTING_collectionSuffix, ")").build();
324 
325 			assertEquals("(1,2,3)", converter.stringify(l(1, 2, 3)));
326 		}
327 
328 		@Test
329 		@DisplayName("d04_map prefix/suffix settings change brackets")
330 		void d04_mapBrackets_changeBrackets() {
331 			var converter = BasicBeanConverter.builder().defaultSettings().addSetting(SETTING_mapPrefix, "<").addSetting(SETTING_mapSuffix, ">").build();
332 
333 			var map = m("a", 1);
334 			var result = converter.stringify(map);
335 			assertTrue(result.startsWith("<"));
336 			assertTrue(result.endsWith(">"));
337 		}
338 
339 		@Test
340 		@DisplayName("d05_mapEntrySeparator setting changes key-value separator")
341 		void d05_mapEntrySeparator_changesKeyValueSeparator() {
342 			var converter = BasicBeanConverter.builder().defaultSettings().addSetting(SETTING_mapEntrySeparator, ":").build();
343 
344 			var map = m("name", "John");
345 			var result = converter.stringify(map);
346 			assertTrue(result.contains("name:John"));
347 		}
348 
349 		@Test
350 		@DisplayName("d06_classNameFormat setting changes class name format")
351 		void d06_classNameFormat_changesFormat() {
352 			var converter = BasicBeanConverter.builder().defaultSettings().addSetting(SETTING_classNameFormat, "full").build();
353 
354 			var bean = new TestBean("John", 30);
355 			var result = converter.stringify(bean);
356 			assertContains(getClass().getDeclaringClass().getName() + "$TestBean", result);
357 		}
358 	}
359 
360 	//------------------------------------------------------------------------------------------------------------------
361 	// Error Handling Tests
362 	//------------------------------------------------------------------------------------------------------------------
363 
364 	@Nested
365 	@DisplayName("Error Handling")
366 	class E_errorHandlingTest extends TestBase {
367 
368 		private BasicBeanConverter converter;
369 
370 		@BeforeEach
371 		void setUp() {
372 			converter = BasicBeanConverter.builder().defaultSettings().build();
373 		}
374 
375 		@Test
376 		@DisplayName("e01_getProperty() with invalid property throws RuntimeException")
377 		void e01_getProperty_invalidProperty_throwsException() {
378 			var bean = new TestBean("John", 30);
379 
380 			var ex = assertThrows(RuntimeException.class, () -> converter.getProperty(bean, "invalidProperty"));
381 			assertContains("Property 'invalidProperty' not found", ex.getMessage());
382 		}
383 	}
384 
385 	//------------------------------------------------------------------------------------------------------------------
386 	// Test Helper Classes
387 	//------------------------------------------------------------------------------------------------------------------
388 
389 	public static class TestBean {
390 		private String name;
391 		private int age;
392 
393 		public TestBean(String name, int age) {
394 			this.name = name;
395 			this.age = age;
396 		}
397 
398 		public String getName() { return name; }
399 
400 		public int getAge() { return age; }
401 
402 		void setName(String name) { this.name = name; }
403 
404 		void setAge(int age) { this.age = age; }
405 	}
406 
407 	public static class TestPerson {
408 		private String name;
409 		private int age;
410 
411 		public TestPerson(String name, int age) {
412 			this.name = name;
413 			this.age = age;
414 		}
415 
416 		public String getName() { return name; }
417 
418 		public int getAge() { return age; }
419 
420 		void setName(String name) { this.name = name; }
421 
422 		void setAge(int age) { this.age = age; }
423 	}
424 
425 	// ====================================================================================================
426 	// Enhanced Edge Case Tests
427 	// ====================================================================================================
428 
429 	@Nested
430 	class H_enhancedEdgeCases extends TestBase {
431 
432 		@Test
433 		void h01_listifyWithMixedArrayTypes() {
434 			var converter = builder().defaultSettings().build();
435 
436 			// Test different array types
437 			var stringArray = a("a", "b", "c");
438 			var intArray = ints(1, 2, 3);
439 			var booleanArray = booleans(true, false, true);
440 
441 			var stringList = converter.listify(stringArray);
442 			assertSize(3, stringList);
443 			assertEquals("a", stringList.get(0));
444 
445 			var intList = converter.listify(intArray);
446 			assertSize(3, intList);
447 			assertEquals(1, intList.get(0));
448 
449 			var booleanList = converter.listify(booleanArray);
450 			assertSize(3, booleanList);
451 			assertEquals(true, booleanList.get(0));
452 		}
453 
454 		@Test
455 		void h02_swapWithSingleRegistration() {
456 			var converter = builder().defaultSettings().addSwapper(TestPerson.class, (conv, person) -> "Person:" + person.getName()).build();
457 
458 			var person = new TestPerson("john", 30);
459 
460 			// Should apply swapper: Person -> String
461 			assertEquals("Person:john", converter.stringify(person));
462 		}
463 
464 		@Test
465 		void h03_canListifyWithEdgeCases() {
466 			var converter = builder().defaultSettings().build();
467 
468 			// Test various types that can/cannot be listified
469 			assertTrue(converter.canListify(l("a", "b")));
470 			assertTrue(converter.canListify(a("a", "b")));
471 			assertTrue(converter.canListify(Set.of("a", "b")));
472 			assertFalse(converter.canListify(null));
473 			assertFalse(converter.canListify("simple string"));
474 			assertFalse(converter.canListify(42));
475 			assertFalse(converter.canListify(new TestPerson("test", 25)));
476 		}
477 
478 		@Test
479 		void h04_performanceWithLargeObjects() {
480 			var converter = builder().defaultSettings().build();
481 
482 			// Test performance with larger objects
483 			var largeList = list();
484 			for (var i = 0; i < 1000; i++) {
485 				largeList.add("item_" + i);
486 			}
487 
488 			var start = System.nanoTime();
489 			var result = converter.stringify(largeList);
490 			var end = System.nanoTime();
491 
492 			assertNotNull(result);
493 			assertTrue(result.length() > 1000, "Should generate substantial output");
494 
495 			var durationMs = (end - start) / 1_000_000;
496 			assertTrue(durationMs < 100, "Should complete quickly for 1000 items, took: " + durationMs + "ms");
497 		}
498 
499 		@Test
500 		void h05_missingPropertyExtractorThrowsException() {
501 			// Test line 305: orElseThrow when no property extractor is found
502 			var converter = builder().build(); // No default extractors
503 			var obj = new TestPerson("John", 30);
504 
505 			// Should throw RuntimeException when no extractor can handle the property
506 			var ex = assertThrows(RuntimeException.class, () -> converter.getProperty(obj, "name"));
507 			assertContains("Could not find extractor for object of type", ex.getMessage());
508 		}
509 
510 		@Test
511 		void h06_iterationSyntaxWithCollections() {
512 			// Test lines 316-317: #{...} syntax for iterating over collections/arrays
513 			var converter = builder().defaultSettings().build();
514 
515 			// Test with list of objects
516 			var people = l(m("name", "John", "age", 30), m("name", "Jane", "age", 25));
517 
518 			assertEquals("[{John},{Jane}]", converter.getNested(people, tokenize("#{name}").get(0)));
519 			assertEquals("[{30},{25}]", converter.getNested(people, tokenize("#{age}").get(0)));
520 			assertEquals("[{John,30},{Jane,25}]", converter.getNested(people, tokenize("#{name,age}").get(0)));
521 		}
522 
523 		@Test
524 		void h07_getNested_earlyReturnConditions() {
525 			// Test line 331: early return when e == null || !token.hasNested()
526 			var converter = builder().defaultSettings().build();
527 			var obj = new HashMap<String,Object>();
528 			obj.put("key", "value");
529 			obj.put("nullKey", null);
530 
531 			// Case 1: e == null (property value is null) and no nested tokens
532 			assertEquals("<null>", converter.getNested(obj, tokenize("nullKey").get(0)));
533 
534 			// Case 2: e != null but token has no nested content
535 			assertEquals("value", converter.getNested(obj, tokenize("key").get(0)));
536 
537 			// Case 3: e == null and token has nested content (should still return early)
538 			assertEquals("<null>", converter.getNested(obj, tokenize("nullKey{nested}").get(0)));
539 		}
540 
541 		@Test
542 		void h08_interfaceBasedStringifierLookup() {
543 			// Test lines 343-344: interface checking in findStringifier()
544 
545 			// Create a custom interface and class to test interface lookup
546 			interface CustomStringifiable {
547 				String getCustomString();
548 			}
549 
550 			class CustomObject implements CustomStringifiable {
551 				@Override
552 				public String getCustomString() { return "custom"; }
553 			}
554 
555 			var converter = builder().defaultSettings().addStringifier(CustomStringifiable.class, (conv, obj) -> "CUSTOM:" + obj.getCustomString()).build();
556 
557 			// CustomObject implements CustomStringifiable, so should find the interface-based stringifier
558 			assertEquals("CUSTOM:custom", converter.stringify(new CustomObject()));
559 		}
560 
561 		@Test
562 		void h09_interfaceBasedListifierLookup() {
563 			// Test lines 357-358: interface checking in findListifier()
564 
565 			// Create an interface hierarchy where we register for a deeper interface
566 			interface BaseInterface {
567 				String getBase();
568 			}
569 
570 			interface MiddleInterface {
571 				String getMiddle();
572 			}
573 
574 			// Class that implements multiple unrelated interfaces
575 			class MultiInterfaceClass implements BaseInterface, MiddleInterface {
576 				@Override
577 				public String getBase() { return "base"; }
578 
579 				@Override
580 				public String getMiddle() { return "middle"; }
581 			}
582 
583 			var converter = builder().defaultSettings()
584 				// Register listifier only for MiddleInterface, not BaseInterface or the class
585 				.addListifier(MiddleInterface.class, (conv, obj) -> l("FROM_MIDDLE_INTERFACE", obj.getMiddle())).build();
586 
587 			// MultiInterfaceClass won't directly match, BaseInterface won't match,
588 			// but MiddleInterface will match during interface iteration
589 			var result = converter.listify(new MultiInterfaceClass());
590 			assertEquals("FROM_MIDDLE_INTERFACE", result.get(0));
591 			assertEquals("middle", result.get(1));
592 		}
593 
594 		@Test
595 		void h10_interfaceBasedSwapperLookup() {
596 			// Test lines 371-372: interface checking in findSwapper()
597 
598 			// Create multiple unrelated interfaces
599 			interface FirstInterface {
600 				String getFirst();
601 			}
602 
603 			interface SecondInterface {
604 				String getSecond();
605 			}
606 
607 			// Class that implements multiple unrelated interfaces
608 			class MultiInterfaceWrapper implements FirstInterface, SecondInterface {
609 				@Override
610 				public String getFirst() { return "first"; }
611 
612 				@Override
613 				public String getSecond() { return "second"; }
614 			}
615 
616 			var converter = builder().defaultSettings()
617 				// Register swapper only for SecondInterface, not FirstInterface or the class
618 				.addSwapper(SecondInterface.class, (conv, obj) -> "SWAPPED:" + obj.getSecond()).build();
619 
620 			// MultiInterfaceWrapper won't directly match, FirstInterface won't match,
621 			// but SecondInterface will match during interface iteration
622 			assertEquals("SWAPPED:second", converter.stringify(new MultiInterfaceWrapper()));
623 		}
624 	}
625 }