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

us.abstracta.jmeter.javadsl.http.DslHttpDefaults 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.http;

import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import org.apache.jmeter.config.Arguments;
import org.apache.jmeter.config.ConfigTestElement;
import org.apache.jmeter.protocol.http.config.gui.HttpDefaultsGui;
import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase;
import org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.testelement.property.TestElementProperty;
import org.apache.jorphan.collections.HashTree;
import us.abstracta.jmeter.javadsl.codegeneration.MethodCall;
import us.abstracta.jmeter.javadsl.codegeneration.MethodCallContext;
import us.abstracta.jmeter.javadsl.codegeneration.MethodCallContext.MethodCallContextEndListener;
import us.abstracta.jmeter.javadsl.codegeneration.MethodParam;
import us.abstracta.jmeter.javadsl.codegeneration.SingleGuiClassCallBuilder;
import us.abstracta.jmeter.javadsl.codegeneration.TestElementParamBuilder;
import us.abstracta.jmeter.javadsl.codegeneration.params.BoolParam;
import us.abstracta.jmeter.javadsl.codegeneration.params.EncodingParam;
import us.abstracta.jmeter.javadsl.core.BuildTreeContext;
import us.abstracta.jmeter.javadsl.core.BuildTreeContext.TreeContextEndListener;
import us.abstracta.jmeter.javadsl.core.configs.BaseConfigElement;
import us.abstracta.jmeter.javadsl.http.DslBaseHttpSampler.BaseHttpSamplerCodeBuilder;
import us.abstracta.jmeter.javadsl.http.DslHttpSampler.HttpClientImpl;

/**
 * Allows configuring default values for common properties of HTTP samplers.
 * 

* This is mainly a way to avoid duplication and an alternative to using java variables or builder * method. When in doubt, prefer using java variables or custom defined builder methods since they * are easier to write (in some cases), read and identify their scope. * * @see DslHttpSampler * @since 0.39 */ public class DslHttpDefaults extends BaseConfigElement { protected String protocol; protected String host; protected String port; protected String path; protected Charset encoding; protected boolean downloadEmbeddedResources; protected String embeddedResourcesMatchRegex; protected String embeddedResourcesNotMatchRegex; protected Duration connectionTimeout; protected Duration responseTimeout; protected String proxyUrl; protected String proxyUser; protected String proxyPassword; protected HttpClientImpl clientImpl; protected Boolean followRedirects; public DslHttpDefaults() { super("HTTP Request Defaults", HttpDefaultsGui.class); } /** * Specifies the default URL for HTTP samplers. *

* The DSL will parse the URL and properly set each of HTTP Request Defaults properties (protocol, * host, port and path). *

* You can later on overwrite in a sampler the path (by specifying only the path as url), or the * entire url (by specifying the full url as url). * * @param url specifies the default URL to be used by HTTP samplers. It might contain the path or * not. * @return the config element for further configuration or usage. */ public DslHttpDefaults url(String url) { /* url is decomposed and not just set on path, to allow in samplers to override just path and reuse other default settings */ JmeterUrl parsedUrl = JmeterUrl.valueOf(url); protocol = parsedUrl.protocol(); host = parsedUrl.host(); port = parsedUrl.port(); path = parsedUrl.path(); return this; } /** * Specifies the default protocol (eg: HTTP, HTTPS) to be used in the HTTP samplers. *

* You can specify entire url through {@link #url(String)}, but this method allows you to only * specify protocol when you need to override some other default, or just want that all samplers * use same default protocol. *

* In general prefer using java variables and methods, to get shorter and more maintainable code, * and use this method sparingly. * * @param protocol contains protocol value to be used (e.g.: http, https, etc). * @return the config element for further configuration or usage. * @since 0.49 */ public DslHttpDefaults protocol(String protocol) { this.protocol = protocol; return this; } /** * Specifies the default server host (domain) to be used in the HTTP samplers. *

* You can specify entire url through {@link #url(String)}, but this method allows you to only * specify host (and not protocol) when you need to override some other default, or just want that * all samplers use same default host. *

* In general prefer using java variables and methods, to get shorter and more maintainable code, * and use this method sparingly. * * @param host contains server name without protocol (no http/https) and path. * @return the config element for further configuration or usage. * @since 0.49 */ public DslHttpDefaults host(String host) { this.host = host; return this; } /** * Specifies the default port to be used in the HTTP samplers. *

* You can specify entire url through {@link #url(String)}, but this method allows you to only * specify port (and not protocol or host) when you need to override some other default, or just * want that all samplers use same default port. *

* In general prefer using java variables and methods, to get shorter and more maintainable code, * and use this method sparingly. * * @param port contains port value to be used. * @return the config element for further configuration or usage. * @since 0.49 */ public DslHttpDefaults port(int port) { this.port = String.valueOf(port); return this; } /** * Specifies the default URL path to be used in the HTTP samplers. *

* You can specify entire url through {@link #url(String)}, but this method allows you to only * specify path (and not protocol, host or port) when you need to override some other default, or * just want that all samplers use same path. *

* In general prefer using java variables and methods, to get shorter and more maintainable code, * and use this method sparingly. * * @param path contains URL path to be used by samplers. * @return the config element for further configuration or usage. * @since 0.49 */ public DslHttpDefaults path(String path) { this.path = path; return this; } /** * Specifies the default charset to be used for encoding URLs and requests contents. *

* This can be overwritten by {@link DslHttpSampler#encoding(Charset)}. * * @param encoding specifies the charset to be used by default. * @return the config element for further configuration or usage. * @see DslHttpSampler#encoding(Charset) */ public DslHttpDefaults encoding(Charset encoding) { this.encoding = encoding; return this; } /** * Specifies if by default HTTP redirects should be automatically followed (a new request * automatically created) when detected, or not. * * @param enable specifies whether to enable or disable automatic redirections by defaults. When * not set then the default is true. * @return the config element for further configuration or usage. * @since 1.9 */ public DslHttpDefaults followRedirects(boolean enable) { this.followRedirects = enable; return this; } /** * Allows enabling automatic download of HTML embedded resources (images, iframes, etc) by * default. * * @return the config element for further configuration or usage. * @see DslHttpSampler#downloadEmbeddedResources() */ public DslHttpDefaults downloadEmbeddedResources() { return downloadEmbeddedResources(true); } /** * Same as {@link #downloadEmbeddedResources()} but allowing to enable and disable the setting. *

* This is helpful when the resolution is taken at runtime. * * @param enable specifies to enable or disable the setting. By default, it is set to false. * @return the config element for further configuration or usage. * @see #downloadEmbeddedResources() * @since 1.0 */ public DslHttpDefaults downloadEmbeddedResources(boolean enable) { this.downloadEmbeddedResources = enable; return this; } /** * Same as {@link #downloadEmbeddedResources()} but allowing to specify which embedded resources * to actually download. *

* This is helpful when only some particular requests need to be downloaded and the rest should be * ignored. Eg: to only make requests to the site under test, and no other external services. *

* An alternative is using {@link #downloadEmbeddedResourcesNotMatching(String)}. If a resources * matches this regex and also one specified in * {@link #downloadEmbeddedResourcesNotMatching(String)}, then it will be ignored. * * @param urlRegex specifies the regular expression which will be used to ignore embedded * resources that have a URL matching with it. * @return the sampler for further configuration or usage. * @see #downloadEmbeddedResources() * @see #downloadEmbeddedResourcesNotMatching(String) * @since 1.3 */ public DslHttpDefaults downloadEmbeddedResourcesMatching(String urlRegex) { this.downloadEmbeddedResources = true; this.embeddedResourcesMatchRegex = urlRegex; return this; } /** * Same as {@link #downloadEmbeddedResources()} but allowing to ignore embedded resources with URL * matching a given regular expression. *

* This is helpful when some particular requests (for example to other external services) don't * want to be included in the test execution. *

* An alternative is using {@link #downloadEmbeddedResourcesMatching(String)}. If a resources * matches this regex and also one specified in * {@link #downloadEmbeddedResourcesMatching(String)}, then it will be ignored. * * @param urlRegex specifies the regular expression which will be used to ignore embedded * resources that have a URL matching with it. * @return the sampler for further configuration or usage. * @see #downloadEmbeddedResources() * @see #downloadEmbeddedResourcesMatching(String) * @since 1.3 */ public DslHttpDefaults downloadEmbeddedResourcesNotMatching(String urlRegex) { this.downloadEmbeddedResources = true; this.embeddedResourcesNotMatchRegex = urlRegex; return this; } /** * Allows to set the default maximum amount of time to wait for an HTTP connection to be * established. *

* This can be overwritten by {@link DslHttpSampler#connectionTimeout(Duration)}. * * @param timeout specifies the duration to be used as connection timeout. When set to 0 it * specifies to not timeout (wait indefinitely), which is not recommended. When set * to a negative number the operating system default is used. * @return the sampler for further configuration or usage. * @see DslHttpSampler#connectionTimeout(Duration) * @since 1.4 */ public DslHttpDefaults connectionTimeout(Duration timeout) { connectionTimeout = timeout; return this; } /** * Allows to set the maximum amount of time to wait for a response to an HTTP request. *

* This can be overwritten by {@link DslHttpSampler#responseTimeout(Duration)}. * * @param timeout specifies the duration to be used as response timeout. When set to 0 it * specifies to not timeout (wait indefinitely), which is not recommended. When set * to a negative number the operating system default is used. * @return the sampler for further configuration or usage. * @see DslHttpSampler#responseTimeout(Duration) * @since 1.4 */ public DslHttpDefaults responseTimeout(Duration timeout) { responseTimeout = timeout; return this; } /** * Allows specifying a proxy through which all http requests will be sent to their final * destination. *

* This is usually helpful when you need to use a proxy to access the internet when all access is * behind and enterprise proxy (due to security measures) or when you want to intercept requests * for further analysis or modification by other tools like fiddler or mitmproxy. *

* If your proxy requires authentication check {@link #proxy(String, String, String)}. * * @param url specifies the proxy url. For example http://myproxy:8181. * @return the config element for further configuration or usage. */ public DslHttpDefaults proxy(String url) { this.proxyUrl = url; return this; } /** * Same as {@link #proxy(String)} but allowing also to specify proxy credentials. * * @param url specifies the proxy url. For example http://myproxy:8181. * @param username specifies the username used to authenticate with the proxy. * @param password specifies the password used to authenticate with the proxy. * @return the config element for further configuration or usage. * @see #proxy(String) */ public DslHttpDefaults proxy(String url, String username, String password) { this.proxyUrl = url; this.proxyUser = username; this.proxyPassword = password; return this; } /** * Allows specifying which http client implementation to use by default for HTTP samplers. *

* This can be overwritten by {@link DslHttpSampler#clientImpl(HttpClientImpl)}. * * @param clientImpl the HTTP client implementation to use. If none is specified, then * {@link DslHttpSampler.HttpClientImpl#HTTP_CLIENT} is used. * @return the config element for further configuration or usage. * @see DslHttpSampler.HttpClientImpl * @see DslHttpSampler#clientImpl(HttpClientImpl) */ public DslHttpDefaults clientImpl(HttpClientImpl clientImpl) { this.clientImpl = clientImpl; return this; } /** * Specifies to reset (drop and recreate) connections on each thread group iteration. *

* By default, connections will be reused to avoid common issues of port and file descriptors * exhaustion requiring OS tuning, even though this means that generated load is not realistic * enough for emulating as if each iteration were a different user. If you need to proper * generation of connections and disconnections between iterations, then consider using this * method. *

* When using reset connection for each thread consider tuning OS like explained in "Configure * your environment" section of * this * article. *

* Warning: This setting is applied at JVM level, which means that it will affect the * entire test plan and potentially other test plans running in the same JVM instance. * * @return the config element for further configuration or usage. * @since 1.0 */ public DslHttpDefaults resetConnectionsBetweenIterations() { return resetConnectionsBetweenIterations(true); } /** * Same as {@link #resetConnectionsBetweenIterations()} but allowing to enable or disable * setting. *

* This is helpful when the resolution is taken at runtime. * * @param enable specifies to reset connections on each thread group iteration when true, * otherwise reuse connections. By default, connections are reused. * @return the config element for further configuration or usage. * @since 0.65 */ public DslHttpDefaults resetConnectionsBetweenIterations(boolean enable) { System.setProperty(DslBaseHttpSampler.RESET_CONNECTIONS_BETWEEN_ITERATIONS_PROP, String.valueOf(enable)); return this; } /** * Allows specifying the connections ttl (time-to-live) used to determine how much time a * connection can be kept open. *

* This setting allows tuning connections handling avoiding unnecessary resources usage depending * on the use case and server under test settings. * * @param ttl specifies the duration for connections to keep open before they are closed. By * default, this is set to 1 minute. * @return the config element for further configuration or usage. * @since 0.65 */ public DslHttpDefaults connectionsTtl(Duration ttl) { System.setProperty("httpclient4.time_to_live", String.valueOf(ttl.toMillis())); return this; } @Override protected TestElement buildTestElement() { ConfigTestElement ret = new ConfigTestElement(); HttpElementHelper.modifyTestElementUrl(ret, protocol, host, port, path); if (encoding != null) { ret.setProperty(HTTPSamplerBase.CONTENT_ENCODING, encoding.toString()); } ret.setProperty(new TestElementProperty(HTTPSamplerBase.ARGUMENTS, new Arguments())); HttpElementHelper.modifyTestElementEmbeddedResources(ret, downloadEmbeddedResources, embeddedResourcesMatchRegex, embeddedResourcesNotMatchRegex); HttpElementHelper.modifyTestElementTimeouts(ret, connectionTimeout, responseTimeout); HttpElementHelper.modifyTestElementProxy(ret, proxyUrl, proxyUser, proxyPassword); if (clientImpl != null) { ret.setProperty(HTTPSamplerBase.IMPLEMENTATION, clientImpl.propertyValue); } return ret; } @Override public HashTree buildTreeUnder(HashTree parent, BuildTreeContext context) { HashTree ret = super.buildTreeUnder(parent, context); if (followRedirects != null) { buildEndListener(context.getParent()).followRedirects = followRedirects; } return ret; } private static DefaultsTreeContextEndListener buildEndListener(BuildTreeContext parentCtx) { return parentCtx.getOrCreateEntry(DslHttpDefaults.class.getName(), () -> new DefaultsTreeContextEndListener(parentCtx)); } /* Set as protected to avoid this method to appear to users while creating a test plan, but still be visible for DslHttpSampler */ protected static void addPendingFollowRedirectsElement(HTTPSamplerProxy element, BuildTreeContext context) { buildEndListener(context.getRoot()).pendingFollowRedirectsElements.add(element); } private static class DefaultsTreeContextEndListener implements TreeContextEndListener { private Boolean followRedirects; private final List pendingFollowRedirectsElements = new ArrayList<>(); private DefaultsTreeContextEndListener(BuildTreeContext context) { context.addEndListener(this); } @Override public void execute(BuildTreeContext context, HashTree tree) { if (followRedirects != null) { setChildrenToFollowRedirects(tree); } else { pendingFollowRedirectsElements.stream() .filter(e -> e.getPropertyAsString(HTTPSamplerBase.FOLLOW_REDIRECTS).isEmpty()) .forEach(e -> e.setFollowRedirects(true)); } } private void setChildrenToFollowRedirects(HashTree tree) { tree.forEach((key, value) -> { if (key instanceof HTTPSamplerProxy) { HTTPSamplerProxy sampler = (HTTPSamplerProxy) key; if (sampler.getPropertyAsString(HTTPSamplerBase.FOLLOW_REDIRECTS).isEmpty()) { sampler.setFollowRedirects(followRedirects); } } else { setChildrenToFollowRedirects(value); } }); } } public static class CodeBuilder extends SingleGuiClassCallBuilder { public CodeBuilder(List builderMethods) { super(HttpDefaultsGui.class, builderMethods); } @Override protected MethodCall buildMethodCall(MethodCallContext context) { MethodCall ret = buildMethodCall(); TestElementParamBuilder paramBuilder = new TestElementParamBuilder(context.getTestElement()); MethodParam protocol = paramBuilder.stringParam(HTTPSamplerBase.PROTOCOL); MethodParam host = paramBuilder.stringParam(HTTPSamplerBase.DOMAIN); MethodParam port = paramBuilder.intParam(HTTPSamplerBase.PORT); MethodParam path = paramBuilder.stringParam(HTTPSamplerBase.PATH, "/"); if (!protocol.isDefault() && !host.isDefault()) { ret.chain("url", BaseHttpSamplerCodeBuilder.buildUrlParam(protocol, host, port, path)); } else { ret.chain("protocol", protocol) .chain("host", host) .chain("port", port) .chain("path", path); } HttpElementHelper.chainEncodingToMethodCall(ret, paramBuilder); HttpElementHelper.chainEmbeddedResourcesOptionsToMethodCall(ret, paramBuilder); HttpElementHelper.chainConnectionOptionsToMethodCall(ret, paramBuilder); HttpElementHelper.chainClientImplToMethodCall(ret, paramBuilder); registerDependency(context); return ret; } public void registerDependency(MethodCallContext context) { MethodCallContext parentCtx = context.getParent(); DefaultsMethodContextEndListener endListener = parentCtx.computeEntryIfAbsent( DslHttpDefaults.class, () -> new DefaultsMethodContextEndListener(parentCtx)); TestElement testElement = context.getTestElement(); String encoding = testElement.getPropertyAsString(HTTPSamplerBase.CONTENT_ENCODING); boolean isDefaultCandidate = testElement.getPropertyAsString(TestElement.GUI_CLASS) .equals(HttpDefaultsGui.class.getName()); endListener.registerEncoding(encoding, context, isDefaultCandidate); boolean followRedirects = isDefaultCandidate || testElement.getPropertyAsBoolean(HTTPSamplerBase.FOLLOW_REDIRECTS); endListener.registerFollowRedirect(followRedirects, context, isDefaultCandidate); } private static class DefaultsMethodContextEndListener implements MethodCallContextEndListener { private final List encodedCalls = new ArrayList<>(); private final List redirectableCalls = new ArrayList<>(); private DefaultsMethodContextEndListener(MethodCallContext parentCtx) { parentCtx.addEndListener(this); } public void registerEncoding(String encoding, MethodCallContext ret, boolean isDefaultCandidate) { encodedCalls.add(new EncodedCall(encoding, ret, isDefaultCandidate)); } public void registerFollowRedirect(boolean followRedirects, MethodCallContext ret, boolean isDefaultCandidate) { redirectableCalls.add(new RedirectableCall(followRedirects, ret, isDefaultCandidate)); } @Override public void execute(MethodCallContext ctx, MethodCall ret) { solveDefaultEncoding(ctx); solveDefaultFollowRedirects(ctx); } private void solveDefaultEncoding(MethodCallContext ctx) { EncodedCall defaultsCall = findDefaultEncodingCall(ctx); if (defaultsCall != null) { encodedCalls.stream() .filter(c -> c != defaultsCall && !c.isDefaultCandidate && c.encoding.equals(defaultsCall.encoding)) .forEach(EncodedCall::removeEncoding); } DefaultsMethodContextEndListener parentListener = buildParentContextEndListener(ctx); if (parentListener != null) { parentListener.registerEncoding(defaultsCall != null ? defaultsCall.encoding : "", defaultsCall != null ? defaultsCall.ctx : null, false); } } private static DefaultsMethodContextEndListener buildParentContextEndListener( MethodCallContext ctx) { MethodCallContext parentCtx = ctx.getParent(); return (parentCtx != null) ? parentCtx.computeEntryIfAbsent(DslHttpDefaults.class, () -> new DefaultsMethodContextEndListener(parentCtx)) : null; } private EncodedCall findDefaultEncodingCall(MethodCallContext ctx) { if (encodedCalls.size() == 1) { return encodedCalls.get(0); } EncodedCall ret = encodedCalls.stream() .filter(c -> c.isDefaultCandidate && !c.encoding.isEmpty()) .findAny() .orElse(null); if (ret != null) { return ret; } String someEncoding = encodedCalls.stream() .filter(c -> !c.encoding.isEmpty()) .map(c -> c.encoding) .findAny() .orElse(null); if (someEncoding != null && encodedCalls.stream() .allMatch(c -> c.isDefaultCandidate || c.encoding.equals(someEncoding))) { ret = encodedCalls.stream() .filter(c -> c.isDefaultCandidate) .findAny() .orElse(null); if (ret == null) { ret = new EncodedCall("", buildDefaultsCall(ctx), true); } ret.setEncoding(someEncoding); return ret; } return null; } private MethodCallContext buildDefaultsCall(MethodCallContext ctx) { return ctx.prependChild(new DslHttpDefaults().buildConfiguredTestElement(), null); } private void solveDefaultFollowRedirects(MethodCallContext ctx) { RedirectableCall defaultsCall = findDefaultRedirectableCall(ctx); if (defaultsCall != null) { redirectableCalls.stream() .filter(c -> c != defaultsCall) .forEach(RedirectableCall::removeFollowRedirects); } DefaultsMethodContextEndListener parentListener = buildParentContextEndListener(ctx); if (parentListener != null) { parentListener.registerFollowRedirect( defaultsCall == null || defaultsCall.followRedirects, defaultsCall != null ? defaultsCall.ctx : null, false); } } private RedirectableCall findDefaultRedirectableCall(MethodCallContext ctx) { if (redirectableCalls.size() == 1) { return redirectableCalls.get(0); } if (redirectableCalls.stream().allMatch(c -> c.isDefaultCandidate || !c.followRedirects)) { RedirectableCall ret = redirectableCalls.stream() .filter(c -> c.isDefaultCandidate) .findAny() .orElse(null); if (ret == null) { ret = new RedirectableCall(false, buildDefaultsCall(ctx), true); } ret.setFollowRedirects(false); return ret; } return null; } } } private static class EncodedCall { private String encoding; private final MethodCallContext ctx; private final boolean isDefaultCandidate; private EncodedCall(String encoding, MethodCallContext ctx, boolean isDefaultCandidate) { this.encoding = encoding; this.ctx = ctx; this.isDefaultCandidate = isDefaultCandidate; } public void setEncoding(String encoding) { this.encoding = encoding; ctx.getMethodCall().chain("encoding", new EncodingParam(encoding, null)); } public void removeEncoding() { ctx.getMethodCall().unchain("encoding"); removeEmptyDefaultsCall(ctx); } } private static void removeEmptyDefaultsCall(MethodCallContext ctx) { MethodCall methodCall = ctx.getMethodCall(); if (methodCall.getReturnType() == DslHttpDefaults.class && methodCall.chainSize() == 0) { ctx.getParent().getMethodCall() .replaceChild(methodCall, MethodCall.emptyCall()); } } private static class RedirectableCall { private boolean followRedirects; private final MethodCallContext ctx; private final boolean isDefaultCandidate; private RedirectableCall(boolean followRedirects, MethodCallContext ctx, boolean isDefaultCandidate) { this.followRedirects = followRedirects; this.ctx = ctx; this.isDefaultCandidate = isDefaultCandidate; } public void setFollowRedirects(boolean followRedirects) { this.followRedirects = followRedirects; ctx.getMethodCall().chain("followRedirects", new BoolParam(followRedirects, true)); } public void removeFollowRedirects() { ctx.getMethodCall().unchain("followRedirects"); removeEmptyDefaultsCall(ctx); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy