001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.junit.bct;
018
019import static org.apache.juneau.commons.utils.Utils.*;
020
021import java.lang.reflect.*;
022import java.util.*;
023
024import org.apache.juneau.commons.utils.*;
025
026/**
027 * Collection of standard property extractor implementations for the Bean-Centric Testing framework.
028 *
029 * <p>This class provides the built-in property extraction strategies that handle the most common
030 * object types and property access patterns. These extractors are automatically registered when
031 * using {@link BasicBeanConverter.Builder#defaultSettings()}.</p>
032 *
033 * <h5 class='section'>Extractor Hierarchy:</h5>
034 * <p>The extractors form an inheritance hierarchy for code reuse:</p>
035 * <ul>
036 *    <li><b>{@link ObjectPropertyExtractor}</b> - Base class with JavaBean property access</li>
037 *    <li><b>{@link ListPropertyExtractor}</b> - Extends ObjectPropertyExtractor with array/collection support</li>
038 *    <li><b>{@link MapPropertyExtractor}</b> - Extends ObjectPropertyExtractor with Map key access</li>
039 * </ul>
040 *
041 * <h5 class='section'>Execution Order:</h5>
042 * <p>In {@link BasicBeanConverter}, the extractors are tried in this order:</p>
043 * <ol>
044 *    <li><b>Custom extractors</b> - User-registered extractors via {@link BasicBeanConverter.Builder#addPropertyExtractor(PropertyExtractor)}</li>
045 *    <li><b>{@link ObjectPropertyExtractor}</b> - JavaBean properties, fields, and methods</li>
046 *    <li><b>{@link ListPropertyExtractor}</b> - Array/collection indices and size properties</li>
047 *    <li><b>{@link MapPropertyExtractor}</b> - Map key access and size property</li>
048 * </ol>
049 *
050 * <h5 class='section'>Property Access Strategy:</h5>
051 * <p>Each extractor implements a comprehensive fallback strategy for maximum compatibility:</p>
052 *
053 * @see PropertyExtractor
054 * @see BasicBeanConverter.Builder#defaultSettings()
055 * @see BasicBeanConverter.Builder#addPropertyExtractor(PropertyExtractor)
056 */
057public class PropertyExtractors {
058
059   /**
060    * Property extractor for array and collection objects with numeric indexing and size access.
061    *
062    * <p>This extractor extends {@link ObjectPropertyExtractor} to add special handling for
063    * collection-like objects. It provides array-style access using numeric indices and
064    * universal size/length properties for any listifiable object.</p>
065    *
066    * <h5 class='section'>Additional Properties:</h5>
067    * <ul>
068    *    <li><b>Numeric indices:</b> <js>"0"</js>, <js>"1"</js>, <js>"2"</js>, etc. for element access</li>
069    *    <li><b>Negative indices:</b> <js>"-1"</js>, <js>"-2"</js> for reverse indexing (from end)</li>
070    *    <li><b>Size properties:</b> <js>"length"</js> and <js>"size"</js> return collection size</li>
071    * </ul>
072    *
073    * <h5 class='section'>Supported Types:</h5>
074    * <p>Works with any object that can be listified by the converter:</p>
075    * <ul>
076    *    <li><b>Arrays:</b> All array types (primitive and object)</li>
077    *    <li><b>Collections:</b> List, Set, Queue, and all Collection subtypes</li>
078    *    <li><b>Iterables:</b> Any object implementing Iterable</li>
079    *    <li><b>Streams:</b> Stream objects and other lazy sequences</li>
080    *    <li><b>Maps:</b> Converted to list of entries for iteration</li>
081    * </ul>
082    *
083    * <h5 class='section'>Examples:</h5>
084    * <p class='bjava'>
085    *    <jc>// Array/List access</jc>
086    *    <jv>list</jv>.get(0)          <jc>// "0" property</jc>
087    *    <jv>array</jv>[2]             <jc>// "2" property</jc>
088    *    <jv>list</jv>.get(-1)         <jc>// "-1" property (last element)</jc>
089    *
090    *    <jc>// Size access</jc>
091    *    <jv>array</jv>.length         <jc>// "length" property</jc>
092    *    <jv>collection</jv>.size()    <jc>// "size" property</jc>
093    *    <jv>stream</jv>.count()       <jc>// "length" or "size" property</jc>
094    * </p>
095    *
096    * <p><b>Fallback:</b> If the property is not a numeric index or size property,
097    * delegates to {@link ObjectPropertyExtractor} for standard property access.</p>
098    */
099   public static class ListPropertyExtractor extends ObjectPropertyExtractor {
100
101      @Override
102      public boolean canExtract(BeanConverter converter, Object o, String name) {
103         return converter.canListify(o);
104      }
105
106      @Override
107      public Object extract(BeanConverter converter, Object o, String name) {
108         var l = converter.listify(o);
109         if (name.matches("-?\\d+")) {
110            var index = StringUtils.parseInt(name);
111            if (index < 0) {
112               index = l.size() + index; // Convert negative index to positive
113            }
114            return l.get(index);
115         }
116         if ("length".equals(name))
117            return l.size();
118         if ("size".equals(name))
119            return l.size();
120         return super.extract(converter, o, name);
121      }
122   }
123
124   /**
125    * Property extractor for Map objects with direct key access and size property.
126    *
127    * <p>This extractor extends {@link ObjectPropertyExtractor} to add special handling for
128    * Map objects. It provides direct key-based property access and a universal size
129    * property for Map objects.</p>
130    *
131    * <h5 class='section'>Map-Specific Properties:</h5>
132    * <ul>
133    *    <li><b>Direct key access:</b> Any property name that exists as a Map key</li>
134    *    <li><b>Size property:</b> {@code "size"} returns {@code map.size()}</li>
135    * </ul>
136    *
137    * <h5 class='section'>Supported Types:</h5>
138    * <p>Works with any object implementing the {@code Map} interface:</p>
139    * <ul>
140    *    <li><b>HashMap, LinkedHashMap:</b> Standard Map implementations</li>
141    *    <li><b>TreeMap, ConcurrentHashMap:</b> Specialized Map implementations</li>
142    *    <li><b>Properties:</b> Java Properties objects</li>
143    *    <li><b>Custom Maps:</b> Any Map implementation</li>
144    * </ul>
145    *
146    * <h5 class='section'>Examples:</h5>
147    * <p class='bjava'>
148    *    <jc>// Direct key access</jc>
149    *    <jv>map</jv>.get(<js>"name"</js>)       <jc>// "name" property</jc>
150    *    <jv>map</jv>.get(<js>"timeout"</js>)    <jc>// "timeout" property</jc>
151    *    <jv>props</jv>.getProperty(<js>"key"</js>) <jc>// "key" property</jc>
152    *
153    *    <jc>// Size access</jc>
154    *    <jv>map</jv>.size()           <jc>// "size" property</jc>
155    * </p>
156    *
157    * <h5 class='section'>Key Priority:</h5>
158    * <p>Map key access takes priority over JavaBean properties. If a Map contains
159    * a key with the same name as a property/method, the Map value is returned first.</p>
160    *
161    * <p><b>Fallback:</b> If the property is not found as a Map key and is not "size",
162    * delegates to {@link ObjectPropertyExtractor} for standard property access.</p>
163    */
164   public static class MapPropertyExtractor extends ObjectPropertyExtractor {
165
166      @Override
167      public boolean canExtract(BeanConverter converter, Object o, String name) {
168         return o instanceof Map;
169      }
170
171      @Override
172      public Object extract(BeanConverter converter, Object o, String name) {
173         var m = (Map<?,?>)o;
174         if (eq(name, converter.getSetting(BasicBeanConverter.SETTING_nullValue, "<null>")))
175            name = null;
176         if (m.containsKey(name))
177            return m.get(name);
178         if ("size".equals(name))
179            return m.size();
180         return super.extract(converter, o, name);
181      }
182   }
183
184   /**
185    * Standard JavaBean property extractor using reflection.
186    *
187    * <p>This extractor serves as the universal fallback for property access, implementing
188    * comprehensive JavaBean property access patterns. It tries multiple approaches to
189    * access object properties, providing maximum compatibility with different coding styles.</p>
190    *
191    * <h5 class='section'>Property Access Order:</h5>
192    * <ol>
193    *    <li><b>{@code is{Property}()}</b> - Boolean property getters (e.g., {@code isActive()})</li>
194    *    <li><b>{@code get{Property}()}</b> - Standard getter methods (e.g., {@code getName()})</li>
195    *    <li><b>{@code get(String)}</b> - Map-style property access with property name as parameter</li>
196    *    <li><b>Fields</b> - Public fields with matching names (searches inheritance hierarchy)</li>
197    *    <li><b>{@code {property}()}</b> - No-argument methods with exact property name</li>
198    * </ol>
199    *
200    * <h5 class='section'>Examples:</h5>
201    * <p class='bjava'>
202    *    <jc>// Property "name" can be accessed via:</jc>
203    *    <jv>obj</jv>.getName()        <jc>// Standard getter</jc>
204    *    <jv>obj</jv>.name             <jc>// Public field</jc>
205    *    <jv>obj</jv>.name()           <jc>// Method with property name</jc>
206    *    <jv>obj</jv>.get(<js>"name"</js>)       <jc>// Map-style getter</jc>
207    *
208    *    <jc>// Property "active" (boolean) can be accessed via:</jc>
209    *    <jv>obj</jv>.isActive()       <jc>// Boolean getter</jc>
210    *    <jv>obj</jv>.getActive()      <jc>// Standard getter alternative</jc>
211    *    <jv>obj</jv>.active           <jc>// Public field</jc>
212    * </p>
213    *
214    * <p><b>Compatibility:</b> This extractor can handle any object type, making it the
215    * universal fallback. It always returns {@code true} from {@link #canExtract(BeanConverter, Object, String)}.</p>
216    */
217   public static class ObjectPropertyExtractor implements PropertyExtractor {
218
219      @Override
220      public boolean canExtract(BeanConverter converter, Object o, String name) {
221         return true;
222      }
223
224      @SuppressWarnings("null")
225      @Override
226      public Object extract(BeanConverter converter, Object o, String name) {
227         return safe(() -> {
228            if (o == null)
229               return null;
230            var f = (Field)null;
231            var c = o.getClass();
232            var n = Character.toUpperCase(name.charAt(0)) + name.substring(1);
233            var m = Arrays.stream(c.getMethods()).filter(x -> x.getName().equals("is" + n) && x.getParameterCount() == 0).findFirst().orElse(null);
234            if (nn(m)) {
235               m.setAccessible(true);
236               return m.invoke(o);
237            }
238            if (o instanceof Map.Entry<?,?> me) {
239               // Reflection to classes inside java.util are restricted in Java 9+.
240               if ("key".equals(name))
241                  return me.getKey();
242               if ("value".equals(name))
243                  return me.getValue();
244            }
245            m = Arrays.stream(c.getMethods()).filter(x -> x.getName().equals("get" + n) && x.getParameterCount() == 0).findFirst().orElse(null);
246            if (nn(m)) {
247               m.setAccessible(true);
248               return m.invoke(o);
249            }
250            m = Arrays.stream(c.getMethods()).filter(x -> x.getName().equals("get") && x.getParameterCount() == 1 && x.getParameterTypes()[0] == String.class).findFirst().orElse(null);
251            if (nn(m)) {
252               m.setAccessible(true);
253               return m.invoke(o, name);
254            }
255            var c2 = c;
256            while (f == null && nn(c2)) {
257               f = Arrays.stream(c2.getDeclaredFields()).filter(x -> x.getName().equals(name)).findFirst().orElse(null);
258               c2 = c2.getSuperclass();
259            }
260            if (nn(f)) {
261               f.setAccessible(true);
262               return f.get(o);
263            }
264            m = Arrays.stream(c.getMethods()).filter(x -> x.getName().equals(name) && x.getParameterCount() == 0).findFirst().orElse(null);
265            if (nn(m)) {
266               m.setAccessible(true);
267               return m.invoke(o);
268            }
269            throw new PropertyNotFoundException(name, o.getClass());
270         });
271      }
272   }
273
274   private PropertyExtractors() {}
275}