1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
33
34 @DisplayName("BasicBeanConverter")
35 class BasicBeanConverter_Test extends TestBase {
36
37
38
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
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
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
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
191 var setResult = converter.stringify(Set.of("z", "a", "m"));
192 assertEquals("[a,m,z]", setResult);
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
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
258 var future = CompletableFuture.completedFuture("test");
259 var result = converter.swap(future);
260
261 assertEquals("test", result);
262 }
263 }
264
265
266
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
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
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
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
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
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
488 assertEquals("Person:john", converter.stringify(person));
489 }
490
491 @Test
492 void h03_canListifyWithEdgeCases() {
493 var converter = builder().defaultSettings().build();
494
495
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
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
529 var converter = builder().build();
530 var obj = new TestPerson("John", 30);
531
532
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
541 var converter = builder().defaultSettings().build();
542
543
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
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
566 assertEquals("<null>", converter.getNested(obj, Utils.tokenize("nullKey").get(0)));
567
568
569 assertEquals("value", converter.getNested(obj, Utils.tokenize("key").get(0)));
570
571
572 assertEquals("<null>", converter.getNested(obj, Utils.tokenize("nullKey{nested}").get(0)));
573 }
574
575 @Test
576 void h08_interfaceBasedStringifierLookup() {
577
578
579
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
595 assertEquals("CUSTOM:custom", converter.stringify(new CustomObject()));
596 }
597
598 @Test
599 void h09_interfaceBasedListifierLookup() {
600
601
602
603 interface BaseInterface {
604 String getBase();
605 }
606
607 interface MiddleInterface {
608 String getMiddle();
609 }
610
611
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
622 .addListifier(MiddleInterface.class, (conv, obj) ->
623 Arrays.asList("FROM_MIDDLE_INTERFACE", obj.getMiddle()))
624 .build();
625
626
627
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
636
637
638 interface FirstInterface {
639 String getFirst();
640 }
641
642 interface SecondInterface {
643 String getSecond();
644 }
645
646
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
657 .addSwapper(SecondInterface.class, (conv, obj) -> "SWAPPED:" + obj.getSecond())
658 .build();
659
660
661
662 assertEquals("SWAPPED:second", converter.stringify(new MultiInterfaceWrapper()));
663 }
664 }
665 }