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.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
38
39 @DisplayName("BasicBeanConverter")
40 class BasicBeanConverter_Test extends TestBase {
41
42
43
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
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
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
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
182 var setResult = converter.stringify(Set.of("z", "a", "m"));
183 assertEquals("[a,m,z]", setResult);
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
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
250 var future = CompletableFuture.completedFuture("test");
251 var result = converter.swap(future);
252
253 assertEquals("test", result);
254 }
255 }
256
257
258
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
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
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
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
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
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
461 assertEquals("Person:john", converter.stringify(person));
462 }
463
464 @Test
465 void h03_canListifyWithEdgeCases() {
466 var converter = builder().defaultSettings().build();
467
468
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
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
502 var converter = builder().build();
503 var obj = new TestPerson("John", 30);
504
505
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
513 var converter = builder().defaultSettings().build();
514
515
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
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
532 assertEquals("<null>", converter.getNested(obj, tokenize("nullKey").get(0)));
533
534
535 assertEquals("value", converter.getNested(obj, tokenize("key").get(0)));
536
537
538 assertEquals("<null>", converter.getNested(obj, tokenize("nullKey{nested}").get(0)));
539 }
540
541 @Test
542 void h08_interfaceBasedStringifierLookup() {
543
544
545
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
558 assertEquals("CUSTOM:custom", converter.stringify(new CustomObject()));
559 }
560
561 @Test
562 void h09_interfaceBasedListifierLookup() {
563
564
565
566 interface BaseInterface {
567 String getBase();
568 }
569
570 interface MiddleInterface {
571 String getMiddle();
572 }
573
574
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
585 .addListifier(MiddleInterface.class, (conv, obj) -> l("FROM_MIDDLE_INTERFACE", obj.getMiddle())).build();
586
587
588
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
597
598
599 interface FirstInterface {
600 String getFirst();
601 }
602
603 interface SecondInterface {
604 String getSecond();
605 }
606
607
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
618 .addSwapper(SecondInterface.class, (conv, obj) -> "SWAPPED:" + obj.getSecond()).build();
619
620
621
622 assertEquals("SWAPPED:second", converter.stringify(new MultiInterfaceWrapper()));
623 }
624 }
625 }