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.utils.CollectionUtils.*;
20 import static org.apache.juneau.junit.bct.BctAssertions.*;
21 import static org.apache.juneau.junit.bct.BctUtils.*;
22 import static org.junit.jupiter.api.Assertions.*;
23
24 import java.util.*;
25
26 import org.apache.juneau.*;
27 import org.junit.jupiter.api.*;
28
29
30
31
32
33
34
35 class PropertyExtractor_Test extends TestBase {
36
37
38
39
40
41 @Nested
42 class A_interfaceContract extends TestBase {
43
44 @SuppressWarnings("cast")
45 @Test
46 void a01_interfaceImplementation() {
47
48 var extractor = new PropertyExtractor() {
49 @Override
50 public boolean canExtract(BeanConverter converter, Object o, String key) {
51 return true;
52 }
53
54 @Override
55 public Object extract(BeanConverter converter, Object o, String key) {
56 return "EXTRACTED:" + key;
57 }
58 };
59
60 assertNotNull(extractor);
61 assertTrue(extractor instanceof PropertyExtractor);
62 }
63
64 @Test
65 void a02_anonymousClassImplementation() {
66
67 var impl = new PropertyExtractor() {
68 @Override
69 public boolean canExtract(BeanConverter converter, Object o, String key) {
70 return o instanceof String;
71 }
72
73 @Override
74 public Object extract(BeanConverter converter, Object o, String key) {
75 return o.getClass().getSimpleName() + "." + key;
76 }
77 };
78
79 BeanConverter converter = BasicBeanConverter.DEFAULT;
80 var result = (String)impl.extract(converter, "test", "length");
81
82 assertEquals("String.length", result);
83 }
84
85 @Test
86 void a03_concreteClassImplementation() {
87
88 var concrete = new PrefixPropertyExtractor();
89
90 var converter = BasicBeanConverter.DEFAULT;
91 var result = (String)concrete.extract(converter, "test", "prop");
92
93 assertEquals("PREFIX:prop", result);
94 }
95
96 @Test
97 void a04_canExtractMethodContract() {
98 var extractor = new PropertyExtractor() {
99 @Override
100 public boolean canExtract(BeanConverter converter, Object o, String key) {
101 return true;
102 }
103
104 @Override
105 public Object extract(BeanConverter converter, Object o, String key) {
106 return key.toUpperCase();
107 }
108 };
109
110 var converter = BasicBeanConverter.DEFAULT;
111
112
113 assertTrue(extractor.canExtract(converter, "test", "any"));
114 assertTrue(extractor.canExtract(converter, null, "any"));
115 assertTrue(extractor.canExtract(converter, new Object(), "any"));
116 }
117 }
118
119
120
121
122
123 @Nested
124 class B_customImplementations extends TestBase {
125
126 @Test
127 void b01_customCanExtractLogic() {
128 var selective = new PropertyExtractor() {
129 @Override
130 public boolean canExtract(BeanConverter converter, Object o, String key) {
131 return o instanceof String && key.startsWith("str");
132 }
133
134 @Override
135 public Object extract(BeanConverter converter, Object o, String key) {
136 return "STRING_PROP:" + key;
137 }
138 };
139
140 var converter = BasicBeanConverter.DEFAULT;
141
142
143 assertTrue(selective.canExtract(converter, "test", "string"));
144 assertTrue(selective.canExtract(converter, "test", "str"));
145 assertFalse(selective.canExtract(converter, "test", "other"));
146 assertFalse(selective.canExtract(converter, 123, "string"));
147
148 assertEquals("STRING_PROP:string", selective.extract(converter, "test", "string"));
149 }
150
151 @Test
152 void b02_nullHandlingExtractor() {
153 var nullSafe = new PropertyExtractor() {
154 @Override
155 public boolean canExtract(BeanConverter converter, Object o, String key) {
156 return true;
157 }
158
159 @Override
160 public Object extract(BeanConverter converter, Object o, String key) {
161 if (o == null)
162 return "NULL_OBJECT";
163 if (key == null)
164 return "NULL_PROPERTY";
165 return o.toString() + ":" + key;
166 }
167 };
168
169 var converter = BasicBeanConverter.DEFAULT;
170
171 assertEquals("NULL_OBJECT", nullSafe.extract(converter, null, "any"));
172 assertEquals("NULL_PROPERTY", nullSafe.extract(converter, "obj", null));
173 assertEquals("test:prop", nullSafe.extract(converter, "test", "prop"));
174 }
175
176 @Test
177 void b03_typeSpecificExtractor() {
178 var numberExtractor = new PropertyExtractor() {
179 @Override
180 public boolean canExtract(BeanConverter converter, Object o, String key) {
181 return o instanceof Number;
182 }
183
184 @Override
185 public Object extract(BeanConverter converter, Object o, String key) {
186 if (o instanceof Number o2) {
187 return switch (key) {
188 case "doubled" -> o2.doubleValue() * 2;
189 case "string" -> o.toString();
190 case "type" -> o.getClass().getSimpleName();
191 default -> "UNKNOWN_PROP:" + key;
192 };
193 }
194 return "NOT_A_NUMBER";
195 }
196 };
197
198 var converter = BasicBeanConverter.DEFAULT;
199
200 assertEquals(84.0, numberExtractor.extract(converter, 42, "doubled"));
201 assertEquals("42", numberExtractor.extract(converter, 42, "string"));
202 assertEquals("Integer", numberExtractor.extract(converter, 42, "type"));
203 assertEquals("UNKNOWN_PROP:other", numberExtractor.extract(converter, 42, "other"));
204 assertEquals("NOT_A_NUMBER", numberExtractor.extract(converter, "string", "doubled"));
205 }
206 }
207
208
209
210
211
212 @Nested
213 class C_edgeCases extends TestBase {
214
215 @Test
216 void c01_exceptionHandling() {
217 var throwing = new PropertyExtractor() {
218 @Override
219 public boolean canExtract(BeanConverter converter, Object o, String key) {
220 return true;
221 }
222
223 @Override
224 public Object extract(BeanConverter converter, Object o, String key) {
225 if ("error".equals(key)) {
226 throw new RuntimeException("Intentional test exception");
227 }
228 return "SUCCESS:" + key;
229 }
230 };
231
232 var converter = BasicBeanConverter.DEFAULT;
233
234
235 assertEquals("SUCCESS:normal", throwing.extract(converter, "obj", "normal"));
236
237
238 assertThrows(RuntimeException.class, () -> throwing.extract(converter, "obj", "error"));
239 }
240
241 @Test
242 void c02_recursiveExtraction() {
243 var recursive = new PropertyExtractor() {
244 @Override
245 public boolean canExtract(BeanConverter converter, Object o, String key) {
246 return o instanceof String && "recursive".equals(key);
247 }
248
249 @Override
250 public Object extract(BeanConverter converter, Object o, String key) {
251 if ("recursive".equals(key) && o instanceof String str) {
252
253 return "RECURSIVE[" + converter.stringify(str.length()) + "]";
254 }
255 return "NON_RECURSIVE:" + key;
256 }
257 };
258
259 var converter = BasicBeanConverter.DEFAULT;
260
261 assertEquals("RECURSIVE[4]", recursive.extract(converter, "test", "recursive"));
262 assertEquals("NON_RECURSIVE:other", recursive.extract(converter, "test", "other"));
263 }
264
265 @Test
266 void c03_complexObjectExtraction() {
267 var complex = new PropertyExtractor() {
268 @Override
269 public boolean canExtract(BeanConverter converter, Object o, String key) {
270 return o instanceof Map;
271 }
272
273 @Override
274 public Object extract(BeanConverter converter, Object o, String key) {
275 if (o instanceof Map o2) {
276 return switch (key) {
277 case "keys" -> toList(o2.keySet());
278 case "values" -> toList(o2.values());
279 case "entries" -> o2.entrySet().size();
280 default -> o2.get(key);
281 };
282 }
283 return "NOT_A_MAP";
284 }
285 };
286
287 var converter = BasicBeanConverter.DEFAULT;
288 var testMap = m("a", "valueA", "b", "valueB");
289
290 var keys = (List<String>)complex.extract(converter, testMap, "keys");
291 assertSize(2, keys);
292 assertTrue(keys.contains("a"));
293 assertTrue(keys.contains("b"));
294
295 assertEquals(2, complex.extract(converter, testMap, "entries"));
296 assertEquals("valueA", complex.extract(converter, testMap, "a"));
297 assertEquals("NOT_A_MAP", complex.extract(converter, "string", "keys"));
298 }
299 }
300
301
302
303
304
305 @Nested
306 class D_integration extends TestBase {
307
308 @Test
309 void d01_integrationWithBasicBeanConverter() {
310
311 var customExtractor = new PropertyExtractor() {
312 @Override
313 public boolean canExtract(BeanConverter converter, Object o, String key) {
314 return "customProp".equals(key);
315 }
316
317 @Override
318 public Object extract(BeanConverter converter, Object o, String key) {
319 return "CUSTOM[" + o.getClass().getSimpleName() + "." + key + "]";
320 }
321 };
322
323
324 var customConverter = BasicBeanConverter.builder()
325 .defaultSettings()
326 .addPropertyExtractor(customExtractor)
327 .build();
328
329
330
331 var bean = new TestBean("test", 42);
332 var result = customConverter.getNested(bean, tokenize("customProp").get(0));
333
334
335 assertEquals("CUSTOM[TestBean.customProp]", result);
336 }
337
338 @Test
339 void d02_multipleExtractorPriority() {
340 var first = new PropertyExtractor() {
341 @Override
342 public boolean canExtract(BeanConverter converter, Object o, String key) {
343 return "first".equals(key);
344 }
345
346 @Override
347 public Object extract(BeanConverter converter, Object o, String key) {
348 return "FIRST_EXTRACTOR";
349 }
350 };
351
352 var second = new PropertyExtractor() {
353 @Override
354 public boolean canExtract(BeanConverter converter, Object o, String key) {
355 return "second".equals(key);
356 }
357
358 @Override
359 public Object extract(BeanConverter converter, Object o, String key) {
360 return "SECOND_EXTRACTOR";
361 }
362 };
363
364
365 var converter = BasicBeanConverter.builder()
366 .defaultSettings()
367 .addPropertyExtractor(first)
368 .addPropertyExtractor(second)
369 .build();
370
371
372
373 var bean = new TestBean("test", 42);
374
375 assertEquals("FIRST_EXTRACTOR", converter.getNested(bean, tokenize("first").get(0)));
376 assertEquals("SECOND_EXTRACTOR", converter.getNested(bean, tokenize("second").get(0)));
377 }
378
379 @Test
380 void d03_fallbackToDefaultExtractors() {
381 var custom = new PropertyExtractor() {
382 @Override
383 public boolean canExtract(BeanConverter converter, Object o, String key) {
384 return "custom".equals(key);
385 }
386
387 @Override
388 public Object extract(BeanConverter converter, Object o, String key) {
389 return "CUSTOM_VALUE";
390 }
391 };
392
393
394 var converter = BasicBeanConverter.builder()
395 .defaultSettings()
396 .addPropertyExtractor(custom)
397 .build();
398
399
400 var bean = new TestBean("test", 42);
401
402
403 assertEquals("CUSTOM_VALUE", converter.getNested(bean, tokenize("custom").get(0)));
404
405
406 assertEquals("test", converter.getNested(bean, tokenize("name").get(0)));
407 assertEquals("42", converter.getNested(bean, tokenize("value").get(0)));
408 }
409 }
410
411
412
413
414
415 static class PrefixPropertyExtractor implements PropertyExtractor {
416 @Override
417 public boolean canExtract(BeanConverter converter, Object o, String key) {
418 return true;
419 }
420
421 @Override
422 public Object extract(BeanConverter converter, Object o, String key) {
423 return "PREFIX:" + key;
424 }
425 }
426
427 static class TestBean {
428 final String name;
429 final int value;
430
431 TestBean(String name, int value) {
432 this.name = name;
433 this.value = value;
434 }
435
436 public String getName() { return name; }
437
438 public int getValue() { return value; }
439 }
440 }