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.BctAssertions.*;
20  import static org.junit.jupiter.api.Assertions.*;
21  
22  import java.util.*;
23  import java.util.concurrent.*;
24  
25  import org.apache.juneau.*;
26  import org.junit.jupiter.api.*;
27  
28  /**
29   * Unit tests for PropertyExtractors.
30   */
31  @DisplayName("PropertyExtractors")
32  class PropertyExtractors_Test extends TestBase {
33  
34  	private BasicBeanConverter converter;
35  
36  	@BeforeEach
37  	void setUp() {
38  		converter = BasicBeanConverter.builder().defaultSettings().build();
39  	}
40  
41  	//------------------------------------------------------------------------------------------------------------------
42  	// ObjectPropertyExtractor
43  	//------------------------------------------------------------------------------------------------------------------
44  
45  	@Nested
46  	@DisplayName("ObjectPropertyExtractor")
47  	class A_objectPropertyExtractorTest extends TestBase {
48  
49  		private PropertyExtractors.ObjectPropertyExtractor extractor = new PropertyExtractors.ObjectPropertyExtractor();
50  
51  		@Test
52  		@DisplayName("canExtract() - always returns true")
53  		void a01_canExtract_alwaysReturnsTrue() {
54  			assertTrue(extractor.canExtract(converter, new TestBean(), "name"));
55  			assertTrue(extractor.canExtract(converter, "string", "length"));
56  			assertTrue(extractor.canExtract(converter, 123, "value"));
57  			assertTrue(extractor.canExtract(converter, null, "anything"));
58  		}
59  
60  		@Test
61  		@DisplayName("extract() - getter methods")
62  		void a02_extract_getterMethods() {
63  			var bean = new TestBean("John", 30, true);
64  
65  			assertEquals("John", extractor.extract(converter, bean, "name"));
66  			assertEquals(30, extractor.extract(converter, bean, "age"));
67  			assertEquals(true, extractor.extract(converter, bean, "active"));
68  		}
69  
70  		@Test
71  		@DisplayName("extract() - boolean is* methods")
72  		void a03_extract_booleanIsMethods() {
73  			var bean = new TestBean("John", 30, true);
74  
75  			assertEquals(true, extractor.extract(converter, bean, "active"));
76  		}
77  
78  		@Test
79  		@DisplayName("extract() - public fields")
80  		void a04_extract_publicFields() {
81  			var bean = new TestBeanWithFields();
82  			bean.publicField = "test value";
83  
84  			assertEquals("test value", extractor.extract(converter, bean, "publicField"));
85  		}
86  
87  		@Test
88  		@DisplayName("extract() - inherited fields")
89  		void a05_extract_inheritedFields() {
90  			var bean = new ChildBeanWithFields();
91  			bean.parentField = "parent value";
92  			bean.childField = "child value";
93  
94  			assertEquals("parent value", extractor.extract(converter, bean, "parentField"));
95  			assertEquals("child value", extractor.extract(converter, bean, "childField"));
96  		}
97  
98  		@Test
99  		@DisplayName("extract() - method with property name")
100 		void a06_extract_methodWithPropertyName() {
101 			var bean = new TestBeanWithMethods();
102 
103 			assertEquals("custom method", extractor.extract(converter, bean, "customMethod"));
104 		}
105 
106 		@Test
107 		@DisplayName("extract() - Map-style get(String) method")
108 		void a07_extract_mapStyleGetter() {
109 			var bean = new TestBeanWithMapGetter();
110 
111 			assertEquals("mapped value", extractor.extract(converter, bean, "key1"));
112 		}
113 
114 		@Test
115 		@DisplayName("extract() - null object returns null")
116 		void a08_extract_nullObject() {
117 			assertNull(extractor.extract(converter, null, "anything"));
118 		}
119 
120 		@Test
121 		@DisplayName("extract() - property not found throws RuntimeException")
122 		void a09_extract_propertyNotFound() {
123 			var bean = new TestBean("John", 30, true);
124 
125 			var ex = assertThrows(RuntimeException.class, () ->
126 			extractor.extract(converter, bean, "nonExistentProperty"));
127 			assertContains("Property 'nonExistentProperty' not found on object of type TestBean", ex.getMessage());
128 		}
129 
130 		@Test
131 		@DisplayName("extract() - isX methods with parameters are ignored (line 91)")
132 		void a10_extract_isMethodsWithParametersIgnored() {
133 			var bean = new TestBeanWithParameterizedMethods();
134 
135 			// isActive() with no parameters should work
136 			assertEquals(true, extractor.extract(converter, bean, "active"));
137 
138 			// isValid(String) with parameters should be ignored and fall back to property not found
139 			var ex = assertThrows(RuntimeException.class, () ->
140 				extractor.extract(converter, bean, "valid"));
141 			assertContains("Property 'valid' not found", ex.getMessage());
142 		}
143 
144 		@Test
145 		@DisplayName("extract() - getX methods with parameters are ignored (line 96)")
146 		void a11_extract_getMethodsWithParametersIgnored() {
147 			var bean = new TestBeanWithParameterizedMethods();
148 
149 			// getName() with no parameters should work
150 			assertEquals("test", extractor.extract(converter, bean, "name"));
151 
152 			// getDescription(String) with parameters should be ignored and fall back to property not found
153 			var ex = assertThrows(RuntimeException.class, () ->
154 				extractor.extract(converter, bean, "description"));
155 			assertContains("Property 'description' not found", ex.getMessage());
156 		}
157 
158 		@Test
159 		@DisplayName("extract() - get() methods with wrong signature are ignored (line 101)")
160 		void a12_extract_getMethodsWithWrongSignatureIgnored() {
161 			var bean = new TestBeanWithInvalidGetMethods();
162 
163 			// get(String) with correct signature should work
164 			assertEquals("mapped_value", extractor.extract(converter, bean, "key"));
165 
166 			// For a property that doesn't exist, the get(String) method will still be called
167 			// but should return the result from get("nonExistent")
168 			assertEquals("mapped_value", extractor.extract(converter, bean, "nonExistent"));
169 
170 			// Test with a bean that has ONLY invalid get methods (no valid get(String))
171 			var beanWithoutValidGet = new TestBeanWithOnlyInvalidGetMethods();
172 			var ex = assertThrows(RuntimeException.class, () ->
173 				extractor.extract(converter, beanWithoutValidGet, "nonExistent"));
174 			assertContains("Property 'nonExistent' not found", ex.getMessage());
175 		}
176 	}
177 
178 	//------------------------------------------------------------------------------------------------------------------
179 	// ListPropertyExtractor
180 	//------------------------------------------------------------------------------------------------------------------
181 
182 	@Nested
183 	@DisplayName("ListPropertyExtractor")
184 	class B_listPropertyExtractorTest extends TestBase {
185 
186 		private PropertyExtractors.ListPropertyExtractor extractor = new PropertyExtractors.ListPropertyExtractor();
187 
188 		@Test
189 		@DisplayName("canExtract() - returns true for listifiable objects")
190 		void b01_canExtract_listifiableObjects() {
191 			assertTrue(extractor.canExtract(converter, Arrays.asList("a", "b", "c"), "0"));
192 			assertTrue(extractor.canExtract(converter, new String[]{"a", "b", "c"}, "1"));
193 			assertTrue(extractor.canExtract(converter, Set.of("a", "b", "c"), "size"));
194 			assertFalse(extractor.canExtract(converter, "not listifiable", "0"));
195 		}
196 
197 		@Test
198 		@DisplayName("extract() - numeric indices")
199 		void b02_extract_numericIndices() {
200 			var list = Arrays.asList("first", "second", "third");
201 
202 			assertEquals("first", extractor.extract(converter, list, "0"));
203 			assertEquals("second", extractor.extract(converter, list, "1"));
204 			assertEquals("third", extractor.extract(converter, list, "2"));
205 		}
206 
207 		@Test
208 		@DisplayName("extract() - negative indices")
209 		void b03_extract_negativeIndices() {
210 			var list = Arrays.asList("first", "second", "third");
211 
212 			assertEquals("third", extractor.extract(converter, list, "-1"));
213 			assertEquals("second", extractor.extract(converter, list, "-2"));
214 			assertEquals("first", extractor.extract(converter, list, "-3"));
215 		}
216 
217 		@Test
218 		@DisplayName("extract() - arrays")
219 		void b04_extract_arrays() {
220 			var array = new String[]{"a", "b", "c"};
221 
222 			assertEquals("a", extractor.extract(converter, array, "0"));
223 			assertEquals("b", extractor.extract(converter, array, "1"));
224 			assertEquals("c", extractor.extract(converter, array, "2"));
225 		}
226 
227 		@Test
228 		@DisplayName("extract() - length property")
229 		void b05_extract_lengthProperty() {
230 			var list = Arrays.asList("a", "b", "c");
231 			var array = new String[]{"a", "b", "c"};
232 
233 			assertEquals(3, extractor.extract(converter, list, "length"));
234 			assertEquals(3, extractor.extract(converter, array, "length"));
235 		}
236 
237 		@Test
238 		@DisplayName("extract() - size property")
239 		void b06_extract_sizeProperty() {
240 			var list = Arrays.asList("a", "b", "c");
241 			var set = Set.of("a", "b", "c");
242 
243 			assertEquals(3, extractor.extract(converter, list, "size"));
244 			assertEquals(3, extractor.extract(converter, set, "size"));
245 		}
246 
247 		@Test
248 		@DisplayName("extract() - falls back to ObjectPropertyExtractor")
249 		void b07_extract_fallbackToObjectPropertyExtractor() {
250 			var list = new ArrayList<>(Arrays.asList("a", "b", "c"));
251 
252 			// Should fall back to ArrayList.isEmpty() method
253 			assertEquals(false, extractor.extract(converter, list, "empty"));
254 		}
255 	}
256 
257 	//------------------------------------------------------------------------------------------------------------------
258 	// MapPropertyExtractor
259 	//------------------------------------------------------------------------------------------------------------------
260 
261 	@Nested
262 	@DisplayName("MapPropertyExtractor")
263 	class C_mapPropertyExtractorTest extends TestBase {
264 
265 		private PropertyExtractors.MapPropertyExtractor extractor = new PropertyExtractors.MapPropertyExtractor();
266 
267 		@Test
268 		@DisplayName("canExtract() - returns true only for Map objects")
269 		void c01_canExtract_mapObjects() {
270 			assertTrue(extractor.canExtract(converter, Map.of("key", "value"), "key"));
271 			assertTrue(extractor.canExtract(converter, new HashMap<>(), "size"));
272 			assertFalse(extractor.canExtract(converter, Arrays.asList("a", "b"), "0"));
273 			assertFalse(extractor.canExtract(converter, "not a map", "length"));
274 		}
275 
276 		@Test
277 		@DisplayName("extract() - direct key access")
278 		void c02_extract_directKeyAccess() {
279 			var map = Map.of("name", "John", "age", 30, "active", true);
280 
281 			assertEquals("John", extractor.extract(converter, map, "name"));
282 			assertEquals(30, extractor.extract(converter, map, "age"));
283 			assertEquals(true, extractor.extract(converter, map, "active"));
284 		}
285 
286 		@Test
287 		@DisplayName("extract() - size property")
288 		void c03_extract_sizeProperty() {
289 			var map = Map.of("a", 1, "b", 2, "c", 3);
290 
291 			assertEquals(3, extractor.extract(converter, map, "size"));
292 		}
293 
294 		@Test
295 		@DisplayName("extract() - empty map")
296 		void c04_extract_emptyMap() {
297 			var map = new HashMap<String, Object>();
298 
299 			assertEquals(0, extractor.extract(converter, map, "size"));
300 
301 			// Non-existent key should fall back to ObjectPropertyExtractor and throw exception
302 			var ex = assertThrows(RuntimeException.class, () ->
303 			extractor.extract(converter, map, "nonExistentKey"));
304 			assertContains("Property 'nonExistentKey' not found on object of type HashMap", ex.getMessage());
305 		}
306 
307 		@Test
308 		@DisplayName("extract() - Properties object")
309 		void c05_extract_propertiesObject() {
310 			var props = new Properties();
311 			props.setProperty("config.timeout", "5000");
312 			props.setProperty("config.enabled", "true");
313 
314 			assertEquals("5000", extractor.extract(converter, props, "config.timeout"));
315 			assertEquals("true", extractor.extract(converter, props, "config.enabled"));
316 		}
317 
318 		@Test
319 		@DisplayName("extract() - ConcurrentHashMap")
320 		void c06_extract_concurrentHashMap() {
321 			var map = new ConcurrentHashMap<String, Object>();
322 			map.put("thread-safe", true);
323 			map.put("capacity", 16);
324 
325 			assertEquals(true, extractor.extract(converter, map, "thread-safe"));
326 			assertEquals(16, extractor.extract(converter, map, "capacity"));
327 			assertEquals(2, extractor.extract(converter, map, "size"));
328 		}
329 
330 		@Test
331 		@DisplayName("extract() - key priority over JavaBean properties")
332 		void c07_extract_keyPriorityOverJavaBeanProperties() {
333 			var map = new TestMapWithMethods();
334 			map.put("customProperty", "map value");
335 
336 			// Map key should take priority over the getCustomProperty() method
337 			assertEquals("map value", extractor.extract(converter, map, "customProperty"));
338 		}
339 
340 		@Test
341 		@DisplayName("extract() - null value setting handling")
342 		void c08_extract_nullValueSettingHandling() {
343 			// Test the specific line 238 logic where property name matches nullValue setting
344 			var map = new HashMap<String, Object>();
345 			map.put(null, "null key value"); // Map with actual null key
346 			map.put("other", "other value");
347 
348 			// When property name matches the nullValue setting ("<null>"), it should be converted to null
349 			assertEquals("null key value", extractor.extract(converter, map, "<null>"));
350 
351 			// Test with custom nullValue setting
352 			var customConverter = BasicBeanConverter.builder()
353 				.defaultSettings()
354 				.addSetting(BasicBeanConverter.SETTING_nullValue, "NULL_KEY")
355 				.build();
356 
357 			var customExtractor = new PropertyExtractors.MapPropertyExtractor();
358 			map.put("NULL_KEY", "literal null key string"); // Map with literal "NULL_KEY" string
359 
360 			// When property name matches custom nullValue setting, it should look for null key
361 			assertEquals("null key value", customExtractor.extract(customConverter, map, "NULL_KEY"));
362 		}
363 
364 		@Test
365 		@DisplayName("extract() - falls back to ObjectPropertyExtractor")
366 		void c09_extract_fallbackToObjectPropertyExtractor() {
367 			var map = new TestMapWithMethods();
368 
369 			// Should fall back to HashMap.isEmpty() method since "empty" is not a key
370 			assertEquals(true, extractor.extract(converter, map, "empty"));
371 		}
372 	}
373 
374 	//------------------------------------------------------------------------------------------------------------------
375 	// Test Helper Classes
376 	//------------------------------------------------------------------------------------------------------------------
377 
378 	public static class TestBean {
379 		private String name;
380 		private int age;
381 		private boolean active;
382 
383 		public TestBean() {}
384 
385 		public TestBean(String name, int age, boolean active) {
386 			this.name = name;
387 			this.age = age;
388 			this.active = active;
389 		}
390 
391 		public String getName() { return name; }
392 		public int getAge() { return age; }
393 		public boolean isActive() { return active; }
394 
395 		void setName(String name) { this.name = name; }
396 		void setAge(int age) { this.age = age; }
397 		void setActive(boolean active) { this.active = active; }
398 	}
399 
400 	public static class TestBeanWithFields {
401 		public String publicField;
402 		@SuppressWarnings("unused")
403 		private String privateField = "private";
404 	}
405 
406 	public static class ParentBeanWithFields {
407 		public String parentField;
408 	}
409 
410 	public static class ChildBeanWithFields extends ParentBeanWithFields {
411 		public String childField;
412 	}
413 
414 	public static class TestBeanWithMethods {
415 		public String customMethod() {
416 			return "custom method";
417 		}
418 	}
419 
420 	public static class TestBeanWithMapGetter {
421 		private Map<String, String> data = Map.of("key1", "mapped value", "key2", "another value");
422 
423 		public String get(String key) {
424 			return data.get(key);
425 		}
426 	}
427 
428 	public static class TestMapWithMethods extends HashMap<String, Object> {
429 		private static final long serialVersionUID = 1L;
430 
431 		public String getCustomProperty() {
432 			return "method value";
433 		}
434 	}
435 
436 	public static class TestBeanWithParameterizedMethods {
437 		// Valid methods that should work
438 		public boolean isActive() {
439 			return true;
440 		}
441 
442 		public String getName() {
443 			return "test";
444 		}
445 
446 		// Invalid methods that should be ignored due to parameters (lines 91 & 96)
447 		public boolean isValid(String criteria) {
448 			return criteria != null;
449 		}
450 
451 		public String getDescription(String language) {
452 			return "Description in " + language;
453 		}
454 
455 		public String getDescription(String language, String format) {
456 			return "Description in " + language + " format " + format;
457 		}
458 	}
459 
460 	public static class TestBeanWithInvalidGetMethods {
461 		// Valid get(String) method that should work (line 101)
462 		public String get(String key) {
463 			return "mapped_value";
464 		}
465 
466 		// Invalid get() methods that should be ignored due to wrong signatures
467 		public String get() {
468 			return "no_parameters";
469 		}
470 
471 		public String get(int index) {
472 			return "wrong_parameter_type";
473 		}
474 
475 		public String get(String key1, String key2) {
476 			return "too_many_parameters";
477 		}
478 
479 		public Integer get(String key, boolean flag) {
480 			return 42; // Wrong return type and too many parameters
481 		}
482 	}
483 
484 	public static class TestBeanWithOnlyInvalidGetMethods {
485 		// Only invalid get() methods that should be ignored due to wrong signatures
486 		public String get() {
487 			return "no_parameters";
488 		}
489 
490 		public String get(int index) {
491 			return "wrong_parameter_type";
492 		}
493 
494 		public String get(String key1, String key2) {
495 			return "too_many_parameters";
496 		}
497 
498 		public Integer get(String key, boolean flag) {
499 			return 42; // Wrong return type and too many parameters
500 		}
501 	}
502 }