| /* |
| * 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 org.apache.drill.exec.expr.fn; |
| |
| import java.util.Arrays; |
| import java.util.List; |
| |
| import org.apache.drill.common.exceptions.DrillRuntimeException; |
| import org.apache.drill.common.exceptions.UserException; |
| import org.apache.drill.common.expression.ExpressionPosition; |
| import org.apache.drill.common.expression.FieldReference; |
| import org.apache.drill.common.expression.FunctionHolderExpression; |
| import org.apache.drill.common.expression.LogicalExpression; |
| import org.apache.drill.common.types.TypeProtos.MajorType; |
| import org.apache.drill.common.types.TypeProtos.MinorType; |
| import org.apache.drill.common.types.Types; |
| import org.apache.drill.exec.compile.bytecode.ScalarReplacementTypes; |
| import org.apache.drill.exec.compile.sig.SignatureHolder; |
| import org.apache.drill.exec.expr.ClassGenerator; |
| import org.apache.drill.exec.expr.ClassGenerator.BlockType; |
| import org.apache.drill.exec.expr.ClassGenerator.HoldingContainer; |
| import org.apache.drill.exec.expr.DrillFuncHolderExpr; |
| import org.apache.drill.exec.expr.EvaluationVisitor.VectorVariableHolder; |
| import org.apache.drill.exec.expr.TypeHelper; |
| import org.apache.drill.exec.expr.annotations.FunctionTemplate.NullHandling; |
| import org.apache.drill.exec.expr.fn.output.OutputWidthCalculator; |
| import org.apache.drill.exec.expr.holders.UnionHolder; |
| import org.apache.drill.exec.expr.holders.ValueHolder; |
| import org.apache.drill.exec.ops.UdfUtilities; |
| import org.apache.drill.exec.vector.complex.reader.FieldReader; |
| import com.google.common.base.Preconditions; |
| import com.google.common.base.Strings; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import com.sun.codemodel.JAssignmentTarget; |
| import com.sun.codemodel.JBlock; |
| import com.sun.codemodel.JCodeModel; |
| import com.sun.codemodel.JExpr; |
| import com.sun.codemodel.JExpression; |
| import com.sun.codemodel.JInvocation; |
| import com.sun.codemodel.JType; |
| import com.sun.codemodel.JVar; |
| |
| public abstract class DrillFuncHolder extends AbstractFuncHolder { |
| private static final Logger logger = LoggerFactory.getLogger(DrillFuncHolder.class); |
| |
| private final FunctionAttributes attributes; |
| private final FunctionInitializer initializer; |
| |
| public DrillFuncHolder( |
| FunctionAttributes attributes, |
| FunctionInitializer initializer) { |
| this.attributes = attributes; |
| this.initializer = initializer; |
| |
| checkNullHandling(attributes.getNullHandling()); |
| } |
| |
| /** |
| * Check if function type supports provided null handling strategy. |
| * <p> |
| * Keep in mind that this method is invoked in |
| * {@link #DrillFuncHolder(FunctionAttributes, FunctionInitializer)} |
| * constructor so make sure not to use any state fields when overriding the |
| * method to avoid uninitialized state. |
| * </p> |
| * |
| * @param nullHandling |
| * null handling strategy defined for a function |
| * @throws IllegalArgumentException |
| * if provided {@code nullHandling} is not supported |
| */ |
| protected void checkNullHandling(NullHandling nullHandling) { |
| } |
| |
| protected String meth(String methodName) { |
| return meth(methodName, true); |
| } |
| |
| protected String meth(String methodName, boolean required) { |
| String method = initializer.getMethod(methodName); |
| if (method == null) { |
| if (!required) { |
| return ""; |
| } |
| throw UserException |
| .functionError() |
| .message("Failure while trying use function. No body found for required method %s.", methodName) |
| .addContext("FunctionClass", initializer.getClassName()) |
| .build(logger); |
| } |
| return method; |
| } |
| |
| @Override |
| public JVar[] renderStart(ClassGenerator<?> g, HoldingContainer[] inputVariables, FieldReference fieldReference) { |
| return declareWorkspaceVariables(g); |
| } |
| |
| @Override |
| public FunctionHolderExpression getExpr(String name, List<LogicalExpression> args, ExpressionPosition pos) { |
| return new DrillFuncHolderExpr(name, this, args, pos); |
| } |
| |
| public boolean isAggregating() { |
| return false; |
| } |
| |
| public boolean isDeterministic() { |
| return attributes.isDeterministic(); |
| } |
| |
| public boolean isNiladic() { |
| return attributes.isNiladic(); |
| } |
| |
| public boolean isInternal() { |
| return attributes.isInternal(); |
| } |
| |
| public boolean isVarArg() { |
| return attributes.isVarArg(); |
| } |
| |
| /** |
| * Generates string representation of function input parameters: |
| * PARAMETER_TYPE_1-PARAMETER_MODE_1,PARAMETER_TYPE_2-PARAMETER_MODE_2 |
| * Example: VARCHAR-REQUIRED,VARCHAR-OPTIONAL |
| * Returns empty string if function has no input parameters. |
| * |
| * @return string representation of function input parameters |
| */ |
| public String getInputParameters() { |
| StringBuilder builder = new StringBuilder(); |
| for (ValueReference ref : attributes.getParameters()) { |
| MajorType type = ref.getType(); |
| builder.append(","); |
| builder.append(type.getMinorType().toString()); |
| builder.append("-"); |
| builder.append(type.getMode().toString()); |
| } |
| if (isVarArg() && getParamCount() > 0) { |
| builder.append("..."); |
| } |
| return builder.length() == 0 ? builder.toString() : builder.substring(1); |
| } |
| |
| /** |
| * @return instance of class loader used to load function |
| */ |
| public ClassLoader getClassLoader() { |
| return initializer.getClassLoader(); |
| } |
| |
| protected JVar[] declareWorkspaceVariables(ClassGenerator<?> g) { |
| JVar[] workspaceJVars = new JVar[attributes.getWorkspaceVars().length]; |
| for (int i = 0; i < attributes.getWorkspaceVars().length; i++) { |
| WorkspaceReference ref = attributes.getWorkspaceVars()[i]; |
| JType jtype = g.getModel()._ref(ref.getType()); |
| workspaceJVars[i] = g.declareClassField("work", jtype); |
| |
| if (ScalarReplacementTypes.CLASSES.contains(ref.getType())) { |
| JBlock b = g.getBlock(SignatureHolder.DRILL_INIT_METHOD); |
| b.assign(workspaceJVars[i], JExpr._new(jtype)); |
| } |
| |
| if (ref.isInject()) { |
| assignInjectableValue(g, workspaceJVars[i], ref); |
| } |
| } |
| return workspaceJVars; |
| } |
| |
| protected void assignInjectableValue(ClassGenerator<?> g, JVar variable, WorkspaceReference ref) { |
| if (UdfUtilities.INJECTABLE_GETTER_METHODS.get(ref.getType()) != null) { |
| g.getBlock(BlockType.SETUP).assign( |
| variable, |
| g.getMappingSet().getIncoming().invoke("getContext").invoke( |
| UdfUtilities.INJECTABLE_GETTER_METHODS.get(ref.getType()) |
| )); |
| } else { |
| // Invalid injectable type provided, this should have been caught in FunctionConverter |
| throw new DrillRuntimeException("Invalid injectable type requested in UDF: " + |
| ref.getType().getSimpleName()); |
| } |
| } |
| |
| /** |
| * Generate the body of a Drill function by copying the source code of the |
| * corresponding function method into the generated output. For this to work, |
| * all symbol references must be absolute (they cannot refer to imports), |
| * or they must refer to local variables or class fields marked with an |
| * annotation. |
| * <p> |
| * To make this work, the function body is wrapped in a code block that |
| * simulates the class fields by declaring local variables of the same |
| * name, and assigning those variables based on the input and workspace |
| * variables provided. |
| * <p> |
| * This version is used for blocks other than the main eval block. |
| * |
| * @param g code generator |
| * @param bt type of the block to be added |
| * @param body source code of the block. Optional. Block will be omitted |
| * if the method body is null or empty |
| * @param inputVariables list of input variable bindings which match up to declared |
| * <code>@Param</code> member variables in order. The input variables have the |
| * same name as the parameters that they represent |
| * @param workspaceJVars list of workspace variables, structures the same as |
| * input variables |
| * @param workspaceOnly <code>true</code> if this is a setup block and |
| * we should declare only constant workspace variables, <code>false</code> to |
| * declare all variables |
| */ |
| protected void generateBody(ClassGenerator<?> g, BlockType bt, String body, HoldingContainer[] inputVariables, |
| JVar[] workspaceJVars, boolean workspaceOnly) { |
| if (Strings.isNullOrEmpty(body) || body.trim().isEmpty()) { |
| return; |
| } |
| g.getBlock(bt).directStatement(String.format( |
| "/** start %s for function %s **/ ", bt.name(), attributes.getRegisteredNames()[0])); |
| |
| JBlock sub = new JBlock(true, true); |
| addProtectedBlock(g, sub, body, inputVariables, workspaceJVars, workspaceOnly); |
| g.getBlock(bt).add(sub); |
| |
| g.getBlock(bt).directStatement(String.format( |
| "/** end %s for function %s **/ ", bt.name(), attributes.getRegisteredNames()[0])); |
| } |
| |
| /** |
| * Generate the function block itself, without surrounding comments, and |
| * whether or not the method is empty. |
| */ |
| protected void addProtectedBlock(ClassGenerator<?> g, JBlock sub, String body, |
| HoldingContainer[] inputVariables, |
| JVar[] workspaceJVars, boolean workspaceOnly) { |
| |
| // Create the binding between simulated function fields and their values, |
| if (inputVariables != null) { |
| // If VarArgs, all input variables go into a single array. |
| if (isVarArg()) { |
| declareVarArgArray(g.getModel(), sub, inputVariables); |
| } |
| for (int i = 0; i < inputVariables.length; i++) { |
| if (workspaceOnly && !inputVariables[i].isConstant()) { |
| continue; |
| } |
| declareInputVariable(g.getModel(), sub, inputVariables[i], i); |
| } |
| } |
| |
| // Declare workspace variables |
| JVar[] internalVars = new JVar[workspaceJVars.length]; |
| for (int i = 0; i < workspaceJVars.length; i++) { |
| internalVars[i] = sub.decl( |
| g.getModel()._ref(attributes.getWorkspaceVars()[i].getType()), |
| attributes.getWorkspaceVars()[i].getName(), workspaceJVars[i]); |
| } |
| |
| // Add the source code for the block. |
| Preconditions.checkNotNull(body); |
| sub.directStatement(body); |
| |
| // reassign workspace variables back to global space. |
| for (int i = 0; i < workspaceJVars.length; i++) { |
| sub.assign(workspaceJVars[i], internalVars[i]); |
| } |
| } |
| |
| /** |
| * Declares array for storing vararg function arguments. |
| * |
| * @param model code model to generate the code |
| * @param jBlock block of code to be populated |
| * @param inputVariables array of input variables for current function |
| */ |
| protected void declareVarArgArray(JCodeModel model, JBlock jBlock, HoldingContainer[] inputVariables) { |
| ValueReference parameter = getAttributeParameter(getParamCount() - 1); |
| JType defaultType; |
| if (inputVariables.length >= getParamCount()) { |
| defaultType = inputVariables[inputVariables.length - 1].getHolder().type(); |
| } else if (parameter.getType().getMinorType() == MinorType.LATE) { |
| defaultType = model._ref(ValueHolder.class); |
| } else { |
| defaultType = TypeHelper.getHolderType(model, parameter.getType().getMinorType(), parameter.getType().getMode()); |
| } |
| JType paramClass = getParamClass(model, parameter, defaultType); |
| jBlock.decl(paramClass.array(), parameter.getName(), JExpr.newArray(paramClass, inputVariables.length - getParamCount() + 1)); |
| } |
| |
| /** |
| * Generate the top part of a function call which simulates passing parameters |
| * into the function. Given the following declaration: <code><pre> |
| * public static class UnionIsBigInt implements DrillSimpleFunc { |
| * @Param UnionHolder in; |
| * </pre></code>, we generate code like the following: <code><pre> |
| * final BitHolder out = new BitHolder(); |
| * FieldReader in = reader4; |
| * </pre></code> |
| * <p> |
| * Declares attribute parameter which corresponds to specified {@code currentIndex} |
| * in specified {@code jBlock}. Parameters are those fields |
| * in the function declaration annotated with a <tt>@Param</tt> tag. Parameters |
| * or expressions can both be represented by either a <code>FieldReader</code> |
| * or a value holder. Perform conversion between the two as needed. |
| * |
| * @param model code model to generate the code |
| * @param jBlock block of code to be populated |
| * @param inputVariable input variable for current function |
| * @param currentIndex index of current parameter |
| */ |
| protected void declareInputVariable(JCodeModel model, JBlock jBlock, |
| HoldingContainer inputVariable, int currentIndex) { |
| ValueReference parameter = getAttributeParameter(currentIndex); |
| if (!inputVariable.isReader() && parameter.isFieldReader()) { |
| convertHolderToReader(model, jBlock, inputVariable, currentIndex, parameter); |
| } else if (inputVariable.isReader() && !parameter.isFieldReader()) { |
| convertReaderToHolder(model, jBlock, inputVariable, currentIndex, parameter); |
| } else { |
| assignParamDirectly(jBlock, inputVariable, currentIndex, parameter); |
| } |
| } |
| |
| /** |
| * Convert an input variable (in the generated code) to a reader as declared |
| * on the input parameter. |
| */ |
| private void convertHolderToReader(JCodeModel model, JBlock jBlock, |
| HoldingContainer inputVariable, int currentIndex, |
| ValueReference parameter) { |
| JVar inputHolder = inputVariable.getHolder(); |
| MajorType inputSqlType = inputVariable.getMajorType(); |
| |
| // The parameter is a reader in this case. |
| JType paramClass = model._ref(FieldReader.class); |
| if (Types.isComplex(inputSqlType)) { |
| throw new UnsupportedOperationException(String.format( |
| "Cannot convert values of type %s from a holder to a reader", |
| inputSqlType.getMinorType().name())); |
| } else if (Types.isUnion(inputSqlType)) { |
| // For the UNION type, there is no simple conversion from |
| // a value holder to a reader. |
| // |
| // Prior to the fix for DRILL-7502, the parameter is redefined |
| // from type FieldReader to type UnionHolder. This is clearly |
| // wrong, but it worked in most cases. It DOES NOT work for the |
| // typeof() function, which needs a reader. Old code: |
| |
| // assignParamDirectly(jBlock, inputVariable, currentIndex, parameter); |
| |
| // A large amount of code depends on the UNION being represented |
| // as a UnionHolder, especially that for handling varying types. |
| // ExpressionTreeMaterializer.rewriteUnionFunction(), for example |
| // relies heavily on UnionHolder. As a result, we cannot simply |
| // change the "holder" for the UNION to be a reader, as is done |
| // for complex types. |
| // |
| // Run TestTopNSchemaChanges.testNumericTypes with saving of code |
| // enabled, and look at the second "PriorityQueueGen" for an example. |
| |
| // One would think that the following should work: the UnionHolder |
| // has a reader field. However, that field is not set in the code |
| // gen path; only when creating a holder from a reader. Code which |
| // does NOT work: |
| |
| // jBlock.decl(paramClass, parameter.getName(), inputHolder.ref("reader")); |
| |
| // One solution that works (but is probably slow and redundant) is to |
| // obtain the reader directly from the underling value vector. We saved |
| // this information when defining the holder. We retrieve it here |
| // and insert the vector --> reader conversion. |
| VectorVariableHolder vvHolder = (VectorVariableHolder) inputVariable; |
| JVar readerVar = vvHolder.generateUnionReader(); |
| declare(jBlock, parameter, paramClass, readerVar, currentIndex); |
| |
| // TODO: This probably needs a more elegant solution, but this does |
| // work for now. Run TestTypeFns.testUnionType to verify. |
| } else { |
| // FooHolderReader param = new FooHolderReader(inputVar); |
| JType singularReaderClass = model._ref( |
| TypeHelper.getHolderReaderImpl(inputSqlType.getMinorType(), |
| inputSqlType.getMode())); |
| JInvocation reader = JExpr._new(singularReaderClass).arg(inputHolder); |
| declare(jBlock, parameter, paramClass, reader, currentIndex); |
| } |
| } |
| |
| /** |
| * Convert an input value holder (in the generated code) into a value |
| * holder as declared on the input parameter. |
| */ |
| private void convertReaderToHolder(JCodeModel model, JBlock jBlock, |
| HoldingContainer inputVariable, int currentIndex, |
| ValueReference parameter) { |
| JVar inputHolder = inputVariable.getHolder(); |
| MajorType inputSqlType = inputVariable.getMajorType(); |
| if (Types.isComplex(parameter.getType())) { |
| // For complex data-types (repeated maps/lists/dicts) the input to the |
| // aggregate will be a FieldReader. However, aggregate |
| // functions like ANY_VALUE, will assume the input to be a |
| // RepeatedMapHolder etc. Generate boilerplate code, to map |
| // from FieldReader to respective Holder. |
| |
| // FooHolder param = new FooHolder(); |
| // param.reader = inputVar; |
| JType holderClass = getParamClass(model, parameter, inputHolder.type()); |
| JAssignmentTarget holderVar = declare(jBlock, parameter, holderClass, JExpr._new(holderClass), currentIndex); |
| jBlock.assign(holderVar.ref("reader"), inputHolder); |
| } else if (Types.isUnion(inputSqlType)) { |
| // Normally unions are generated as a UnionHolder. However, if a parameter |
| // is a FieldReader, then we must generate the union as a UnionReader. |
| // Then, if there is another function that use a holder, we can convert |
| // from the UnionReader to a UnionHolder. |
| // |
| // UnionHolder param = new UnionHolder(); |
| // inputVar.read(param); |
| JType holderClass = model._ref(UnionHolder.class); |
| JAssignmentTarget paramVar = jBlock.decl(holderClass, parameter.getName(), JExpr._new(holderClass)); |
| JInvocation readCall = inputHolder.invoke("read"); |
| readCall.arg(paramVar); |
| jBlock.add(readCall); |
| } else { |
| throw new UnsupportedOperationException(String.format( |
| "Cannot convert values of type %s from a reader to a holder", |
| inputSqlType.getMinorType().name())); |
| } |
| } |
| |
| /** |
| * The input variable and parameter are both either a holder or a |
| * field reader. |
| */ |
| private void assignParamDirectly(JBlock jBlock, |
| HoldingContainer inputVariable, int currentIndex, |
| ValueReference parameter) { |
| |
| // Declare parameter as the type of the input variable because |
| // if we get here, they must be the same type. |
| // |
| // InputType param = inputVar; |
| JVar inputHolder = inputVariable.getHolder(); |
| declare(jBlock, parameter, inputHolder.type(), |
| inputHolder, currentIndex); |
| } |
| |
| /** |
| * Returns {@link JType} instance which corresponds to the parameter of the function. |
| * |
| * @param model code model to generate the code |
| * @param parameter function parameter which determines resulting type |
| * @param defaultType type to be returned for the case when parameter does not hold specific type |
| * @return {@link JType} instance which corresponds to the parameter of the function |
| */ |
| private JType getParamClass(JCodeModel model, ValueReference parameter, JType defaultType) { |
| if (parameter.isFieldReader()) { |
| return model._ref(FieldReader.class); |
| } |
| |
| if (Types.isComplex(parameter.getType())) { |
| MajorType type = parameter.getType(); |
| return TypeHelper.getComplexHolderType(model, type.getMinorType(), type.getMode()); |
| } |
| |
| return defaultType; |
| } |
| |
| /** |
| * Declares specified {@code paramExpression} in specified {@code jBlock} |
| * and assigns it to the array component if required and / or returns declared expression. |
| * |
| * @param jBlock target block where declaration is added |
| * @param parameter function parameter which should be declared |
| * @param paramClass type of the declared variable |
| * @param paramExpression expression to be declared |
| * @param currentIndex index of current parameter |
| * @return declared expression |
| */ |
| protected JAssignmentTarget declare(JBlock jBlock, ValueReference parameter, |
| JType paramClass, JExpression paramExpression, int currentIndex) { |
| if (parameter.isVarArg()) { |
| // For VarArg, an array has already been generated: |
| |
| // FieldReader[] in = new FieldReader[ 4 ] ; |
| |
| // Here we assign the input value to the array: |
| |
| // in[ 0 ] = new VarCharHolderReaderImpl(constant1); |
| JAssignmentTarget arrayComponent = JExpr.ref(parameter.getName()).component(JExpr.lit(currentIndex - getParamCount() + 1)); |
| jBlock.assign(arrayComponent, paramExpression); |
| return arrayComponent; |
| } else { |
| // Actual define the input variable with an expression. |
| // Foo in = bar12; |
| |
| return jBlock.decl(paramClass, parameter.getName(), paramExpression); |
| } |
| } |
| |
| public boolean matches(MajorType returnType, List<MajorType> argTypes) { |
| |
| if (!softCompare(returnType, attributes.getReturnValue().getType())) { |
| return false; |
| } |
| |
| if (argTypes.size() != attributes.getParameters().length) { |
| return false; |
| } |
| |
| for (int i = 0; i < attributes.getParameters().length; i++) { |
| if (!softCompare(getAttributeParameter(i).getType(), argTypes.get(i))) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| @Override |
| public MajorType getParamMajorType(int i) { |
| return getAttributeParameter(i).getType(); |
| } |
| |
| @Override |
| public int getParamCount() { |
| return attributes.getParameters().length; |
| } |
| |
| public boolean isConstant(int i) { |
| return getAttributeParameter(i).isConstant(); |
| } |
| |
| /** |
| * Returns i-th function attribute parameter. |
| * For the case when current function is vararg and specified index |
| * is greater than or equals to the attributes count, the last function attribute parameter is returned. |
| * |
| * @param i index of function attribute parameter to be returned |
| * @return i-th function attribute parameter |
| */ |
| public ValueReference getAttributeParameter(int i) { |
| if (i >= getParamCount() && attributes.isVarArg()) { |
| return attributes.getParameters()[getParamCount() - 1]; |
| } |
| return attributes.getParameters()[i]; |
| } |
| |
| public boolean isFieldReader(int i) { |
| return getAttributeParameter(i).isFieldReader(); |
| } |
| |
| public MajorType getReturnType(final List<LogicalExpression> logicalExpressions) { |
| return attributes.getReturnType().getType(logicalExpressions, attributes); |
| } |
| |
| public OutputWidthCalculator getOutputWidthCalculator() { |
| return attributes.getOutputWidthCalculatorType().getOutputWidthCalculator(); |
| } |
| |
| public int variableOutputSizeEstimate(){ |
| return attributes.variableOutputSizeEstimate(); |
| } |
| |
| public NullHandling getNullHandling() { |
| return attributes.getNullHandling(); |
| } |
| |
| private boolean softCompare(MajorType a, MajorType b) { |
| return Types.softEquals(a, b, getNullHandling() == NullHandling.NULL_IF_NULL); |
| } |
| |
| public String[] getRegisteredNames() { |
| return attributes.getRegisteredNames(); |
| } |
| |
| public int getCostCategory() { |
| return attributes.getCostCategory().getValue(); |
| } |
| |
| public ValueReference[] getParameters() { |
| return attributes.getParameters(); |
| } |
| |
| public boolean checkPrecisionRange() { |
| return attributes.checkPrecisionRange(); |
| } |
| |
| public MajorType getReturnType() { |
| return attributes.getReturnValue().getType(); |
| } |
| |
| public ValueReference getReturnValue() { |
| return attributes.getReturnValue(); |
| } |
| |
| public WorkspaceReference[] getWorkspaceVars() { |
| return attributes.getWorkspaceVars(); |
| } |
| |
| @Override |
| public String toString() { |
| final int maxLen = 10; |
| return this.getClass().getSimpleName() |
| + " [functionNames=" + Arrays.toString(attributes.getRegisteredNames()) |
| + ", returnType=" + Types.toString(attributes.getReturnValue().getType()) |
| + ", nullHandling=" + attributes.getNullHandling() |
| + ", parameters=" + (attributes.getParameters() != null ? |
| Arrays.asList(attributes.getParameters()).subList(0, Math.min(attributes.getParameters().length, maxLen)) : null) + "]"; |
| } |
| } |