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.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   * Unit tests for the {@link PropertyExtractor} interface.
31   *
32   * <p>This test class verifies interface contract compliance, custom implementations,
33   * and edge case handling for PropertyExtractor implementations.</p>
34   */
35  class PropertyExtractor_Test extends TestBase {
36  
37  	// ====================================================================================================
38  	// Interface Contract Tests
39  	// ====================================================================================================
40  
41  	@Nested
42  	class A_interfaceContract extends TestBase {
43  
44  		@SuppressWarnings("cast")
45  		@Test
46  		void a01_interfaceImplementation() {
47  			// Verify it's a proper interface with two methods
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  			// Test anonymous class implementation
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  			// Test concrete class implementation
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 			// canExtract method should work correctly
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 	// Custom Implementation Tests
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 			// Should extract only for String objects with "str" prefix
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; // Always can extract
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 	// Edge Case Tests
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 			// Normal case should work
235 			assertEquals("SUCCESS:normal", throwing.extract(converter, "obj", "normal"));
236 
237 			// Exception case should throw
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 						// Use the converter recursively
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 	// Integration Tests
303 	// ====================================================================================================
304 
305 	@Nested
306 	class D_integration extends TestBase {
307 
308 		@Test
309 		void d01_integrationWithBasicBeanConverter() {
310 			// Test custom extractor with BasicBeanConverter
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 			// @formatter:off
324 			var customConverter = BasicBeanConverter.builder()
325 				.defaultSettings()
326 				.addPropertyExtractor(customExtractor)
327 				.build();
328 			// @formatter:on
329 
330 			// Test that the custom extractor works
331 			var bean = new TestBean("test", 42);
332 			var result = customConverter.getNested(bean, tokenize("customProp").get(0));
333 
334 			// Should get our custom result
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 			// @formatter:off
365 			var converter = BasicBeanConverter.builder()
366 				.defaultSettings()
367 				.addPropertyExtractor(first)
368 				.addPropertyExtractor(second)
369 				.build();
370 			// @formatter:on
371 
372 			// Test that each extractor handles its specific properties
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 			// @formatter:off
394 			var converter = BasicBeanConverter.builder()
395 				.defaultSettings()
396 				.addPropertyExtractor(custom)
397 				.build();
398 			// @formatter:on
399 
400 			var bean = new TestBean("test", 42);
401 
402 			// Custom property should use our extractor
403 			assertEquals("CUSTOM_VALUE", converter.getNested(bean, tokenize("custom").get(0)));
404 
405 			// Regular properties should use default extractors
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 	// Helper Classes
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 }