/* * 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 java.lang.invoke.MethodType; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.function.Function; import static java.util.Collections.unmodifiableList; import static org.elasticsearch.painless.WriterConstants.USES_PARAMETER_METHOD_TYPE; /** * Information about the interface being implemented by the painless script. */ public class ScriptInterface { private final Class<?> iface; private final org.objectweb.asm.commons.Method executeMethod; private final Definition.Type executeMethodReturnType; private final List<MethodArgument> executeArguments; private final List<org.objectweb.asm.commons.Method> usesMethods; public ScriptInterface(Definition definition, Class<?> iface) { this.iface = iface; // Find the main method and the uses$argName methods java.lang.reflect.Method executeMethod = null; List<org.objectweb.asm.commons.Method> usesMethods = new ArrayList<>(); for (java.lang.reflect.Method m : iface.getMethods()) { if (m.isDefault()) { continue; } if (m.getName().equals("execute")) { if (executeMethod == null) { executeMethod = m; } else { throw new IllegalArgumentException( "Painless can only implement interfaces that have a single method named [execute] but [" + iface.getName() + "] has more than one."); } continue; } if (m.getName().startsWith("uses$")) { if (false == m.getReturnType().equals(boolean.class)) { throw new IllegalArgumentException("Painless can only implement uses$ methods that return boolean but [" + iface.getName() + "#" + m.getName() + "] returns [" + m.getReturnType().getName() + "]."); } if (m.getParameterTypes().length > 0) { throw new IllegalArgumentException("Painless can only implement uses$ methods that do not take parameters but [" + iface.getName() + "#" + m.getName() + "] does."); } usesMethods.add(new org.objectweb.asm.commons.Method(m.getName(), USES_PARAMETER_METHOD_TYPE.toMethodDescriptorString())); continue; } throw new IllegalArgumentException("Painless can only implement methods named [execute] and [uses$argName] but [" + iface.getName() + "] contains a method named [" + m.getName() + "]"); } MethodType methodType = MethodType.methodType(executeMethod.getReturnType(), executeMethod.getParameterTypes()); this.executeMethod = new org.objectweb.asm.commons.Method(executeMethod.getName(), methodType.toMethodDescriptorString()); executeMethodReturnType = definitionTypeForClass(definition, executeMethod.getReturnType(), componentType -> "Painless can only implement execute methods returning a whitelisted type but [" + iface.getName() + "#execute] returns [" + componentType.getName() + "] which isn't whitelisted."); // Look up the argument names Set<String> argumentNames = new LinkedHashSet<>(); List<MethodArgument> arguments = new ArrayList<>(); String[] argumentNamesConstant = readArgumentNamesConstant(iface); Class<?>[] types = executeMethod.getParameterTypes(); if (argumentNamesConstant.length != types.length) { throw new IllegalArgumentException("[" + iface.getName() + "#ARGUMENTS] has length [2] but [" + iface.getName() + "#execute] takes [1] argument."); } for (int arg = 0; arg < types.length; arg++) { arguments.add(methodArgument(definition, types[arg], argumentNamesConstant[arg])); argumentNames.add(argumentNamesConstant[arg]); } this.executeArguments = unmodifiableList(arguments); // Validate that the uses$argName methods reference argument names for (org.objectweb.asm.commons.Method usesMethod : usesMethods) { if (false == argumentNames.contains(usesMethod.getName().substring("uses$".length()))) { throw new IllegalArgumentException("Painless can only implement uses$ methods that match a parameter name but [" + iface.getName() + "#" + usesMethod.getName() + "] doesn't match any of " + argumentNames + "."); } } this.usesMethods = unmodifiableList(usesMethods); } /** * The interface that the Painless script should implement. */ public Class<?> getInterface() { return iface; } /** * An asm method descriptor for the {@code execute} method. */ public org.objectweb.asm.commons.Method getExecuteMethod() { return executeMethod; } /** * The Painless {@link Definition.Type} or the return type of the {@code execute} method. This is used to generate the appropriate * return bytecode. */ public Definition.Type getExecuteMethodReturnType() { return executeMethodReturnType; } /** * Painless {@link Definition.Type}s and names of the arguments to the {@code execute} method. The names are exposed to the Painless * script. */ public List<MethodArgument> getExecuteArguments() { return executeArguments; } /** * The {@code uses$varName} methods that must be implemented by Painless to complete implementing the interface. */ public List<org.objectweb.asm.commons.Method> getUsesMethods() { return usesMethods; } /** * Painless {@link Definition.Type}s and name of the argument to the {@code execute} method. */ public static class MethodArgument { private final Definition.Type type; private final String name; public MethodArgument(Definition.Type type, String name) { this.type = type; this.name = name; } public Definition.Type getType() { return type; } public String getName() { return name; } } private MethodArgument methodArgument(Definition definition, Class<?> type, String argName) { Definition.Type defType = definitionTypeForClass(definition, type, componentType -> "[" + argName + "] is of unknown type [" + componentType.getName() + ". Painless interfaces can only accept arguments that are of whitelisted types."); return new MethodArgument(defType, argName); } private static Definition.Type definitionTypeForClass(Definition definition, Class<?> type, Function<Class<?>, String> unknownErrorMessageSource) { int dimensions = 0; Class<?> componentType = type; while (componentType.isArray()) { dimensions++; componentType = componentType.getComponentType(); } Definition.Struct struct; if (componentType.equals(Object.class)) { struct = Definition.DEF_TYPE.struct; } else { Definition.RuntimeClass runtimeClass = definition.getRuntimeClass(componentType); if (runtimeClass == null) { throw new IllegalArgumentException(unknownErrorMessageSource.apply(componentType)); } struct = runtimeClass.getStruct(); } return definition.getType(struct, dimensions); } private static String[] readArgumentNamesConstant(Class<?> iface) { Field argumentNamesField; try { argumentNamesField = iface.getField("ARGUMENTS"); } catch (NoSuchFieldException e) { throw new IllegalArgumentException("Painless needs a constant [String[] ARGUMENTS] on all interfaces it implements with the " + "names of the method arguments but [" + iface.getName() + "] doesn't have one.", e); } if (false == argumentNamesField.getType().equals(String[].class)) { throw new IllegalArgumentException("Painless needs a constant [String[] ARGUMENTS] on all interfaces it implements with the " + "names of the method arguments but [" + iface.getName() + "] doesn't have one."); } try { return (String[]) argumentNamesField.get(null); } catch (IllegalArgumentException | IllegalAccessException e) { throw new IllegalArgumentException("Error trying to read [" + iface.getName() + "#ARGUMENTS]", e); } } }