us.abstracta.jmeter.javadsl.jdbc.DslJdbcSampler Maven / Gradle / Ivy
Show all versions of jmeter-java-dsl-jdbc Show documentation
package us.abstracta.jmeter.javadsl.jdbc;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Types;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.jmeter.protocol.jdbc.sampler.JDBCSampler;
import org.apache.jmeter.testbeans.gui.TestBeanGUI;
import org.apache.jmeter.testelement.TestElement;
import us.abstracta.jmeter.javadsl.codegeneration.MethodCall;
import us.abstracta.jmeter.javadsl.codegeneration.MethodCallContext;
import us.abstracta.jmeter.javadsl.codegeneration.MethodParam;
import us.abstracta.jmeter.javadsl.codegeneration.SingleTestElementCallBuilder;
import us.abstracta.jmeter.javadsl.codegeneration.TestElementParamBuilder;
import us.abstracta.jmeter.javadsl.codegeneration.params.BoolParam;
import us.abstracta.jmeter.javadsl.codegeneration.params.EnumParam;
import us.abstracta.jmeter.javadsl.codegeneration.params.EnumParam.EnumPropertyValue;
import us.abstracta.jmeter.javadsl.codegeneration.params.FixedParam;
import us.abstracta.jmeter.javadsl.codegeneration.params.StringArrayParam;
import us.abstracta.jmeter.javadsl.core.samplers.BaseSampler;
/**
* Allows interacting with databases through configured JDBC connections.
*
* This sampler currently does not require to specify the type of query to send to the database (as
* JMeter element does), and calculates the proper type from query string and defined parameters.
*
* @see DslJdbcConnectionPool
* @since 0.38
*/
public class DslJdbcSampler extends BaseSampler {
private static final String DEFAULT_NAME = "JDBC Request";
private static final String NULL_ARGUMENT = "]NULL[";
protected String poolName;
protected String query;
protected final List params = new ArrayList<>();
protected final List vars = new ArrayList<>();
protected String resultsVar;
protected Duration timeout;
protected QueryType queryType;
public DslJdbcSampler(String name, String poolName, String query) {
super(name != null ? name : DEFAULT_NAME, TestBeanGUI.class);
this.poolName = poolName;
this.query = query;
}
protected static class QueryParameter {
private static final Map JDBC_TYPE_TO_PROPERTY_VALUE =
buildJdbcTypeToPropertyValueMapping();
public final Object value;
public final int jdbcType;
public final JdbcParamMode mode;
private QueryParameter(Object value, int jdbcType, JdbcParamMode mode) {
this.value = value;
this.jdbcType = jdbcType;
this.mode = mode;
}
private static Map buildJdbcTypeToPropertyValueMapping() {
HashMap ret = new HashMap<>();
Field[] fields = java.sql.Types.class.getFields();
try {
for (Field field : fields) {
String name = field.getName();
Integer value = (Integer) field.get(null);
ret.put(value, name.toLowerCase(java.util.Locale.ENGLISH));
}
return ret;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private String getTypePropertyValue() {
String propVal = JDBC_TYPE_TO_PROPERTY_VALUE.get(jdbcType);
if (propVal == null) {
// this is required because some databases define their own types
propVal = String.valueOf(jdbcType);
}
return mode == JdbcParamMode.IN ? propVal : mode.name() + " " + propVal;
}
}
/**
* Specifies the mode to apply to a given query parameter.
*/
public enum JdbcParamMode implements EnumPropertyValue {
/**
* The parameter is only used by database query to read a value.
*
* This is the default mode of parameters when none is specified.
*/
IN,
/**
* The parameter value is altered by the database query, and used as a resulting output of the
* query.
*/
OUT,
/**
* The parameter value is used to read a value by the query and is altered containing a
* resulting output.
*/
INOUT;
@Override
public String propertyValue() {
return name();
}
}
/**
* Specifies the type of query to use the JDBC driver with.
*
* In general this should be auto-detected, but, when auto-detection fails to properly identify
* the type, then you can manually specify the type through
* {@link DslJdbcSampler#queryType(QueryType)} method.
*/
public enum QueryType implements EnumPropertyValue {
/**
* Identifies simple select statement with no parameters as placeholders.
*
* This query type is usually used in combination with {@link DslJdbcSampler#vars(String...)}
* and/or {@link DslJdbcSampler#resultsVar}.
*/
SELECT("Select Statement"),
/**
* Identifies simple insert, update, delete or similar database altering query with no
* parameters as placeholders.
*/
UPDATE("Update Statement"),
/**
* Identifies callable statement with potential input, output or inout parameters. For example
* functions, stored procedures, etc.
*
* This type of query usually requires setting parameters through
* {@link DslJdbcSampler#param(Object, int)} or
* {@link DslJdbcSampler#param(Object, int, JdbcParamMode)} and potentially getting results
* through {@link DslJdbcSampler#vars(String...)} and/or {@link DslJdbcSampler#resultsVar}.
*/
CALLABLE("Callable Statement"),
/**
* Same as {@link QueryType#SELECT} but with parameters set through placeholders ("?" symbol).
*
* This type of query requires setting parameters through
* {@link DslJdbcSampler#param(Object, int)}.
*/
PREPARED_SELECT("Prepared Select Statement"),
/**
* Same as {@link QueryType#UPDATE} but with parameters set through placeholders ("?" symbol).
*
* This type of query requires setting parameters through
* {@link DslJdbcSampler#param(Object, int)}.
*/
PREPARED_UPDATE("Prepared Update Statement"),
/**
* Identifies a commit statement. This is the same as {@link DslJdbcSampler#commit()}
*/
COMMIT("Commit"),
/**
* Identifies a rollback statement. This is the same as {@link DslJdbcSampler#rollback()}
*/
ROLLBACK("Rollback"),
/**
* Disables auto-commits. This is the same as {@link DslJdbcSampler#autoCommit(boolean)} with
* false argument.
*/
AUTO_COMMIT_FALSE("AutoCommit(false)"),
/**
* Enables auto-commits. This is the same as {@link DslJdbcSampler#autoCommit(boolean)} with
* true argument.
*/
AUTO_COMMIT_TRUE("AutoCommit(true)");
private final String propertyValue;
QueryType(String propertyValue) {
this.propertyValue = propertyValue;
}
@Override
public String propertyValue() {
return propertyValue;
}
}
/**
* Allows specifying a parameter value to pass to the query and used in provided query
* placeholders ("?" symbol).
*
* In general, prefer using this instead of using JMeter variable references or expressions inside
* query, since is more performance and safer.
*
* In the end, this just specifies parameters to pass to a prepared statement generated from the
* given query string.
*
* There is no need to replace nulls with special values or have any special consideration for
* strings containing commas and quotes since the DSL abstracts such details.
*
* @param value specifies the actual value to set on the parameter.
* @param jdbcParamType sets the type of the parameter, being the value one of
* {@link java.sql.Types} defined ones, or a specific one for the database
* that is not included in Types class.
* @return the sampler for further configuration or usage.
* @see java.sql.Types
*/
public DslJdbcSampler param(Object value, int jdbcParamType) {
return param(value, jdbcParamType, JdbcParamMode.IN);
}
/**
* Same as {@link #param(Object, int)} but allowing also to specify parameter mode.
*
* When mode is not specified, then {@link JdbcParamMode#IN} is used.
*
* Remember specifying a parameter for each of query placeholders ('?'), even if is an OUT
* parameter.
*
* @param value specifies the actual value to set on the parameter.
* @param jdbcParamType sets the type of the parameter, being the value one of
* {@link java.sql.Types} defined ones, or a specific one for the database
* that is not included in Types class.
* @param mode specifies the {@link JdbcParamMode} for the parameter.
* @return the sampler for further configuration or usage.
* @see #param(Object, int)
* @see JdbcParamMode
*/
public DslJdbcSampler param(Object value, int jdbcParamType, JdbcParamMode mode) {
params.add(new QueryParameter(value, jdbcParamType, mode));
return this;
}
/**
* Allows specifying the name prefixes of variables where to store query retrieved row columns.
*
* JMeter adds a suffix to each variable with the row number, starting with 1 (eg: if the name of
* the variable is MY_FIELD, then the first row column value will be MY_FIELD_1). Additionally,
* JMeter creates a variable with "_#" suffix which includes the number of returned values/rows
* (eg: MY_FIELD_#).
*
* Alternatively you may use {@link #resultsVar} for getting a list of row maps.
*
* @param vars specifies variables names prefixes to use for each returned query column or
* function/procedure output parameter.
* @return the DslJdbcSampler for further configuration or usage.
*/
public DslJdbcSampler vars(String... vars) {
this.vars.addAll(Arrays.asList(vars));
return this;
}
/**
* Allows specifying the name of a variable where to store a list of query retrieved row maps.
*
* Each element in the list will contain a map where the key is the column name or alias, or
* parameter output name, and the value is the associated value.
*
* @param resultsVar specifies the name of the variable where to store the list of row maps.
* @return the sampler for further configuration or usage.
*/
public DslJdbcSampler resultsVar(String resultsVar) {
this.resultsVar = resultsVar;
return this;
}
/**
* Allows specifying a maximum amount of time a query can take before it fails.
*
* Warning: by default the timeout is not set, which differs from default JMeter component
* default behavior that sets it to 0 (infinity timeout). We decided to do this since we consider
* setting an infinity timeout to be a bad practices and additionally some database drivers might
* not support setting one.
*
* You should set the timeout with a big enough value that covers all expected scenarios, but also
* low enough to quickly detect any potential abnormal behavior.
*
* @param timeout specifies the timeout duration.
*
* By default, no timeout will be set, which might use the default timeout for the
* driver, connection, database. This also avoid issues with drivers that don't
* support a query timeout. When set to 0, then an infinity timeout will be
* considered (not recommended).
*
* Since JDBC only supports specifying times in seconds, if you specify a smaller
* granularity (like milliseconds) it will be rounded up to seconds.
* @return the sampler for further configuration or usage.
*/
public DslJdbcSampler timeout(Duration timeout) {
this.timeout = timeout;
return this;
}
/**
* Allows committing changes applied by previous queries when auto-commit is disabled on the
* connection pool.
*
* When this method is used, query string is ignored (so you can set it to null).
*
* @return the sampler for further configuration or usage.
*/
public DslJdbcSampler commit() {
this.queryType = QueryType.COMMIT;
return this;
}
/**
* Allows undoing changes done by previous queries when auto-commit is disabled on the connection
* pool.
*
* When this method is used, query string is ignored (so you can set it to null).
*
* @return the sampler for further configuration or usage.
*/
public DslJdbcSampler rollback() {
this.queryType = QueryType.ROLLBACK;
return this;
}
/**
* Allows enabling or disabling auto-commits on the connection pool.
*
* When this method is used, query string is ignored (so you can set it to null).
*
* @param enabled specifies to enable auto-commits when set to true, or disable them otherwise.
* @return the DslJdbcSampler for further configuration or usage.
*/
public DslJdbcSampler autoCommit(boolean enabled) {
this.queryType = enabled ? QueryType.AUTO_COMMIT_TRUE : QueryType.AUTO_COMMIT_FALSE;
return this;
}
/**
* Allows to explicitly specify the query type when auto-detection is not enough.
*
* In general this method should not be needed to invoke, but in some cases auto-detection may
* fail, and this method allows you to explicitly specify it.
*
* @param queryType the type of query to assign to use with the JDBC Driver.
* @return the sampler for further configuration or usage.
* @see QueryType
*/
public DslJdbcSampler queryType(QueryType queryType) {
this.queryType = queryType;
return this;
}
@Override
protected TestElement buildTestElement() {
JDBCSampler ret = new JDBCSampler();
ret.setQueryType(getQueryType().propertyValue);
ret.setDataSource(poolName);
ret.setQuery(query);
ret.setQueryArguments(params.stream()
.map(this::extractParamValue)
.collect(Collectors.joining(",")));
ret.setQueryArgumentsTypes(params.stream()
.map(QueryParameter::getTypePropertyValue)
.collect(Collectors.joining(",")));
ret.setVariableNames(String.join(",", vars));
ret.setResultVariable(resultsVar);
ret.setQueryTimeout(timeout != null ? String.valueOf(durationToSeconds(timeout)) : "-1");
return ret;
}
private QueryType getQueryType() {
if (queryType != null) {
return queryType;
}
if (query == null) {
throw new IllegalStateException(
"Query can only be null when using commit, rollback or autoCommit");
}
return solveQueryType(query, params.isEmpty());
}
private static QueryType solveQueryType(String query,
boolean hasParams) {
String queryType = query.trim();
if (query.isEmpty() || query.matches("\\$\\{.*}")) {
return null;
}
queryType = queryType.substring(0, queryType.indexOf(" ")).toLowerCase(Locale.US);
if ("select".equals(queryType)) {
return hasParams ? QueryType.SELECT : QueryType.PREPARED_SELECT;
} else if ("insert".equals(queryType) || "update".equals(queryType) || "delete".equals(
queryType)) {
return hasParams ? QueryType.UPDATE : QueryType.PREPARED_UPDATE;
} else {
return QueryType.CALLABLE;
}
}
private String extractParamValue(QueryParameter p) {
if (p.value == null) {
return NULL_ARGUMENT;
}
String strValue = p.value.toString();
if (strValue.contains(",") || strValue.contains("\"")) {
return "\"" + strValue.replace("\"", "\"\"") + "\"";
}
return strValue;
}
public static class CodeBuilder extends SingleTestElementCallBuilder {
public CodeBuilder(List builderMethods) {
super(JDBCSampler.class, builderMethods);
}
@Override
protected MethodCall buildMethodCall(JDBCSampler testElement, MethodCallContext context) {
TestElementParamBuilder paramBuilder = new TestElementParamBuilder(testElement);
FixedParam queryType = (FixedParam) paramBuilder.enumParam("queryType",
QueryType.COMMIT);
MethodParam query = paramBuilder.stringParam("query");
String queryArgumentsTypes = testElement.getPropertyAsString("queryArgumentsTypes");
MethodCall ret = buildMethodCall(paramBuilder.nameParam(DEFAULT_NAME),
paramBuilder.stringParam("dataSource"), query);
if (queryType.getValue() == QueryType.COMMIT) {
ret.chain("commit");
} else if (queryType.getValue() == QueryType.ROLLBACK) {
ret.chain("rollback");
} else if (queryType.getValue() == QueryType.AUTO_COMMIT_TRUE
|| queryType.getValue() == QueryType.AUTO_COMMIT_FALSE) {
ret.chain("autoCommit",
new BoolParam(queryType.getValue() == QueryType.AUTO_COMMIT_TRUE, null));
} else if (queryType.getValue() != solveQueryType(query.getExpression(),
queryArgumentsTypes == null || queryArgumentsTypes.isEmpty())) {
ret.chain("queryType", queryType);
} else {
chainParams(ret, queryArgumentsTypes, testElement.getPropertyAsString("queryArguments"));
ret.chain("vars", new StringArrayParam(testElement.getPropertyAsString("variableNames")))
.chain("resultVar", paramBuilder.stringParam("resultVariable"));
}
return ret.chain("timeout",
paramBuilder.durationParam("queryTimeout", Duration.ofSeconds(-1)));
}
private void chainParams(MethodCall ret, String queryArgumentsTypes, String queryArguments) {
if (queryArgumentsTypes == null || queryArgumentsTypes.isEmpty()) {
return;
}
QueryArgumentsParser argumentParser = new QueryArgumentsParser(queryArguments);
for (ParsedArgumentType argumentType : ParsedArgumentType.parseAll(queryArgumentsTypes)) {
MethodParam value = argumentParser.parseNext(argumentType);
if (argumentType.mode.isDefault()) {
ret.chain("param", value, argumentType.paramType);
} else {
ret.chain("param", value, argumentType.paramType, argumentType.mode);
}
}
}
}
private static class QueryArgumentsParser {
private final String queryArguments;
private int pos = 0;
private QueryArgumentsParser(String queryArguments) {
this.queryArguments = queryArguments;
}
private ValueParam parseNext(ParsedArgumentType argumentType) {
String ret;
int finalPos;
if (queryArguments.startsWith(NULL_ARGUMENT, pos)) {
ret = null;
finalPos = queryArguments.indexOf(pos, ',');
} else if (queryArguments.charAt(pos) == '"') {
finalPos = findQuotedParamEnd();
ret = queryArguments.substring(pos + 1, finalPos).replace("\"\"", "\"");
finalPos = queryArguments.indexOf(',', finalPos);
} else {
finalPos = queryArguments.indexOf(',', pos);
ret = queryArguments.substring(pos, finalPos == -1 ? queryArguments.length() : finalPos);
}
pos = finalPos + 1;
return new ValueParam(ret, argumentType);
}
private int findQuotedParamEnd() {
int finalPos = pos + 1;
while (!(queryArguments.charAt(finalPos) == '"' && (finalPos + 1 >= queryArguments.length()
|| queryArguments.charAt(finalPos + 1) != '"'))) {
if (queryArguments.charAt(finalPos) == '"'
&& queryArguments.charAt(finalPos + 1) == '"') {
finalPos++;
}
finalPos++;
}
return finalPos;
}
}
private static class ParsedArgumentType {
private final EnumParam mode;
private final ArgumentTypeParam paramType;
private ParsedArgumentType(EnumParam mode, ArgumentTypeParam paramType) {
this.mode = mode;
this.paramType = paramType;
}
private static List parseAll(String str) {
if (str == null || str.isEmpty()) {
return Collections.emptyList();
}
Pattern paramTypePattern = Pattern.compile("(?:(IN|OUT|INOUT) )?(-?\\w+)");
return Arrays.stream(str.split(","))
.map(s -> {
Matcher paramTypeMatcher = paramTypePattern.matcher(s);
paramTypeMatcher.find();
return new ParsedArgumentType(
new EnumParam<>(JdbcParamMode.class, paramTypeMatcher.group(1), JdbcParamMode.IN),
new ArgumentTypeParam(paramTypeMatcher.group(2)));
})
.collect(Collectors.toList());
}
}
private static class ArgumentTypeParam extends MethodParam {
private static final Set PREDEFINED_TYPES = findConstantNames(Types.class, int.class,
f -> true);
private final String type;
protected ArgumentTypeParam(String expression) {
super(int.class, expression);
type = expression.toUpperCase();
}
@Override
public boolean isDefault() {
return expression == null;
}
@Override
public Set getImports() {
return PREDEFINED_TYPES.contains(type) ? Collections.singleton(Types.class.getName())
: Collections.emptySet();
}
@Override
protected String buildCode(String indent) {
return PREDEFINED_TYPES.contains(type) ? "Types." + type : type;
}
}
private static class ValueParam extends MethodParam {
private static final Set STRING_TYPES = new HashSet<>(
Arrays.asList("CHAR", "VARCHAR", "LONGVARCHAR", "NCHAR", "NVARCHAR", "LONGNVARCHAR"));
private final ParsedArgumentType argumentType;
private ValueParam(String expression, ParsedArgumentType argumentType) {
super(Object.class, expression);
this.argumentType = argumentType;
}
@Override
protected String buildCode(String indent) {
return (STRING_TYPES.contains(argumentType.paramType.type)) ? buildStringLiteral(expression,
indent) : expression;
}
}
}