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