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.marshall.objecttools;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.io.*;
022import java.lang.reflect.*;
023import java.util.*;
024import java.util.function.*;
025
026import org.apache.juneau.commons.reflect.*;
027import org.apache.juneau.marshall.json5.*;
028import org.apache.juneau.marshall.parser.*;
029
030/**
031 * POJO method introspector.
032 *
033 * <p>
034 *    This class is used to invoke methods on {@code Objects} using arguments in serialized form.
035 * </p>
036 *
037 * <h5 class='section'>Security - secure by default:</h5>
038 * <p>
039 *    As of Juneau 10.0, reflective method dispatch is <b>denied by default</b>.  Before calling
040 *    {@link #invokeMethod(String, String) invokeMethod(...)} (in any of its overloaded forms), the caller must
041 *    explicitly allow-list the method(s) that may be invoked via {@link #allow(Class, String...) allow(...)} or
042 *    {@link #allow(Predicate) allow(Predicate)}.  Invoking a method that isn't allow-listed throws
043 *    {@link MethodNotAllowlistedException}.
044 * </p>
045 * <p>
046 *    For trusted, in-process callers that need the pre-10.0 behavior of dispatching to any public,
047 *    non-deprecated method, call {@link #allowAll()} as a one-line migration.  <b>Never</b> call
048 *    {@link #allowAll()} on an introspector whose method name/arguments are derived from an untrusted
049 *    source (e.g. a REST request parameter) &mdash; allow-list the specific methods instead.
050 * </p>
051 *
052 * <h5 class='section'>Example:</h5>
053 * <p class='bjava'>
054 *    String <jv>string1</jv> = <js>"foobar"</js>;
055 *    String <jv>string2</jv> = ObjectIntrospector
056 *       .create(<jv>string1</jv>)
057 *       .allow(String.<jk>class</jk>, <js>"substring(int,int)"</js>)  <jc>// Explicit allow-list.</jc>
058 *       .invokeMethod(String.<jk>class</jk>, <js>"substring(int,int)"</js>, <js>"[3,6]"</js>);  <jc>// "bar"</jc>
059 * </p>
060 * <p>
061 *    For trusted, in-process-only use, {@link #allowAll()} can be used instead of an explicit allow-list:
062 * </p>
063 * <p class='bjava'>
064 *    String <jv>string2</jv> = ObjectIntrospector
065 *       .create(<jv>string1</jv>)
066 *       .allowAll()
067 *       .invokeMethod(String.<jk>class</jk>, <js>"substring(int,int)"</js>, <js>"[3,6]"</js>);  <jc>// "bar"</jc>
068 * </p>
069 * <p>
070 *    The arguments passed to the identified method are POJOs serialized in JSON format.  Arbitrarily complex arguments can be passed
071 *    in as arguments.
072 * </p>
073 * <ul>
074 *    <li class='warn'>This is an extremely powerful but potentially dangerous tool.  Use wisely.
075 * </ul>
076 *
077 */
078public class ObjectIntrospector {
079
080   /**
081    * Static creator.
082    * @param o The object on which Java methods will be invoked.
083    * @return A new {@link ObjectIntrospector} object.
084    */
085   public static ObjectIntrospector create(Object o) {
086      return new ObjectIntrospector(o);
087   }
088
089   /**
090    * Static creator.
091    * @param o The object on which Java methods will be invoked.
092    * @param parser The parser to use to parse the method arguments.  If <jk>null</jk>, {@link Json5Parser#DEFAULT} is used.
093    * @return A new {@link ObjectIntrospector} object.
094    */
095   public static ObjectIntrospector create(Object o, ReaderParser parser) {
096      return new ObjectIntrospector(o, parser);
097   }
098
099   private final Object object;
100   private final ReaderParser parser;
101
102   /** Allow-list filter.  <jk>null</jk> means no methods are allow-listed (secure default = deny-all). */
103   private Predicate<Method> allowed;
104
105   /**
106    * Shortcut for calling <code><jk>new</jk> ObjectIntrospector(o, <jk>null</jk>);</code>
107    *
108    * @param o The object on which Java methods will be invoked.
109    */
110   public ObjectIntrospector(Object o) {
111      this(o, null);
112   }
113
114   /**
115    * Constructor.
116    *
117    * @param object The object on which Java methods will be invoked.  Can be <jk>null</jk> (subsequent method invocations return <jk>null</jk>).
118    * @param parser The parser to use to parse the method arguments.
119    * If <jk>null</jk>, {@link Json5Parser#DEFAULT} is used.
120    */
121   public ObjectIntrospector(Object object, ReaderParser parser) {
122      if (parser == null)
123         parser = Json5Parser.DEFAULT;
124      this.object = object;
125      this.parser = parser;
126   }
127
128   /**
129    * Allow-lists methods matching the specified filter for invocation via {@link #invokeMethod}.
130    *
131    * <p>
132    * Can be called multiple times; the filters are OR'ed together, so a method is allowed if it matches
133    * <b>any</b> filter that was added.
134    *
135    * @param filter Filter that returns <jk>true</jk> for methods that may be invoked.
136    * @return This object.
137    */
138   public ObjectIntrospector allow(Predicate<Method> filter) {
139      if (filter != null)
140         allowed = (allowed == null) ? filter : allowed.or(filter);
141      return this;
142   }
143
144   /**
145    * Allow-lists specific method signatures declared on (or inherited by) the specified class.
146    *
147    * <h5 class='section'>Example:</h5>
148    * <p class='bjava'>
149    *    ObjectIntrospector.<jsm>create</jsm>(<jv>myBean</jv>).allow(MyBean.<jk>class</jk>, <js>"getName"</js>, <js>"getAge"</js>);
150    * </p>
151    *
152    * @param declaringClass The class the allow-listed methods must be declared on (or a subtype thereof).
153    * @param signatures
154    *    One or more method signatures as returned by {@link MethodInfo#getSignature()} (e.g. <js>"getName"</js>,
155    *    <js>"substring(int,int)"</js>).
156    * @return This object.
157    */
158   public ObjectIntrospector allow(Class<?> declaringClass, String...signatures) {
159      var sigs = Set.of(signatures);
160      return allow(m -> declaringClass.isAssignableFrom(m.getDeclaringClass()) && sigs.contains(MethodInfo.of(m).getSignature()));
161   }
162
163   /**
164    * Disables allow-list enforcement, restoring the pre-10.0 behavior of allowing any public, non-deprecated
165    * method to be invoked.
166    *
167    * <p>
168    * <b>Use only for trusted, in-process callers.</b>  Never call this method on an introspector whose method
169    * name and/or arguments are sourced from an untrusted caller (e.g. parsed from an HTTP request) &mdash;
170    * allow-list the specific methods instead via {@link #allow(Class, String...) allow(...)}.
171    *
172    * @return This object.
173    */
174   public ObjectIntrospector allowAll() {
175      allowed = m -> true;
176      return this;
177   }
178
179   /**
180    * Primary method.
181    *
182    * <p>
183    * Invokes the specified method on this bean.
184    *
185    * @param <T> The return type of the method call.
186    * @param returnType The return type of the method call.
187    * @param method The method being invoked.
188    * @param args
189    *    The arguments to pass as parameters to the method.
190    *    These will automatically be converted to the appropriate object type if possible.
191    *    Can be <jk>null</jk> if method has no arguments.
192    * @return The object returned by the call to the method, or <jk>null</jk> if target object is <jk>null</jk>.
193    * @throws IllegalAccessException
194    *    If the <c>Constructor</c> object enforces Java language access control and the underlying constructor is
195    *    inaccessible.
196    * @throws IllegalArgumentException
197    *    If one of the following occurs:
198    *    <ul class='spaced-list'>
199    *       <li>
200    *          The number of actual and formal parameters differ.
201    *       <li>
202    *          An unwrapping conversion for primitive arguments fails.
203    *       <li>
204    *          A parameter value cannot be converted to the corresponding formal parameter type by a method invocation
205    *          conversion.
206    *       <li>
207    *          The constructor pertains to an enum type.
208    *    </ul>
209    * @throws InvocationTargetException If the underlying constructor throws an exception.
210    * @throws ParseException Malformed input encountered.
211    * @throws IOException Thrown by underlying stream.
212    * @throws MethodNotAllowlistedException If the method has not been allow-listed via {@link #allow(Class, String...) allow(...)} or {@link #allowAll()}.
213    */
214   public <T> T invokeMethod(Class<T> returnType, Method method, Reader args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException, ParseException, IOException {
215      return returnType.cast(invokeMethod(method, args));
216   }
217
218   /**
219    * Convenience method for invoking argument from method signature (@see {@link MethodInfo#getSignature()}.
220    *
221    * @param <T> The return type of the method call.
222    * @param returnType The return type of the method call.
223    * @param method The method being invoked.
224    * @param args
225    *    The arguments to pass as parameters to the method.
226    *    These will automatically be converted to the appropriate object type if possible.
227    *    Can be <jk>null</jk> if method has no arguments.
228    * @return The object returned by the call to the method, or <jk>null</jk> if target object is <jk>null</jk>.
229    * @throws NoSuchMethodException If method does not exist.
230    * @throws IllegalAccessException
231    *    If the <c>Constructor</c> object enforces Java language access control and
232    *    the underlying constructor is inaccessible.
233    * @throws IllegalArgumentException
234    *    If one of the following occurs:
235    *    <ul class='spaced-list'>
236    *       <li>
237    *          The number of actual and formal parameters differ.
238    *       <li>
239    *          An unwrapping conversion for primitive arguments fails.
240    *       <li>
241    *          A parameter value cannot be converted to the corresponding formal parameter type by a method invocation
242    *          conversion.
243    *       <li>
244    *          The constructor pertains to an enum type.
245    *    </ul>
246    * @throws InvocationTargetException If the underlying constructor throws an exception.
247    * @throws ParseException Malformed input encountered.
248    * @throws IOException Thrown by underlying stream.
249    * @throws MethodNotAllowlistedException If the method has not been allow-listed via {@link #allow(Class, String...) allow(...)} or {@link #allowAll()}.
250    */
251   public <T> T invokeMethod(Class<T> returnType, String method, String args)
252      throws NoSuchMethodException, IllegalArgumentException, InvocationTargetException, IllegalAccessException, ParseException, IOException {
253      return returnType.cast(invokeMethod(method, args));
254   }
255
256   /**
257    * Primary method.
258    *
259    * <p>
260    * Invokes the specified method on this bean.
261    *
262    * <p>
263    * The method must have been allow-listed via {@link #allow(Class, String...) allow(...)} (or
264    * {@link #allow(Predicate) allow(Predicate)}) or {@link #allowAll()} prior to calling this method, otherwise
265    * a {@link MethodNotAllowlistedException} is thrown.  See the class-level javadoc for details.
266    *
267    * @param method The method being invoked.
268    * @param args
269    *    The arguments to pass as parameters to the method.
270    *    These will automatically be converted to the appropriate object type if possible.
271    *    Can be <jk>null</jk> if method has no arguments.
272    * @return The object returned by the call to the method, or <jk>null</jk> if target object is <jk>null</jk>.
273    * @throws IllegalAccessException
274    *    If the <c>Constructor</c> object enforces Java language access control and the underlying constructor is
275    *    inaccessible.
276    * @throws IllegalArgumentException
277    *    If one of the following occurs:
278    *    <ul class='spaced-list'>
279    *       <li>
280    *          The number of actual and formal parameters differ.
281    *       <li>
282    *          An unwrapping conversion for primitive arguments fails.
283    *       <li>
284    *          A parameter value cannot be converted to the corresponding formal parameter type by a method invocation
285    *          conversion.
286    *       <li>
287    *          The constructor pertains to an enum type.
288    *    </ul>
289    * @throws InvocationTargetException If the underlying constructor throws an exception.
290    * @throws ParseException Malformed input encountered.
291    * @throws IOException Thrown by underlying stream.
292    * @throws MethodNotAllowlistedException If the method has not been allow-listed via {@link #allow(Class, String...) allow(...)} or {@link #allowAll()}.
293    */
294   public Object invokeMethod(Method method, Reader args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException, ParseException, IOException {
295      if (object == null)
296         return null;
297      if (allowed == null || !allowed.test(method))
298         throw new MethodNotAllowlistedException(
299            "Method '%s' on class '%s' has not been allow-listed for reflective invocation via ObjectIntrospector. "
300            + "Call allow(declaringClass, signatures) to allow-list specific methods, or allowAll() to permit any public, "
301            + "non-deprecated method (trusted in-process callers only).",
302            method.getName(), method.getDeclaringClass().getName());
303      Object[] params = args == null ? null : parser.readArgs(args, method.getGenericParameterTypes());
304      return method.invoke(object, params);
305   }
306
307   /**
308    * Convenience method for invoking argument from method signature (@see {@link MethodInfo#getSignature()}.
309    *
310    * @param method The method being invoked.
311    * @param args
312    *    The arguments to pass as parameters to the method.
313    *    These will automatically be converted to the appropriate object type if possible.
314    *    Can be <jk>null</jk> if method has no arguments.
315    * @return The object returned by the call to the method, or <jk>null</jk> if target object is <jk>null</jk>.
316    * @throws NoSuchMethodException If method does not exist.
317    * @throws IllegalAccessException
318    *    If the <c>Constructor</c> object enforces Java language access control and
319    *    the underlying constructor is inaccessible.
320    * @throws IllegalArgumentException
321    *    If one of the following occurs:
322    *    <ul class='spaced-list'>
323    *       <li>
324    *          The number of actual and formal parameters differ.
325    *       <li>
326    *          An unwrapping conversion for primitive arguments fails.
327    *       <li>
328    *          A parameter value cannot be converted to the corresponding formal parameter type by a method invocation
329    *          conversion.
330    *       <li>
331    *          The constructor pertains to an enum type.
332    *    </ul>
333    * @throws InvocationTargetException If the underlying constructor throws an exception.
334    * @throws ParseException Malformed input encountered.
335    * @throws IOException Thrown by underlying stream.
336    * @throws MethodNotAllowlistedException If the method has not been allow-listed via {@link #allow(Class, String...) allow(...)} or {@link #allowAll()}.
337    */
338   public Object invokeMethod(String method, String args) throws NoSuchMethodException, IllegalArgumentException, InvocationTargetException, IllegalAccessException, ParseException, IOException {
339      if (object == null)
340         return null;
341      var m = parser.getMarshallingContext()
342         .getClassMeta(object.getClass())
343         .getPublicMethods()
344         .stream()
345         .filter(x -> x.isNotDeprecated() && eq(x.getSignature(), method))
346         .findFirst()
347         .orElseThrow(() -> new NoSuchMethodException(method));
348      return invokeMethod(m.inner(), args == null ? null : new StringReader(args));
349   }
350}