All Downloads are FREE. Search and download functionalities are using the official Maven repository.

us.abstracta.jmeter.javadsl.codegeneration.MethodCall Maven / Gradle / Ivy

Go to download

Simple API to run JMeter performance tests in an VCS and programmers friendly way.

There is a newer version: 028
Show newest version
package us.abstracta.jmeter.javadsl.codegeneration;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.http.entity.ContentType;
import org.apache.jorphan.collections.HashTree;
import us.abstracta.jmeter.javadsl.codegeneration.MethodParam.ChildrenParam;
import us.abstracta.jmeter.javadsl.core.BuildTreeContext;
import us.abstracta.jmeter.javadsl.core.DslTestElement;
import us.abstracta.jmeter.javadsl.core.DslTestPlan;
import us.abstracta.jmeter.javadsl.core.testelements.MultiLevelTestElement;

/**
 * Represents a method call, it's parameters and chained invocations.
 * 

* It's main purpose is to generate the code for the method call, parameters and chained methods * invocations. * * @since 0.45 */ public class MethodCall { private static final String INDENT = " "; private static final MethodCall EMPTY_METHOD_CALL = new EmptyMethodCall(); protected final String methodName; private final Class returnType; private MethodCall childrenMethod; private ChildrenParam childrenParam; private final List> params; private final List chain = new ArrayList<>(); protected MethodCall(String methodName, Class returnType, MethodParam... params) { this.methodName = methodName; this.returnType = returnType; this.params = Arrays.asList(params); if (params.length > 0 && params[params.length - 1] instanceof ChildrenParam) { childrenMethod = this; childrenParam = (ChildrenParam) params[params.length - 1]; } } protected static MethodCall from(Method method, MethodParam... params) { return new MethodCall(method.getName(), method.getReturnType(), params); } /** * Generates a new instance for a static method within a given class that is applicable to a given * set of parameters. *

* This is usually used to get clas factory methods calls. Eg: Duration.ofSeconds. * * @param methodClass the class that contains the static method. * @param methodName the name of the method to search for in the given class. * @param params the parameters used to search the method in the given class and to associate * to the method call. * @return the newly created instance */ public static MethodCall forStaticMethod(Class methodClass, String methodName, MethodParam... params) { Class[] paramsTypes = Arrays.stream(params) .map(MethodParam::getType) .toArray(Class[]::new); Method method = MethodCall.findRequiredStaticMethod(methodClass, methodName, paramsTypes); return new MethodCall(methodClass.getSimpleName() + "." + method.getName(), method.getReturnType(), params); } private static Method findRequiredStaticMethod(Class methodClass, String methodName, Class... paramsTypes) { try { Method ret = methodClass.getDeclaredMethod(methodName, paramsTypes); if (!Modifier.isPublic(ret.getModifiers()) || !Modifier.isStatic(ret.getModifiers())) { throw new RuntimeException( "Can't access method " + ret + " which is no longer static or public. " + "Check that no dependencies or APIs have been changed."); } return ret; } catch (NoSuchMethodException e) { throw new RuntimeException( "Can't find method " + methodClass.getName() + "." + methodName + " for parameter types " + Arrays.toString(paramsTypes) + ". Check that no dependencies or APIs have been changed.", e); } } public static MethodCall buildUnsupported() { return new MethodCall("unsupported", UnsupportedTestElement.class); } private static class UnsupportedTestElement implements MultiLevelTestElement { public void children(DslTestElement... child) { } @Override public HashTree buildTreeUnder(HashTree parent, BuildTreeContext context) { return null; } @Override public void showInGui() { } } /** * Generates a method call that should be ignored (no code should be generated). *

* This is helpful when some MethodCallBuilder supports a given test element conversion, but no * associated generated DSL code should be included. * * @return the empty method call. */ public static MethodCall emptyCall() { return EMPTY_METHOD_CALL; } private static class EmptyMethodCall extends MethodCall { protected EmptyMethodCall() { super(null, MultiLevelTestElement.class); } @Override public String buildCode(String indent) { return ""; } } public Class getReturnType() { return returnType; } /** * Allows adding a child call to this call. *

* This method should only be used in seldom scenarios where you need to manually add children * calls. In most of the cases this is not necessary, since DSL framework automatically takes care * of JMeter children conversion. *

* If the call defines a {@link ChildrenParam} parameter, then children are just added as * parameters of the call. Otherwise, a children method will be looked into the class retunrned by * this method, and if there is, then chained into this call and used to register provided child * element. * * Warning: You should only use this method after applying any required chaining. * * @param child specifies the method call to be added as child call of this call. * @return the current call instance, or chained children method for further addition of children. */ public MethodCall child(MethodCall child) { if (childrenMethod == null) { childrenMethod = findChildrenMethod(); chain.add(childrenMethod); childrenParam = (ChildrenParam) childrenMethod.params.get(0); } childrenParam.addChild(child); return childrenMethod; } private MethodCall findChildrenMethod() { Method childrenMethod = null; Class methodHolder = returnType; while (childrenMethod == null && methodHolder != Object.class) { childrenMethod = Arrays.stream(methodHolder.getDeclaredMethods()) .filter(m -> Modifier.isPublic(m.getModifiers()) && "children".equals(m.getName()) && m.getParameterCount() == 1) .findAny() .orElse(null); methodHolder = methodHolder.getSuperclass(); } if (childrenMethod == null) { throw new IllegalStateException("No children method found for " + returnType + ". " + "This might be due to unexpected test plan structure or missing method in test element" + ". Please create an issue in GitHub repository if you find any of these cases."); } return new ChildrenMethodCall(childrenMethod); } private static class ChildrenMethodCall extends MethodCall { protected ChildrenMethodCall(Method method) { super(method.getName(), method.getReturnType(), new ChildrenParam<>(method.getParameterTypes()[0])); } @Override protected String buildCode(String indent) { String paramsCode = buildParamsCode(indent + INDENT); return paramsCode.isEmpty() ? "" : methodName + "(" + paramsCode + indent + ")"; } } /** * Allows chaining a method call to this call. *

* This method is useful when adding property configuration methods (like {@link * DslTestPlan#sequentialThreadGroups()}) or other chained methods that further configure the * element (like {@link us.abstracta.jmeter.javadsl.http.DslHttpSampler#post(String, * ContentType)}. *

* This method abstracts some common logic regarding chaining. For example: if chained method only * contains a parameter and its value is the default one, then method is not chained, since it is * not necessary. It also takes care of handling boolean parameters which chained method may or * may not include a boolean parameter. * * @param methodName is the name of the method contained in the returned instance of this method * call, which has to be chained to this method call. * @param params is the list of parameters used to find the method and associated to the * chained method call. Take into consideration that the exact same number and * type of parameters must be specified for the method to be found, otherwise an * exception will be generated. * @return this call instance for further chaining or addition of children elements. * @throws UnsupportedOperationException when no method with given names and/or parameters can be * found to be chained in current method call. */ public MethodCall chain(String methodName, MethodParam... params) { // this eases chaining don't having to check in client code for this condition if (params.length == 1 && params[0].isDefault()) { return this; } Method method = findMethodInClassHierarchyMatchingParams(methodName, returnType, params); /* when chaining methods with booleans in some cases the parameter is required, and in some others is not. */ if (method == null && params.length == 1 && params[0] instanceof MethodParam.BoolParam) { method = findMethodInClassHierarchyMatchingParams(methodName, returnType, new MethodParam[0]); if (method != null) { params = new MethodParam[0]; } } if (method == null) { throw buildNoMatchingMethodFoundException( "public '" + methodName + "' method in " + returnType.getName(), params); } chain.add(MethodCall.from(method, params)); return this; } private Method findMethodInClassHierarchyMatchingParams(String methodName, Class methodClass, MethodParam[] params) { Method ret = null; while (ret == null && methodClass != Object.class) { ret = findMethodInClassMatchingParams(methodName, methodClass, params); methodClass = methodClass.getSuperclass(); } return ret; } private Method findMethodInClassMatchingParams(String methodName, Class methodClass, MethodParam[] params) { Stream chainableMethods = Arrays.stream(methodClass.getDeclaredMethods()) .filter(m -> methodName.equals(m.getName()) && Modifier.isPublic(m.getModifiers()) && m.getReturnType() == methodClass); return findParamsMatchingMethod(chainableMethods, params); } protected static Method findParamsMatchingMethod(Stream methods, MethodParam[] params) { List> finalParams = Arrays.stream(params) .filter(p -> !p.isIgnored()) .collect(Collectors.toList()); return methods .filter(m -> methodMatchesParameters(m, finalParams)) .findAny() .orElse(null); } private static boolean methodMatchesParameters(Method m, List> params) { if (m.getParameterCount() != params.size()) { return false; } Class[] paramTypes = m.getParameterTypes(); for (int i = 0; i < params.size(); i++) { if (!params.get(i).getType().isAssignableFrom(paramTypes[i])) { return false; } } return true; } protected static UnsupportedOperationException buildNoMatchingMethodFoundException( String methodCondition, MethodParam[] params) { return new UnsupportedOperationException( "No " + methodCondition + " method was found for parameters " + Arrays.toString(params) + ". This is probably due to some change in DSL not reflected in associated code " + "builder."); } /** * Allows extracting from a given call the list of chained method calls and re assign them to this * call. *

* This is usually helpful when you provide in a DSL element alias methods for children elements. * Eg: {@link us.abstracta.jmeter.javadsl.http.DslHttpSampler#header(String, String)}. * * @param other is the call to extract the chained methods from. */ public void reChain(MethodCall other) { this.chain.addAll(other.chain); } /** * Generates the code for this method call and all associated parameters, children elements and * chained methods. * * @return the generated code. */ public String buildCode() { return buildCode(""); } protected String buildCode(String indent) { StringBuilder ret = new StringBuilder(); ret.append(methodName) .append("("); String childIndent = indent + INDENT; String paramsCode = buildParamsCode(childIndent); ret.append(paramsCode); boolean hasChildren = paramsCode.endsWith("\n"); if (hasChildren) { ret.append(indent); } ret.append(")"); String chainedCode = buildChainedCode(childIndent); if (!chainedCode.isEmpty()) { if (!hasChildren) { ret.append("\n") .append(childIndent); } ret.append("."); ret.append(chainedCode); } return ret.toString(); } protected String buildParamsCode(String indent) { String ret = params.stream() .filter(p -> !p.isIgnored()) .map(p -> p.buildCode(indent)) .filter(s -> !s.isEmpty()) .collect(Collectors.joining(", ")); return ret.replace(", \n", ",\n").replaceAll("\n\\s*\n", "\n"); } private String buildChainedCode(String indent) { return chain.stream() .map(c -> c.buildCode(indent)) .filter(s -> !s.isEmpty()) .collect(Collectors.joining("\n" + indent + ".")); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy