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) — 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) — 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}