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