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