/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package freemarker.ext.beans;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.Set;

import freemarker.core.BugException;
import freemarker.core._DelayedConversionToString;
import freemarker.core._DelayedJQuote;
import freemarker.core._TemplateModelException;
import freemarker.template.TemplateModelException;
import freemarker.template.utility.ClassUtil;

For internal use only; don't depend on this, there's no backward compatibility guarantee at all! This class is to work around the lack of module system in Java, i.e., so that other FreeMarker packages can access things inside this package that users shouldn't.
/** * For internal use only; don't depend on this, there's no backward compatibility guarantee at all! * This class is to work around the lack of module system in Java, i.e., so that other FreeMarker packages can * access things inside this package that users shouldn't. */
public final class _MethodUtil { private _MethodUtil() { // Not meant to be instantiated }
Determines whether the type given as the 1st argument is convertible to the type given as the 2nd argument for method call argument conversion. This follows the rules of the Java reflection-based method call, except that since we don't have the value here, a boxed class is never seen as convertible to a primitive type.
Returns:0 means false, non-0 means true. That is, 0 is returned less specificity or incomparable specificity, also when if then method was aborted because of ifHigherThan. The absolute value of the returned non-0 number symbolizes how more specific it is:
  • 1: The two classes are identical
  • 2: The 1st type is primitive, the 2nd type is the corresponding boxing class
  • 3: Both classes are numerical, and one is convertible into the other with widening conversion. E.g., int is convertible to long and {#code double}, hence int is more specific. This ignores primitive VS boxed mismatches, except that a boxed class is never seen as convertible to a primitive class.
  • 4: One class is instanceof of the other, but they aren't identical. But unlike in Java, primitive numerical types are instanceof Number here.
/** * Determines whether the type given as the 1st argument is convertible to the type given as the 2nd argument * for method call argument conversion. This follows the rules of the Java reflection-based method call, except * that since we don't have the value here, a boxed class is never seen as convertible to a primitive type. * * @return 0 means {@code false}, non-0 means {@code true}. * That is, 0 is returned less specificity or incomparable specificity, also when if * then method was aborted because of {@code ifHigherThan}. * The absolute value of the returned non-0 number symbolizes how more specific it is: * <ul> * <li>1: The two classes are identical</li> * <li>2: The 1st type is primitive, the 2nd type is the corresponding boxing class</li> * <li>3: Both classes are numerical, and one is convertible into the other with widening conversion. * E.g., {@code int} is convertible to {@code long} and {#code double}, hence {@code int} is more * specific. * This ignores primitive VS boxed mismatches, except that a boxed class is never seen as * convertible to a primitive class.</li> * <li>4: One class is {@code instanceof} of the other, but they aren't identical. * But unlike in Java, primitive numerical types are {@code instanceof} {@link Number} here.</li> * </ul> */
public static int isMoreOrSameSpecificParameterType(final Class specific, final Class generic, boolean bugfixed, int ifHigherThan) { if (ifHigherThan >= 4) return 0; if (generic.isAssignableFrom(specific)) { // Identity or widening reference conversion: return generic == specific ? 1 : 4; } else { final boolean specificIsPrim = specific.isPrimitive(); final boolean genericIsPrim = generic.isPrimitive(); if (specificIsPrim) { if (genericIsPrim) { if (ifHigherThan >= 3) return 0; return isWideningPrimitiveNumberConversion(specific, generic) ? 3 : 0; } else { // => specificIsPrim && !genericIsPrim if (bugfixed) { final Class specificAsBoxed = ClassUtil.primitiveClassToBoxingClass(specific); if (specificAsBoxed == generic) { // A primitive class is more specific than its boxing class, because it can't store null return 2; } else if (generic.isAssignableFrom(specificAsBoxed)) { // Note: This only occurs if `specific` is a primitive numerical, and `generic == Number` return 4; } else if (ifHigherThan >= 3) { return 0; } else if (Number.class.isAssignableFrom(specificAsBoxed) && Number.class.isAssignableFrom(generic)) { return isWideningBoxedNumberConversion(specificAsBoxed, generic) ? 3 : 0; } else { return 0; } } else { return 0; } } } else { // => !specificIsPrim if (ifHigherThan >= 3) return 0; if (bugfixed && !genericIsPrim && Number.class.isAssignableFrom(specific) && Number.class.isAssignableFrom(generic)) { return isWideningBoxedNumberConversion(specific, generic) ? 3 : 0; } else { return 0; } } } // of: !generic.isAssignableFrom(specific) } private static boolean isWideningPrimitiveNumberConversion(final Class source, final Class target) { if (target == Short.TYPE && (source == Byte.TYPE)) { return true; } else if (target == Integer.TYPE && (source == Short.TYPE || source == Byte.TYPE)) { return true; } else if (target == Long.TYPE && (source == Integer.TYPE || source == Short.TYPE || source == Byte.TYPE)) { return true; } else if (target == Float.TYPE && (source == Long.TYPE || source == Integer.TYPE || source == Short.TYPE || source == Byte.TYPE)) { return true; } else if (target == Double.TYPE && (source == Float.TYPE || source == Long.TYPE || source == Integer.TYPE || source == Short.TYPE || source == Byte.TYPE)) { return true; } else { return false; } } private static boolean isWideningBoxedNumberConversion(final Class source, final Class target) { if (target == Short.class && source == Byte.class) { return true; } else if (target == Integer.class && (source == Short.class || source == Byte.class)) { return true; } else if (target == Long.class && (source == Integer.class || source == Short.class || source == Byte.class)) { return true; } else if (target == Float.class && (source == Long.class || source == Integer.class || source == Short.class || source == Byte.class)) { return true; } else if (target == Double.class && (source == Float.class || source == Long.class || source == Integer.class || source == Short.class || source == Byte.class)) { return true; } else { return false; } }
Attention, this doesn't handle primitive classes correctly, nor numerical conversions.
/** * Attention, this doesn't handle primitive classes correctly, nor numerical conversions. */
public static Set getAssignables(Class c1, Class c2) { Set s = new HashSet(); collectAssignables(c1, c2, s); return s; } private static void collectAssignables(Class c1, Class c2, Set s) { if (c1.isAssignableFrom(c2)) { s.add(c1); } Class sc = c1.getSuperclass(); if (sc != null) { collectAssignables(sc, c2, s); } Class[] itf = c1.getInterfaces(); for (int i = 0; i < itf.length; ++i) { collectAssignables(itf[i], c2, s); } } public static Class[] getParameterTypes(Member member) { if (member instanceof Method) { return ((Method) member).getParameterTypes(); } if (member instanceof Constructor) { return ((Constructor) member).getParameterTypes(); } throw new IllegalArgumentException("\"member\" must be Method or Constructor"); } public static boolean isVarargs(Member member) { if (member instanceof Method) { return ((Method) member).isVarArgs(); } if (member instanceof Constructor) { return ((Constructor) member).isVarArgs(); } throw new BugException(); }
Returns a more streamlined method or constructor description than Member.toString() does.
/** * Returns a more streamlined method or constructor description than {@code Member.toString()} does. */
public static String toString(Member member) { if (!(member instanceof Method || member instanceof Constructor)) { throw new IllegalArgumentException("\"member\" must be a Method or Constructor"); } StringBuilder sb = new StringBuilder(); if ((member.getModifiers() & Modifier.STATIC) != 0) { sb.append("static "); } String className = ClassUtil.getShortClassName(member.getDeclaringClass()); if (className != null) { sb.append(className); sb.append('.'); } sb.append(member.getName()); sb.append('('); Class[] paramTypes = _MethodUtil.getParameterTypes(member); for (int i = 0; i < paramTypes.length; i++) { if (i != 0) sb.append(", "); String paramTypeDecl = ClassUtil.getShortClassName(paramTypes[i]); if (i == paramTypes.length - 1 && paramTypeDecl.endsWith("[]") && _MethodUtil.isVarargs(member)) { sb.append(paramTypeDecl.substring(0, paramTypeDecl.length() - 2)); sb.append("..."); } else { sb.append(paramTypeDecl); } } sb.append(')'); return sb.toString(); } public static Object[] invocationErrorMessageStart(Member member) { return invocationErrorMessageStart(member, member instanceof Constructor); } private static Object[] invocationErrorMessageStart(Object member, boolean isConstructor) { return new Object[] { "Java ", isConstructor ? "constructor " : "method ", new _DelayedJQuote(member) }; } public static TemplateModelException newInvocationTemplateModelException(Object object, Member member, Throwable e) { return newInvocationTemplateModelException( object, member, (member.getModifiers() & Modifier.STATIC) != 0, member instanceof Constructor, e); } public static TemplateModelException newInvocationTemplateModelException(Object object, CallableMemberDescriptor callableMemberDescriptor, Throwable e) { return newInvocationTemplateModelException( object, new _DelayedConversionToString(callableMemberDescriptor) { @Override protected String doConversion(Object callableMemberDescriptor) { return ((CallableMemberDescriptor) callableMemberDescriptor).getDeclaration(); } }, callableMemberDescriptor.isStatic(), callableMemberDescriptor.isConstructor(), e); } private static TemplateModelException newInvocationTemplateModelException( Object parentObject, Object member, boolean isStatic, boolean isConstructor, Throwable e) { while (e instanceof InvocationTargetException) { Throwable cause = ((InvocationTargetException) e).getTargetException(); if (cause != null) { e = cause; } else { break; } } return new _TemplateModelException(e, invocationErrorMessageStart(member, isConstructor), " threw an exception", isStatic || isConstructor ? (Object) "" : new Object[] { " when invoked on ", parentObject.getClass(), " object ", new _DelayedJQuote(parentObject) }, "; see cause exception in the Java stack trace."); }
Extracts the JavaBeans property from a reader method name, or returns null if the method name doesn't look like a reader method name.
/** * Extracts the JavaBeans property from a reader method name, or returns {@code null} if the method name doesn't * look like a reader method name. */
public static String getBeanPropertyNameFromReaderMethodName(String name, Class<?> returnType) { int start; if (name.startsWith("get")) { start = 3; } else if (returnType == boolean.class && name.startsWith("is")) { start = 2; } else { return null; } int ln = name.length(); if (start == ln) { return null; } char c1 = name.charAt(start); return start + 1 < ln && Character.isUpperCase(name.charAt(start + 1)) && Character.isUpperCase(c1) ? name.substring(start) // getFOOBar => "FOOBar" (not lower case) according the JavaBeans spec. : new StringBuilder(ln - start).append(Character.toLowerCase(c1)).append(name, start + 1, ln) .toString(); } }