/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch 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 org.elasticsearch.painless; import org.apache.lucene.util.Constants; import org.apache.lucene.util.SetOnce; import org.elasticsearch.painless.api.Augmentation; import java.io.InputStream; import java.io.InputStreamReader; import java.io.LineNumberReader; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.reflect.Modifier; import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.PrimitiveIterator; import java.util.Spliterator; /** * The entire API for Painless. Also used as a whitelist for checking for legal * methods and fields during at both compile-time and runtime. */ public final class Definition { private static final List<String> DEFINITION_FILES = Collections.unmodifiableList( Arrays.asList("org.elasticsearch.txt", "java.lang.txt", "java.math.txt", "java.text.txt", "java.time.txt", "java.time.chrono.txt", "java.time.format.txt", "java.time.temporal.txt", "java.time.zone.txt", "java.util.txt", "java.util.function.txt", "java.util.regex.txt", "java.util.stream.txt", "joda.time.txt")); /** * Whitelist that is "built in" to Painless and required by all scripts. */ public static final Definition BUILTINS = new Definition(); /** Some native types as constants: */ public static final Type VOID_TYPE = BUILTINS.getType("void"); public static final Type BOOLEAN_TYPE = BUILTINS.getType("boolean"); public static final Type BOOLEAN_OBJ_TYPE = BUILTINS.getType("Boolean"); public static final Type BYTE_TYPE = BUILTINS.getType("byte"); public static final Type BYTE_OBJ_TYPE = BUILTINS.getType("Byte"); public static final Type SHORT_TYPE = BUILTINS.getType("short"); public static final Type SHORT_OBJ_TYPE = BUILTINS.getType("Short"); public static final Type INT_TYPE = BUILTINS.getType("int"); public static final Type INT_OBJ_TYPE = BUILTINS.getType("Integer"); public static final Type LONG_TYPE = BUILTINS.getType("long"); public static final Type LONG_OBJ_TYPE = BUILTINS.getType("Long"); public static final Type FLOAT_TYPE = BUILTINS.getType("float"); public static final Type FLOAT_OBJ_TYPE = BUILTINS.getType("Float"); public static final Type DOUBLE_TYPE = BUILTINS.getType("double"); public static final Type DOUBLE_OBJ_TYPE = BUILTINS.getType("Double"); public static final Type CHAR_TYPE = BUILTINS.getType("char"); public static final Type CHAR_OBJ_TYPE = BUILTINS.getType("Character"); public static final Type OBJECT_TYPE = BUILTINS.getType("Object"); public static final Type DEF_TYPE = BUILTINS.getType("def"); public static final Type NUMBER_TYPE = BUILTINS.getType("Number"); public static final Type STRING_TYPE = BUILTINS.getType("String"); public static final Type EXCEPTION_TYPE = BUILTINS.getType("Exception"); public static final Type PATTERN_TYPE = BUILTINS.getType("Pattern"); public static final Type MATCHER_TYPE = BUILTINS.getType("Matcher"); public static final Type ITERATOR_TYPE = BUILTINS.getType("Iterator"); public static final Type ARRAY_LIST_TYPE = BUILTINS.getType("ArrayList"); public static final Type HASH_MAP_TYPE = BUILTINS.getType("HashMap"); public enum Sort { VOID( void.class , Void.class , null , 0 , true , false , false , false ), BOOL( boolean.class , Boolean.class , null , 1 , true , true , false , true ), BYTE( byte.class , Byte.class , null , 1 , true , false , true , true ), SHORT( short.class , Short.class , null , 1 , true , false , true , true ), CHAR( char.class , Character.class , null , 1 , true , false , true , true ), INT( int.class , Integer.class , null , 1 , true , false , true , true ), LONG( long.class , Long.class , null , 2 , true , false , true , true ), FLOAT( float.class , Float.class , null , 1 , true , false , true , true ), DOUBLE( double.class , Double.class , null , 2 , true , false , true , true ), VOID_OBJ( Void.class , null , void.class , 1 , true , false , false , false ), BOOL_OBJ( Boolean.class , null , boolean.class , 1 , false , true , false , false ), BYTE_OBJ( Byte.class , null , byte.class , 1 , false , false , true , false ), SHORT_OBJ( Short.class , null , short.class , 1 , false , false , true , false ), CHAR_OBJ( Character.class , null , char.class , 1 , false , false , true , false ), INT_OBJ( Integer.class , null , int.class , 1 , false , false , true , false ), LONG_OBJ( Long.class , null , long.class , 1 , false , false , true , false ), FLOAT_OBJ( Float.class , null , float.class , 1 , false , false , true , false ), DOUBLE_OBJ( Double.class , null , double.class , 1 , false , false , true , false ), NUMBER( Number.class , null , null , 1 , false , false , false , false ), STRING( String.class , null , null , 1 , false , false , false , true ), OBJECT( null , null , null , 1 , false , false , false , false ), DEF( null , null , null , 1 , false , false , false , false ), ARRAY( null , null , null , 1 , false , false , false , false ); public final Class<?> clazz; public final Class<?> boxed; public final Class<?> unboxed; public final int size; public final boolean primitive; public final boolean bool; public final boolean numeric; public final boolean constant; Sort(final Class<?> clazz, final Class<?> boxed, final Class<?> unboxed, final int size, final boolean primitive, final boolean bool, final boolean numeric, final boolean constant) { this.clazz = clazz; this.boxed = boxed; this.unboxed = unboxed; this.size = size; this.bool = bool; this.primitive = primitive; this.numeric = numeric; this.constant = constant; } } public static final class Type { public final String name; public final int dimensions; public final Struct struct; public final Class<?> clazz; public final org.objectweb.asm.Type type; public final Sort sort; private Type(final String name, final int dimensions, final Struct struct, final Class<?> clazz, final org.objectweb.asm.Type type, final Sort sort) { this.name = name; this.dimensions = dimensions; this.struct = struct; this.clazz = clazz; this.type = type; this.sort = sort; } @Override public boolean equals(final Object object) { if (this == object) { return true; } if (object == null || getClass() != object.getClass()) { return false; } final Type type = (Type)object; return this.type.equals(type.type) && struct.equals(type.struct); } @Override public int hashCode() { int result = struct.hashCode(); result = 31 * result + type.hashCode(); return result; } @Override public String toString() { return name; } } public static class Method { public final String name; public final Struct owner; public final boolean augmentation; public final Type rtn; public final List<Type> arguments; public final org.objectweb.asm.commons.Method method; public final int modifiers; public final MethodHandle handle; public Method(String name, Struct owner, boolean augmentation, Type rtn, List<Type> arguments, org.objectweb.asm.commons.Method method, int modifiers, MethodHandle handle) { this.name = name; this.augmentation = augmentation; this.owner = owner; this.rtn = rtn; this.arguments = Collections.unmodifiableList(arguments); this.method = method; this.modifiers = modifiers; this.handle = handle; } /** * Returns MethodType for this method. * <p> * This works even for user-defined Methods (where the MethodHandle is null). */ public MethodType getMethodType() { // we have a methodhandle already (e.g. whitelisted class) // just return its type if (handle != null) { return handle.type(); } // otherwise compute it final Class<?> params[]; final Class<?> returnValue; if (augmentation) { // static method disguised as virtual/interface method params = new Class<?>[1 + arguments.size()]; params[0] = Augmentation.class; for (int i = 0; i < arguments.size(); i++) { params[i + 1] = arguments.get(i).clazz; } returnValue = rtn.clazz; } else if (Modifier.isStatic(modifiers)) { // static method: straightforward copy params = new Class<?>[arguments.size()]; for (int i = 0; i < arguments.size(); i++) { params[i] = arguments.get(i).clazz; } returnValue = rtn.clazz; } else if ("<init>".equals(name)) { // constructor: returns the owner class params = new Class<?>[arguments.size()]; for (int i = 0; i < arguments.size(); i++) { params[i] = arguments.get(i).clazz; } returnValue = owner.clazz; } else { // virtual/interface method: add receiver class params = new Class<?>[1 + arguments.size()]; params[0] = owner.clazz; for (int i = 0; i < arguments.size(); i++) { params[i + 1] = arguments.get(i).clazz; } returnValue = rtn.clazz; } return MethodType.methodType(returnValue, params); } public void write(MethodWriter writer) { final org.objectweb.asm.Type type; if (augmentation) { assert java.lang.reflect.Modifier.isStatic(modifiers); type = WriterConstants.AUGMENTATION_TYPE; } else { type = owner.type; } if (java.lang.reflect.Modifier.isStatic(modifiers)) { writer.invokeStatic(type, method); } else if (java.lang.reflect.Modifier.isInterface(owner.clazz.getModifiers())) { writer.invokeInterface(type, method); } else { writer.invokeVirtual(type, method); } } } public static final class Field { public final String name; public final Struct owner; public final Type type; public final String javaName; public final int modifiers; private final MethodHandle getter; private final MethodHandle setter; private Field(String name, String javaName, Struct owner, Type type, int modifiers, MethodHandle getter, MethodHandle setter) { this.name = name; this.javaName = javaName; this.owner = owner; this.type = type; this.modifiers = modifiers; this.getter = getter; this.setter = setter; } } // TODO: instead of hashing on this, we could have a 'next' pointer in Method itself, but it would make code more complex // please do *NOT* under any circumstances change this to be the crappy Tuple from elasticsearch! /** * Key for looking up a method. * <p> * Methods are keyed on both name and arity, and can be overloaded once per arity. * This allows signatures such as {@code String.indexOf(String) vs String.indexOf(String, int)}. * <p> * It is less flexible than full signature overloading where types can differ too, but * better than just the name, and overloading types adds complexity to users, too. */ public static final class MethodKey { public final String name; public final int arity; /** * Create a new lookup key * @param name name of the method * @param arity number of parameters */ public MethodKey(String name, int arity) { this.name = Objects.requireNonNull(name); this.arity = arity; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + arity; result = prime * result + name.hashCode(); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; MethodKey other = (MethodKey) obj; if (arity != other.arity) return false; if (!name.equals(other.name)) return false; return true; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(name); sb.append('/'); sb.append(arity); return sb.toString(); } } public static final class Struct { public final String name; public final Class<?> clazz; public final org.objectweb.asm.Type type; public final Map<MethodKey, Method> constructors; public final Map<MethodKey, Method> staticMethods; public final Map<MethodKey, Method> methods; public final Map<String, Field> staticMembers; public final Map<String, Field> members; private final SetOnce<Method> functionalMethod; private Struct(final String name, final Class<?> clazz, final org.objectweb.asm.Type type) { this.name = name; this.clazz = clazz; this.type = type; constructors = new HashMap<>(); staticMethods = new HashMap<>(); methods = new HashMap<>(); staticMembers = new HashMap<>(); members = new HashMap<>(); functionalMethod = new SetOnce<>(); } private Struct(final Struct struct) { name = struct.name; clazz = struct.clazz; type = struct.type; constructors = Collections.unmodifiableMap(struct.constructors); staticMethods = Collections.unmodifiableMap(struct.staticMethods); methods = Collections.unmodifiableMap(struct.methods); staticMembers = Collections.unmodifiableMap(struct.staticMembers); members = Collections.unmodifiableMap(struct.members); functionalMethod = struct.functionalMethod; } private Struct freeze() { return new Struct(this); } @Override public boolean equals(Object object) { if (this == object) { return true; } if (object == null || getClass() != object.getClass()) { return false; } Struct struct = (Struct)object; return name.equals(struct.name); } @Override public int hashCode() { return name.hashCode(); } /** * If this class is a functional interface according to JLS, returns its method. * Otherwise returns null. */ public Method getFunctionalMethod() { return functionalMethod.get(); } } public static class Cast { public final Type from; public final Type to; public final boolean explicit; public final Type unboxFrom; public final Type unboxTo; public final Type boxFrom; public final Type boxTo; public Cast(final Type from, final Type to, final boolean explicit) { this.from = from; this.to = to; this.explicit = explicit; this.unboxFrom = null; this.unboxTo = null; this.boxFrom = null; this.boxTo = null; } public Cast(final Type from, final Type to, final boolean explicit, final Type unboxFrom, final Type unboxTo, final Type boxFrom, final Type boxTo) { this.from = from; this.to = to; this.explicit = explicit; this.unboxFrom = unboxFrom; this.unboxTo = unboxTo; this.boxFrom = boxFrom; this.boxTo = boxTo; } } public static final class RuntimeClass { private final Struct struct; public final Map<MethodKey, Method> methods; public final Map<String, MethodHandle> getters; public final Map<String, MethodHandle> setters; private RuntimeClass(final Struct struct, final Map<MethodKey, Method> methods, final Map<String, MethodHandle> getters, final Map<String, MethodHandle> setters) { this.struct = struct; this.methods = Collections.unmodifiableMap(methods); this.getters = Collections.unmodifiableMap(getters); this.setters = Collections.unmodifiableMap(setters); } public Struct getStruct() { return struct; } } /** Returns whether or not a non-array type exists. */ public boolean isSimpleType(final String name) { return BUILTINS.structsMap.containsKey(name); } /** Gets the type given by its name */ public Type getType(final String name) { return BUILTINS.getTypeInternal(name); } /** Creates an array type from the given Struct. */ public Type getType(final Struct struct, final int dimensions) { return BUILTINS.getTypeInternal(struct, dimensions); } public RuntimeClass getRuntimeClass(Class<?> clazz) { return BUILTINS.runtimeMap.get(clazz); } /** Collection of all simple types. Used by {@code PainlessDocGenerator} to generate an API reference. */ static Collection<Type> allSimpleTypes() { return BUILTINS.simpleTypesMap.values(); } // INTERNAL IMPLEMENTATION: private final Map<Class<?>, RuntimeClass> runtimeMap; private final Map<String, Struct> structsMap; private final Map<String, Type> simpleTypesMap; private Definition() { structsMap = new HashMap<>(); simpleTypesMap = new HashMap<>(); runtimeMap = new HashMap<>(); // parse the classes and return hierarchy (map of class name -> superclasses/interfaces) Map<String, List<String>> hierarchy = addStructs(); // add every method for each class addElements(); // apply hierarchy: this means e.g. copying Object's methods into String (thats how subclasses work) for (Map.Entry<String,List<String>> clazz : hierarchy.entrySet()) { copyStruct(clazz.getKey(), clazz.getValue()); } // if someone declares an interface type, its still an Object for (Map.Entry<String,Struct> clazz : structsMap.entrySet()) { String name = clazz.getKey(); Class<?> javaPeer = clazz.getValue().clazz; if (javaPeer.isInterface()) { copyStruct(name, Collections.singletonList("Object")); } else if (name.equals("def") == false && name.equals("Object") == false && javaPeer.isPrimitive() == false) { // but otherwise, unless its a primitive type, it really should assert hierarchy.get(name) != null : "class '" + name + "' does not extend Object!"; assert hierarchy.get(name).contains("Object") : "class '" + name + "' does not extend Object!"; } } // mark functional interfaces (or set null, to mark class is not) for (Struct clazz : structsMap.values()) { clazz.functionalMethod.set(computeFunctionalInterfaceMethod(clazz)); } // precompute runtime classes for (Struct struct : structsMap.values()) { addRuntimeClass(struct); } // copy all structs to make them unmodifiable for outside users: for (final Map.Entry<String,Struct> entry : structsMap.entrySet()) { entry.setValue(entry.getValue().freeze()); } } /** adds classes from definition. returns hierarchy */ private Map<String,List<String>> addStructs() { final Map<String,List<String>> hierarchy = new HashMap<>(); for (String file : DEFINITION_FILES) { int currentLine = -1; try { try (InputStream stream = Definition.class.getResourceAsStream(file); LineNumberReader reader = new LineNumberReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) { String line = null; while ((line = reader.readLine()) != null) { currentLine = reader.getLineNumber(); line = line.trim(); if (line.length() == 0 || line.charAt(0) == '#') { continue; } if (line.startsWith("class ")) { String elements[] = line.split("\u0020"); assert elements[2].equals("->") : "Invalid struct definition [" + String.join(" ", elements) +"]"; if (elements.length == 7) { hierarchy.put(elements[1], Arrays.asList(elements[5].split(","))); } else { assert elements.length == 5 : "Invalid struct definition [" + String.join(" ", elements) + "]"; } String className = elements[1]; String javaPeer = elements[3]; final Class<?> javaClazz; switch (javaPeer) { case "void": javaClazz = void.class; break; case "boolean": javaClazz = boolean.class; break; case "byte": javaClazz = byte.class; break; case "short": javaClazz = short.class; break; case "char": javaClazz = char.class; break; case "int": javaClazz = int.class; break; case "long": javaClazz = long.class; break; case "float": javaClazz = float.class; break; case "double": javaClazz = double.class; break; default: javaClazz = Class.forName(javaPeer); break; } addStruct(className, javaClazz); } } } } catch (Exception e) { throw new RuntimeException("error in " + file + ", line: " + currentLine, e); } } return hierarchy; } /** adds class methods/fields/ctors */ private void addElements() { for (String file : DEFINITION_FILES) { int currentLine = -1; try { try (InputStream stream = Definition.class.getResourceAsStream(file); LineNumberReader reader = new LineNumberReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) { String line = null; String currentClass = null; while ((line = reader.readLine()) != null) { currentLine = reader.getLineNumber(); line = line.trim(); if (line.length() == 0 || line.charAt(0) == '#') { continue; } else if (line.startsWith("class ")) { assert currentClass == null; currentClass = line.split("\u0020")[1]; } else if (line.equals("}")) { assert currentClass != null; currentClass = null; } else { assert currentClass != null; addSignature(currentClass, line); } } } } catch (Exception e) { throw new RuntimeException("syntax error in " + file + ", line: " + currentLine, e); } } } private void addStruct(final String name, final Class<?> clazz) { if (!name.matches("^[_a-zA-Z][\\.,_a-zA-Z0-9]*$")) { throw new IllegalArgumentException("Invalid struct name [" + name + "]."); } if (structsMap.containsKey(name)) { throw new IllegalArgumentException("Duplicate struct name [" + name + "]."); } final Struct struct = new Struct(name, clazz, org.objectweb.asm.Type.getType(clazz)); structsMap.put(name, struct); simpleTypesMap.put(name, getTypeInternal(name)); } private void addConstructorInternal(final String struct, final String name, final Type[] args) { final Struct owner = structsMap.get(struct); if (owner == null) { throw new IllegalArgumentException( "Owner struct [" + struct + "] not defined for constructor [" + name + "]."); } if (!name.matches("<init>")) { throw new IllegalArgumentException( "Invalid constructor name [" + name + "] with the struct [" + owner.name + "]."); } MethodKey methodKey = new MethodKey(name, args.length); if (owner.constructors.containsKey(methodKey)) { throw new IllegalArgumentException( "Duplicate constructor [" + methodKey + "] found within the struct [" + owner.name + "]."); } if (owner.staticMethods.containsKey(methodKey)) { throw new IllegalArgumentException("Constructors and static methods may not have the same signature" + " [" + methodKey + "] within the same struct [" + owner.name + "]."); } if (owner.methods.containsKey(methodKey)) { throw new IllegalArgumentException("Constructors and methods may not have the same signature" + " [" + methodKey + "] within the same struct [" + owner.name + "]."); } final Class<?>[] classes = new Class<?>[args.length]; for (int count = 0; count < classes.length; ++count) { classes[count] = args[count].clazz; } final java.lang.reflect.Constructor<?> reflect; try { reflect = owner.clazz.getConstructor(classes); } catch (final NoSuchMethodException exception) { throw new IllegalArgumentException("Constructor [" + name + "] not found for class" + " [" + owner.clazz.getName() + "] with arguments " + Arrays.toString(classes) + "."); } final org.objectweb.asm.commons.Method asm = org.objectweb.asm.commons.Method.getMethod(reflect); final Type returnType = getTypeInternal("void"); final MethodHandle handle; try { handle = MethodHandles.publicLookup().in(owner.clazz).unreflectConstructor(reflect); } catch (final IllegalAccessException exception) { throw new IllegalArgumentException("Constructor " + " not found for class [" + owner.clazz.getName() + "]" + " with arguments " + Arrays.toString(classes) + "."); } final Method constructor = new Method(name, owner, false, returnType, Arrays.asList(args), asm, reflect.getModifiers(), handle); owner.constructors.put(methodKey, constructor); } /** * Adds a new signature to the definition. * <p> * Signatures have the following forms: * <ul> * <li>{@code void method(String,int)} * <li>{@code boolean field} * <li>{@code Class <init>(String)} * </ul> * no spaces allowed. */ private void addSignature(String className, String signature) { String elements[] = signature.split("\u0020"); if (elements.length != 2) { throw new IllegalArgumentException("Malformed signature: " + signature); } // method or field type (e.g. return type) Type rtn = getTypeInternal(elements[0]); int parenIndex = elements[1].indexOf('('); if (parenIndex != -1) { // method or ctor int parenEnd = elements[1].indexOf(')'); final Type args[]; if (parenEnd > parenIndex + 1) { String arguments[] = elements[1].substring(parenIndex + 1, parenEnd).split(","); args = new Type[arguments.length]; for (int i = 0; i < arguments.length; i++) { args[i] = getTypeInternal(arguments[i]); } } else { args = new Type[0]; } String methodName = elements[1].substring(0, parenIndex); if (methodName.equals("<init>")) { if (!elements[0].equals(className)) { throw new IllegalArgumentException("Constructors must return their own type"); } addConstructorInternal(className, "<init>", args); } else { if (methodName.indexOf("*") >= 0) { addMethodInternal(className, methodName.substring(0, methodName.length() - 1), true, rtn, args); } else { addMethodInternal(className, methodName, false, rtn, args); } } } else { // field addFieldInternal(className, elements[1], rtn); } } private void addMethodInternal(String struct, String name, boolean augmentation, Type rtn, Type[] args) { final Struct owner = structsMap.get(struct); if (owner == null) { throw new IllegalArgumentException("Owner struct [" + struct + "] not defined" + " for method [" + name + "]."); } if (!name.matches("^[_a-zA-Z][_a-zA-Z0-9]*$")) { throw new IllegalArgumentException("Invalid method name" + " [" + name + "] with the struct [" + owner.name + "]."); } MethodKey methodKey = new MethodKey(name, args.length); if (owner.constructors.containsKey(methodKey)) { throw new IllegalArgumentException("Constructors and methods" + " may not have the same signature [" + methodKey + "] within the same struct" + " [" + owner.name + "]."); } if (owner.staticMethods.containsKey(methodKey) || owner.methods.containsKey(methodKey)) { throw new IllegalArgumentException( "Duplicate method signature [" + methodKey + "] found within the struct [" + owner.name + "]."); } final Class<?> implClass; final Class<?>[] params; if (augmentation == false) { implClass = owner.clazz; params = new Class<?>[args.length]; for (int count = 0; count < args.length; ++count) { params[count] = args[count].clazz; } } else { implClass = Augmentation.class; params = new Class<?>[args.length + 1]; params[0] = owner.clazz; for (int count = 0; count < args.length; ++count) { params[count+1] = args[count].clazz; } } final java.lang.reflect.Method reflect; try { reflect = implClass.getMethod(name, params); } catch (NoSuchMethodException exception) { throw new IllegalArgumentException("Method [" + name + "] not found for class [" + implClass.getName() + "]" + " with arguments " + Arrays.toString(params) + "."); } if (!reflect.getReturnType().equals(rtn.clazz)) { throw new IllegalArgumentException("Specified return type class [" + rtn.clazz + "]" + " does not match the found return type class [" + reflect.getReturnType() + "] for the" + " method [" + name + "]" + " within the struct [" + owner.name + "]."); } final org.objectweb.asm.commons.Method asm = org.objectweb.asm.commons.Method.getMethod(reflect); MethodHandle handle; try { handle = MethodHandles.publicLookup().in(implClass).unreflect(reflect); } catch (final IllegalAccessException exception) { throw new IllegalArgumentException("Method [" + name + "]" + " not found for class [" + implClass.getName() + "]" + " with arguments " + Arrays.toString(params) + "."); } final int modifiers = reflect.getModifiers(); final Method method = new Method(name, owner, augmentation, rtn, Arrays.asList(args), asm, modifiers, handle); if (augmentation == false && java.lang.reflect.Modifier.isStatic(modifiers)) { owner.staticMethods.put(methodKey, method); } else { owner.methods.put(methodKey, method); } } private void addFieldInternal(String struct, String name, Type type) { final Struct owner = structsMap.get(struct); if (owner == null) { throw new IllegalArgumentException("Owner struct [" + struct + "] not defined for " + " field [" + name + "]."); } if (!name.matches("^[_a-zA-Z][_a-zA-Z0-9]*$")) { throw new IllegalArgumentException("Invalid field " + " name [" + name + "] with the struct [" + owner.name + "]."); } if (owner.staticMembers.containsKey(name) || owner.members.containsKey(name)) { throw new IllegalArgumentException("Duplicate field name [" + name + "]" + " found within the struct [" + owner.name + "]."); } java.lang.reflect.Field reflect; try { reflect = owner.clazz.getField(name); } catch (final NoSuchFieldException exception) { throw new IllegalArgumentException("Field [" + name + "]" + " not found for class [" + owner.clazz.getName() + "]."); } final int modifiers = reflect.getModifiers(); boolean isStatic = java.lang.reflect.Modifier.isStatic(modifiers); MethodHandle getter = null; MethodHandle setter = null; try { if (!isStatic) { getter = MethodHandles.publicLookup().unreflectGetter(reflect); setter = MethodHandles.publicLookup().unreflectSetter(reflect); } } catch (final IllegalAccessException exception) { throw new IllegalArgumentException("Getter/Setter [" + name + "]" + " not found for class [" + owner.clazz.getName() + "]."); } final Field field = new Field(name, reflect.getName(), owner, type, modifiers, getter, setter); if (isStatic) { // require that all static fields are static final if (!java.lang.reflect.Modifier.isFinal(modifiers)) { throw new IllegalArgumentException("Static [" + name + "]" + " within the struct [" + owner.name + "] is not final."); } owner.staticMembers.put(name, field); } else { owner.members.put(name, field); } } private void copyStruct(String struct, List<String> children) { final Struct owner = structsMap.get(struct); if (owner == null) { throw new IllegalArgumentException("Owner struct [" + struct + "] not defined for copy."); } for (int count = 0; count < children.size(); ++count) { final Struct child = structsMap.get(children.get(count)); if (child == null) { throw new IllegalArgumentException("Child struct [" + children.get(count) + "]" + " not defined for copy to owner struct [" + owner.name + "]."); } if (!child.clazz.isAssignableFrom(owner.clazz)) { throw new ClassCastException("Child struct [" + child.name + "]" + " is not a super type of owner struct [" + owner.name + "] in copy."); } for (Map.Entry<MethodKey,Method> kvPair : child.methods.entrySet()) { MethodKey methodKey = kvPair.getKey(); Method method = kvPair.getValue(); if (owner.methods.get(methodKey) == null) { // sanity check, look for missing covariant/generic override if (owner.clazz.isInterface() && child.clazz == Object.class) { // ok } else if (child.clazz == Spliterator.OfPrimitive.class || child.clazz == PrimitiveIterator.class) { // ok, we rely on generics erasure for these (its guaranteed in the javadocs though!!!!) } else if (Constants.JRE_IS_MINIMUM_JAVA9 && owner.clazz == LocalDate.class) { // ok, java 9 added covariant override for LocalDate.getEra() to return IsoEra: // https://bugs.openjdk.java.net/browse/JDK-8072746 } else { try { // TODO: we *have* to remove all these public members and use getter methods to encapsulate! final Class<?> impl; final Class<?> arguments[]; if (method.augmentation) { impl = Augmentation.class; arguments = new Class<?>[method.arguments.size() + 1]; arguments[0] = method.owner.clazz; for (int i = 0; i < method.arguments.size(); i++) { arguments[i + 1] = method.arguments.get(i).clazz; } } else { impl = owner.clazz; arguments = new Class<?>[method.arguments.size()]; for (int i = 0; i < method.arguments.size(); i++) { arguments[i] = method.arguments.get(i).clazz; } } java.lang.reflect.Method m = impl.getMethod(method.method.getName(), arguments); if (m.getReturnType() != method.rtn.clazz) { throw new IllegalStateException("missing covariant override for: " + m + " in " + owner.name); } if (m.isBridge() && !Modifier.isVolatile(method.modifiers)) { // its a bridge in the destination, but not in the source, but it might still be ok, check generics: java.lang.reflect.Method source = child.clazz.getMethod(method.method.getName(), arguments); if (!Arrays.equals(source.getGenericParameterTypes(), source.getParameterTypes())) { throw new IllegalStateException("missing generic override for: " + m + " in " + owner.name); } } } catch (ReflectiveOperationException e) { throw new AssertionError(e); } } owner.methods.put(methodKey, method); } } for (Field field : child.members.values()) { if (owner.members.get(field.name) == null) { owner.members.put(field.name, new Field(field.name, field.javaName, owner, field.type, field.modifiers, field.getter, field.setter)); } } } } /** * Precomputes a more efficient structure for dynamic method/field access. */ private void addRuntimeClass(final Struct struct) { final Map<MethodKey, Method> methods = struct.methods; final Map<String, MethodHandle> getters = new HashMap<>(); final Map<String, MethodHandle> setters = new HashMap<>(); // add all members for (final Map.Entry<String, Field> member : struct.members.entrySet()) { getters.put(member.getKey(), member.getValue().getter); setters.put(member.getKey(), member.getValue().setter); } // add all getters/setters for (final Map.Entry<MethodKey, Method> method : methods.entrySet()) { final String name = method.getKey().name; final Method m = method.getValue(); if (m.arguments.size() == 0 && name.startsWith("get") && name.length() > 3 && Character.isUpperCase(name.charAt(3))) { final StringBuilder newName = new StringBuilder(); newName.append(Character.toLowerCase(name.charAt(3))); newName.append(name.substring(4)); getters.putIfAbsent(newName.toString(), m.handle); } else if (m.arguments.size() == 0 && name.startsWith("is") && name.length() > 2 && Character.isUpperCase(name.charAt(2))) { final StringBuilder newName = new StringBuilder(); newName.append(Character.toLowerCase(name.charAt(2))); newName.append(name.substring(3)); getters.putIfAbsent(newName.toString(), m.handle); } if (m.arguments.size() == 1 && name.startsWith("set") && name.length() > 3 && Character.isUpperCase(name.charAt(3))) { final StringBuilder newName = new StringBuilder(); newName.append(Character.toLowerCase(name.charAt(3))); newName.append(name.substring(4)); setters.putIfAbsent(newName.toString(), m.handle); } } runtimeMap.put(struct.clazz, new RuntimeClass(struct, methods, getters, setters)); } /** computes the functional interface method for a class, or returns null */ private Method computeFunctionalInterfaceMethod(Struct clazz) { if (!clazz.clazz.isInterface()) { return null; } // if its marked with this annotation, we fail if the conditions don't hold (means whitelist bug) // otherwise, this annotation is pretty useless. boolean hasAnnotation = clazz.clazz.isAnnotationPresent(FunctionalInterface.class); List<java.lang.reflect.Method> methods = new ArrayList<>(); for (java.lang.reflect.Method m : clazz.clazz.getMethods()) { // default interface methods don't count if (m.isDefault()) { continue; } // static methods don't count if (Modifier.isStatic(m.getModifiers())) { continue; } // if its from Object, it doesn't count try { Object.class.getMethod(m.getName(), m.getParameterTypes()); continue; } catch (ReflectiveOperationException e) { // it counts } methods.add(m); } if (methods.size() != 1) { if (hasAnnotation) { throw new IllegalArgumentException("Class: " + clazz.name + " is marked with FunctionalInterface but doesn't fit the bill: " + methods); } return null; } // inspect the one method found from the reflection API, it should match the whitelist! java.lang.reflect.Method oneMethod = methods.get(0); Method painless = clazz.methods.get(new Definition.MethodKey(oneMethod.getName(), oneMethod.getParameterCount())); if (painless == null || painless.method.equals(org.objectweb.asm.commons.Method.getMethod(oneMethod)) == false) { throw new IllegalArgumentException("Class: " + clazz.name + " is functional but the functional " + "method is not whitelisted!"); } return painless; } private Type getTypeInternal(String name) { // simple types (e.g. 0 array dimensions) are a simple hash lookup for speed Type simple = simpleTypesMap.get(name); if (simple != null) { return simple; } int dimensions = getDimensions(name); String structstr = dimensions == 0 ? name : name.substring(0, name.indexOf('[')); Struct struct = structsMap.get(structstr); if (struct == null) { throw new IllegalArgumentException("The struct with name [" + name + "] has not been defined."); } return getTypeInternal(struct, dimensions); } private Type getTypeInternal(Struct struct, int dimensions) { String name = struct.name; org.objectweb.asm.Type type = struct.type; Class<?> clazz = struct.clazz; Sort sort; if (dimensions > 0) { StringBuilder builder = new StringBuilder(name); char[] brackets = new char[dimensions]; for (int count = 0; count < dimensions; ++count) { builder.append("[]"); brackets[count] = '['; } String descriptor = new String(brackets) + struct.type.getDescriptor(); name = builder.toString(); type = org.objectweb.asm.Type.getType(descriptor); try { clazz = Class.forName(type.getInternalName().replace('/', '.')); } catch (ClassNotFoundException exception) { throw new IllegalArgumentException("The class [" + type.getInternalName() + "]" + " could not be found to create type [" + name + "]."); } sort = Sort.ARRAY; } else if ("def".equals(struct.name)) { sort = Sort.DEF; } else { sort = Sort.OBJECT; for (Sort value : Sort.values()) { if (value.clazz == null) { continue; } if (value.clazz.equals(struct.clazz)) { sort = value; break; } } } return new Type(name, dimensions, struct, clazz, type, sort); } private int getDimensions(String name) { int dimensions = 0; int index = name.indexOf('['); if (index != -1) { int length = name.length(); while (index < length) { if (name.charAt(index) == '[' && ++index < length && name.charAt(index++) == ']') { ++dimensions; } else { throw new IllegalArgumentException("Invalid array braces in canonical name [" + name + "]."); } } } return dimensions; } }