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