diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java index 752c0c205dd..bcecd7bbdc7 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java @@ -30,30 +30,30 @@ import java.util.Map; public final class PainlessLookup { public Collection getStructs() { - return javaClassesToPainlessStructs.values(); + return classesToPainlessClasses.values(); } - private final Map> painlessTypesToJavaClasses; - private final Map, PainlessClass> javaClassesToPainlessStructs; + private final Map> canonicalClassNamesToClasses; + private final Map, PainlessClass> classesToPainlessClasses; - PainlessLookup(Map> painlessTypesToJavaClasses, Map, PainlessClass> javaClassesToPainlessStructs) { - this.painlessTypesToJavaClasses = Collections.unmodifiableMap(painlessTypesToJavaClasses); - this.javaClassesToPainlessStructs = Collections.unmodifiableMap(javaClassesToPainlessStructs); + PainlessLookup(Map> canonicalClassNamesToClasses, Map, PainlessClass> classesToPainlessClasses) { + this.canonicalClassNamesToClasses = Collections.unmodifiableMap(canonicalClassNamesToClasses); + this.classesToPainlessClasses = Collections.unmodifiableMap(classesToPainlessClasses); } public Class getClassFromBinaryName(String painlessType) { - return painlessTypesToJavaClasses.get(painlessType.replace('$', '.')); + return canonicalClassNamesToClasses.get(painlessType.replace('$', '.')); } public boolean isSimplePainlessType(String painlessType) { - return painlessTypesToJavaClasses.containsKey(painlessType); + return canonicalClassNamesToClasses.containsKey(painlessType); } public PainlessClass getPainlessStructFromJavaClass(Class clazz) { - return javaClassesToPainlessStructs.get(clazz); + return classesToPainlessClasses.get(clazz); } public Class getJavaClassFromPainlessType(String painlessType) { - return PainlessLookupUtility.canonicalTypeNameToType(painlessType, painlessTypesToJavaClasses); + return PainlessLookupUtility.canonicalTypeNameToType(painlessType, canonicalClassNamesToClasses); } } diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java index 8945c956c27..b15f1f13f20 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java @@ -37,7 +37,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Stack; import java.util.regex.Pattern; import static org.elasticsearch.painless.lookup.PainlessLookupUtility.CONSTRUCTOR_NAME; @@ -666,179 +665,6 @@ public class PainlessLookupBuilder { } } - private void copyStruct(String struct, List children) { - final PainlessClassBuilder owner = classesToPainlessClassBuilders.get(canonicalClassNamesToClasses.get(struct)); - - if (owner == null) { - throw new IllegalArgumentException("Owner struct [" + struct + "] not defined for copy."); - } - - for (int count = 0; count < children.size(); ++count) { - final PainlessClassBuilder child = - classesToPainlessClassBuilders.get(canonicalClassNamesToClasses.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 kvPair : child.methods.entrySet()) { - String methodKey = kvPair.getKey(); - PainlessMethod method = kvPair.getValue(); - if (owner.methods.get(methodKey) == null) { - // TODO: some of these are no longer valid or outright don't work - // TODO: since classes may not come from the Painless classloader - // TODO: and it was dependent on the order of the extends which - // TODO: which no longer exists since this is generated automatically - // 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 != null) { - impl = method.augmentation; - 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 (PainlessField field : child.members.values()) { - if (owner.members.get(field.name) == null) { - owner.members.put(field.name, new PainlessField( - field.name, field.javaName, owner.clazz, field.clazz, field.modifiers, field.getter, field.setter)); - } - } - } - } - - /** - * Precomputes a more efficient structure for dynamic method/field access. - */ - private void addRuntimeClass(final PainlessClassBuilder struct) { - // add all getters/setters - for (Map.Entry method : struct.methods.entrySet()) { - String name = method.getValue().name; - PainlessMethod m = method.getValue(); - - if (m.arguments.size() == 0 && - name.startsWith("get") && - name.length() > 3 && - Character.isUpperCase(name.charAt(3))) { - StringBuilder newName = new StringBuilder(); - newName.append(Character.toLowerCase(name.charAt(3))); - newName.append(name.substring(4)); - struct.getters.putIfAbsent(newName.toString(), m.handle); - } else if (m.arguments.size() == 0 && - name.startsWith("is") && - name.length() > 2 && - Character.isUpperCase(name.charAt(2))) { - StringBuilder newName = new StringBuilder(); - newName.append(Character.toLowerCase(name.charAt(2))); - newName.append(name.substring(3)); - struct.getters.putIfAbsent(newName.toString(), m.handle); - } - - if (m.arguments.size() == 1 && - name.startsWith("set") && - name.length() > 3 && - Character.isUpperCase(name.charAt(3))) { - StringBuilder newName = new StringBuilder(); - newName.append(Character.toLowerCase(name.charAt(3))); - newName.append(name.substring(4)); - struct.setters.putIfAbsent(newName.toString(), m.handle); - } - } - - // add all members - for (Map.Entry member : struct.members.entrySet()) { - struct.getters.put(member.getKey(), member.getValue().getter); - struct.setters.put(member.getKey(), member.getValue().setter); - } - } - - /** computes the functional interface method for a class, or returns null */ - private PainlessMethod computeFunctionalInterfaceMethod(PainlessClassBuilder 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 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); - PainlessMethod painless = clazz.methods.get(buildPainlessMethodKey(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; - } - public PainlessLookup build() { String origin = "internal error"; @@ -849,11 +675,11 @@ public class PainlessLookupBuilder { for (WhitelistClass whitelistStruct : whitelist.whitelistStructs) { String painlessTypeName = whitelistStruct.javaClassName.replace('$', '.'); PainlessClassBuilder painlessStruct = - classesToPainlessClassBuilders.get(canonicalClassNamesToClasses.get(painlessTypeName)); + classesToPainlessClassBuilders.get(canonicalClassNamesToClasses.get(painlessTypeName)); if (painlessStruct != null && painlessStruct.clazz.getName().equals(whitelistStruct.javaClassName) == false) { throw new IllegalArgumentException("struct [" + painlessStruct.name + "] cannot represent multiple classes " + - "[" + painlessStruct.clazz.getName() + "] and [" + whitelistStruct.javaClassName + "]"); + "[" + painlessStruct.clazz.getName() + "] and [" + whitelistStruct.javaClassName + "]"); } origin = whitelistStruct.origin; @@ -894,78 +720,144 @@ public class PainlessLookupBuilder { throw new IllegalArgumentException("error loading whitelist(s) " + origin, exception); } - // goes through each Painless struct and determines the inheritance list, - // and then adds all inherited types to the Painless struct's whitelist + copyPainlessClassMembers(); + cacheRuntimeHandles(); + setFunctionalInterfaceMethods(); + + Map, PainlessClass> classesToPainlessClasses = new HashMap<>(classesToPainlessClassBuilders.size()); + + for (Map.Entry, PainlessClassBuilder> painlessClassBuilderEntry : classesToPainlessClassBuilders.entrySet()) { + classesToPainlessClasses.put(painlessClassBuilderEntry.getKey(), painlessClassBuilderEntry.getValue().build()); + } + + return new PainlessLookup(canonicalClassNamesToClasses, classesToPainlessClasses); + } + + private void copyPainlessClassMembers() { + for (Class parentClass : classesToPainlessClassBuilders.keySet()) { + copyPainlessInterfaceMembers(parentClass, parentClass); + + Class childClass = parentClass.getSuperclass(); + + while (childClass != null) { + if (classesToPainlessClassBuilders.containsKey(childClass)) { + copyPainlessClassMembers(childClass, parentClass); + } + + copyPainlessInterfaceMembers(childClass, parentClass); + childClass = childClass.getSuperclass(); + } + } + for (Class javaClass : classesToPainlessClassBuilders.keySet()) { - PainlessClassBuilder painlessStruct = classesToPainlessClassBuilders.get(javaClass); + if (javaClass.isInterface()) { + copyPainlessClassMembers(Object.class, javaClass); + } + } + } - List painlessSuperStructs = new ArrayList<>(); - Class javaSuperClass = painlessStruct.clazz.getSuperclass(); - - Stack> javaInteraceLookups = new Stack<>(); - javaInteraceLookups.push(painlessStruct.clazz); - - // adds super classes to the inheritance list - if (javaSuperClass != null && javaSuperClass.isInterface() == false) { - while (javaSuperClass != null) { - PainlessClassBuilder painlessSuperStruct = classesToPainlessClassBuilders.get(javaSuperClass); - - if (painlessSuperStruct != null) { - painlessSuperStructs.add(painlessSuperStruct.name); - } - - javaInteraceLookups.push(javaSuperClass); - javaSuperClass = javaSuperClass.getSuperclass(); - } + private void copyPainlessInterfaceMembers(Class parentClass, Class targetClass) { + for (Class childClass : parentClass.getInterfaces()) { + if (classesToPainlessClassBuilders.containsKey(childClass)) { + copyPainlessClassMembers(childClass, targetClass); } - // adds all super interfaces to the inheritance list - while (javaInteraceLookups.isEmpty() == false) { - Class javaInterfaceLookup = javaInteraceLookups.pop(); + copyPainlessInterfaceMembers(childClass, targetClass); + } + } - for (Class javaSuperInterface : javaInterfaceLookup.getInterfaces()) { - PainlessClassBuilder painlessInterfaceStruct = classesToPainlessClassBuilders.get(javaSuperInterface); + private void copyPainlessClassMembers(Class originalClass, Class targetClass) { + PainlessClassBuilder originalPainlessClassBuilder = classesToPainlessClassBuilders.get(originalClass); + PainlessClassBuilder targetPainlessClassBuilder = classesToPainlessClassBuilders.get(targetClass); - if (painlessInterfaceStruct != null) { - String painlessInterfaceStructName = painlessInterfaceStruct.name; + Objects.requireNonNull(originalPainlessClassBuilder); + Objects.requireNonNull(targetPainlessClassBuilder); - if (painlessSuperStructs.contains(painlessInterfaceStructName) == false) { - painlessSuperStructs.add(painlessInterfaceStructName); - } + for (Map.Entry painlessMethodEntry : originalPainlessClassBuilder.methods.entrySet()) { + String painlessMethodKey = painlessMethodEntry.getKey(); + PainlessMethod newPainlessMethod = painlessMethodEntry.getValue(); + PainlessMethod existingPainlessMethod = targetPainlessClassBuilder.methods.get(painlessMethodKey); - for (Class javaPushInterface : javaInterfaceLookup.getInterfaces()) { - javaInteraceLookups.push(javaPushInterface); - } + if (existingPainlessMethod == null || existingPainlessMethod.target != newPainlessMethod.target && + existingPainlessMethod.target.isAssignableFrom(newPainlessMethod.target)) { + targetPainlessClassBuilder.methods.put(painlessMethodKey, newPainlessMethod); + } + } + + for (Map.Entry painlessFieldEntry : originalPainlessClassBuilder.members.entrySet()) { + String painlessFieldKey = painlessFieldEntry.getKey(); + PainlessField newPainlessField = painlessFieldEntry.getValue(); + PainlessField existingPainlessField = targetPainlessClassBuilder.members.get(painlessFieldKey); + + if (existingPainlessField == null || existingPainlessField.target != newPainlessField.target && + existingPainlessField.target.isAssignableFrom(newPainlessField.target)) { + targetPainlessClassBuilder.members.put(painlessFieldKey, newPainlessField); + } + } + } + + private void cacheRuntimeHandles() { + for (PainlessClassBuilder painlessClassBuilder : classesToPainlessClassBuilders.values()) { + cacheRuntimeHandles(painlessClassBuilder); + } + } + + private void cacheRuntimeHandles(PainlessClassBuilder painlessClassBuilder) { + for (PainlessMethod painlessMethod : painlessClassBuilder.methods.values()) { + String methodName = painlessMethod.name; + int typeParametersSize = painlessMethod.arguments.size(); + + if (typeParametersSize == 0 && methodName.startsWith("get") && methodName.length() > 3 && + Character.isUpperCase(methodName.charAt(3))) { + painlessClassBuilder.getters.putIfAbsent( + Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4), painlessMethod.handle); + } else if (typeParametersSize == 0 && methodName.startsWith("is") && methodName.length() > 2 && + Character.isUpperCase(methodName.charAt(2))) { + painlessClassBuilder.getters.putIfAbsent( + Character.toLowerCase(methodName.charAt(2)) + methodName.substring(3), painlessMethod.handle); + } else if (typeParametersSize == 1 && methodName.startsWith("set") && methodName.length() > 3 && + Character.isUpperCase(methodName.charAt(3))) { + painlessClassBuilder.setters.putIfAbsent( + Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4), painlessMethod.handle); + } + } + + for (PainlessField painlessField : painlessClassBuilder.members.values()) { + painlessClassBuilder.getters.put(painlessField.name, painlessField.getter); + painlessClassBuilder.setters.put(painlessField.name, painlessField.setter); + } + } + + private void setFunctionalInterfaceMethods() { + for (Map.Entry, PainlessClassBuilder> painlessClassBuilderEntry : classesToPainlessClassBuilders.entrySet()) { + setFunctionalInterfaceMethod(painlessClassBuilderEntry.getValue()); + } + } + + private void setFunctionalInterfaceMethod(PainlessClassBuilder painlessClassBuilder) { + Class targetClass = painlessClassBuilder.clazz; + + if (targetClass.isInterface()) { + List javaMethods = new ArrayList<>(); + + for (java.lang.reflect.Method javaMethod : targetClass.getMethods()) { + if (javaMethod.isDefault() == false && Modifier.isStatic(javaMethod.getModifiers()) == false) { + try { + Object.class.getMethod(javaMethod.getName(), javaMethod.getParameterTypes()); + } catch (ReflectiveOperationException roe) { + javaMethods.add(javaMethod); } } } - // copies methods and fields from super structs to the parent struct - copyStruct(painlessStruct.name, painlessSuperStructs); - - // copies methods and fields from Object into interface types - if (painlessStruct.clazz.isInterface() || (def.class.getSimpleName()).equals(painlessStruct.name)) { - PainlessClassBuilder painlessObjectStruct = classesToPainlessClassBuilders.get(Object.class); - - if (painlessObjectStruct != null) { - copyStruct(painlessStruct.name, Collections.singletonList(painlessObjectStruct.name)); - } + if (javaMethods.size() != 1 && targetClass.isAnnotationPresent(FunctionalInterface.class)) { + throw new IllegalArgumentException("class [" + typeToCanonicalTypeName(targetClass) + "] " + + "is illegally marked as a FunctionalInterface with java methods " + javaMethods); + } else if (javaMethods.size() == 1) { + java.lang.reflect.Method javaMethod = javaMethods.get(0); + String painlessMethodKey = buildPainlessMethodKey(javaMethod.getName(), javaMethod.getParameterCount()); + painlessClassBuilder.functionalMethod = painlessClassBuilder.methods.get(painlessMethodKey); } } - - // precompute runtime classes - for (PainlessClassBuilder painlessStruct : classesToPainlessClassBuilders.values()) { - addRuntimeClass(painlessStruct); - } - - Map, PainlessClass> javaClassesToPainlessClasses = new HashMap<>(); - - // copy all structs to make them unmodifiable for outside users: - for (Map.Entry,PainlessClassBuilder> entry : classesToPainlessClassBuilders.entrySet()) { - entry.getValue().functionalMethod = computeFunctionalInterfaceMethod(entry.getValue()); - javaClassesToPainlessClasses.put(entry.getKey(), entry.getValue().build()); - } - - return new PainlessLookup(canonicalClassNamesToClasses, javaClassesToPainlessClasses); } }