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