blob: f64ba7809928544fc584f4e7f60e2df25baecb9d [file] [log] [blame]
/*
* 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.jmeter.protocol.http.util;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.regex.Pattern;
import org.apache.commons.lang3.RegExUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.entity.ContentType;
import org.apache.jmeter.config.Argument;
import org.apache.jmeter.config.Arguments;
import org.apache.jmeter.protocol.http.config.GraphQLRequestParams;
import org.apache.jmeter.testelement.property.JMeterProperty;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.StreamReadFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* Utilities to (de)serialize GraphQL request parameters.
*/
public final class GraphQLRequestParamUtils {
public static final String VARIABLES_FIELD = "variables";
public static final String OPERATION_NAME_FIELD = "operationName";
public static final String QUERY_FIELD = "query";
private static final Pattern WHITESPACES_PATTERN = Pattern.compile("\\p{Space}+");
private static final JsonFactory jsonFactory = new JsonFactory();
private static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder()
// See https://github.com/FasterXML/jackson-core/issues/991
.enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION)
.build();
private GraphQLRequestParamUtils() {
}
/**
* Return true if the content type is GraphQL content type (i.e. 'application/json').
* @param contentType Content-Type value
* @return true if the content type is GraphQL content type
*/
public static boolean isGraphQLContentType(final String contentType) {
if (StringUtils.isEmpty(contentType)) {
return false;
}
final ContentType type = ContentType.parse(contentType);
return ContentType.APPLICATION_JSON.getMimeType().equals(type.getMimeType());
}
/**
* Convert the GraphQL request parameters input data to an HTTP POST body string.
* @param params GraphQL request parameter input data
* @return an HTTP POST body string converted from the GraphQL request parameters input data
* @throws RuntimeException if JSON serialization fails for some reason due to any runtime environment issues
*/
public static String toPostBodyString(final GraphQLRequestParams params) {
final StringWriter writer = new StringWriter();
try (JsonGenerator gen = jsonFactory.createGenerator(writer)) {
gen.writeStartObject();
gen.writeStringField(OPERATION_NAME_FIELD, StringUtils.trimToNull(params.getOperationName()));
if (StringUtils.isNotBlank(params.getVariables())) {
gen.writeFieldName(VARIABLES_FIELD);
gen.writeRawValue(StringUtils.trim(params.getVariables()));
}
gen.writeStringField(QUERY_FIELD, StringUtils.trim(params.getQuery()));
gen.writeEndObject();
} catch (IOException e) {
throw new IllegalStateException("Error while writing graphql post body " + params, e);
}
return writer.toString();
}
/**
* Convert the GraphQL Query input string into an HTTP GET request parameter value.
* @param query the GraphQL Query input string
* @return an HTTP GET request parameter value converted from the GraphQL Query input string
*/
public static String queryToGetParamValue(final String query) {
return RegExUtils.replaceAll(StringUtils.trim(query), WHITESPACES_PATTERN, " ");
}
/**
* Convert the GraphQL Variables JSON input string into an HTTP GET request parameter value.
* @param variables the GraphQL Variables JSON input string
* @return an HTTP GET request parameter value converted from the GraphQL Variables JSON input string
*/
public static String variablesToGetParamValue(final String variables) {
return StringUtils.trimToNull(variables);
}
/**
* Parse {@code postData} and convert it to a {@link GraphQLRequestParams} object if it is a valid GraphQL post data.
* @param postData post data
* @param contentEncoding content encoding
* @return a converted {@link GraphQLRequestParams} object form the {@code postData}
* @throws IllegalArgumentException if {@code postData} is not a GraphQL post JSON data or not a valid JSON
* @throws JsonProcessingException if it fails to serialize a parsed JSON object to string
* @throws UnsupportedEncodingException if it fails to decode parameter value
*/
public static GraphQLRequestParams toGraphQLRequestParams(byte[] postData, final String contentEncoding)
throws JsonProcessingException, UnsupportedEncodingException {
final String encoding = StringUtils.isNotEmpty(contentEncoding) ? contentEncoding
: EncoderCache.URL_ARGUMENT_ENCODING;
ObjectNode data;
try (InputStreamReader reader = new InputStreamReader(new ByteArrayInputStream(postData), encoding)) {
data = OBJECT_MAPPER.readValue(reader, ObjectNode.class);
} catch (IOException e) {
throw new IllegalArgumentException("Invalid json data: " + e.getLocalizedMessage(), e);
}
String operationName = null;
String query;
String variables = null;
final JsonNode operationNameNode = data.has(OPERATION_NAME_FIELD) ? data.get(OPERATION_NAME_FIELD) : null;
if (operationNameNode != null) {
operationName = getJsonNodeTextContent(operationNameNode, true);
}
if (!data.has(QUERY_FIELD)) {
throw new IllegalArgumentException("Not a valid GraphQL query.");
}
final JsonNode queryNode = data.get(QUERY_FIELD);
query = getJsonNodeTextContent(queryNode, false);
final String trimmedQuery = StringUtils.trim(query);
if (!StringUtils.startsWith(trimmedQuery, QUERY_FIELD) && !StringUtils.startsWith(trimmedQuery, "mutation")) {
throw new IllegalArgumentException("Not a valid GraphQL query.");
}
final JsonNode variablesNode = data.has(VARIABLES_FIELD) ? data.get(VARIABLES_FIELD) : null;
if (variablesNode != null) {
final JsonNodeType nodeType = variablesNode.getNodeType();
if (nodeType != JsonNodeType.NULL) {
if (nodeType == JsonNodeType.OBJECT) {
variables = OBJECT_MAPPER.writeValueAsString(variablesNode);
} else {
throw new IllegalArgumentException("Not a valid object node for GraphQL variables.");
}
}
}
return new GraphQLRequestParams(operationName, query, variables);
}
/**
* Parse {@code arguments} and convert it to a {@link GraphQLRequestParams} object if it has valid GraphQL HTTP arguments.
* @param arguments arguments
* @param contentEncoding content encoding
* @return a converted {@link GraphQLRequestParams} object form the {@code arguments}
* @throws IllegalArgumentException if {@code arguments} does not contain valid GraphQL request arguments
* @throws UnsupportedEncodingException if it fails to decode parameter value
*/
public static GraphQLRequestParams toGraphQLRequestParams(final Arguments arguments, final String contentEncoding)
throws UnsupportedEncodingException {
final String encoding = StringUtils.defaultIfEmpty(contentEncoding, EncoderCache.URL_ARGUMENT_ENCODING);
String operationName = null;
String query = null;
String variables = null;
for (JMeterProperty prop : arguments) {
final Argument arg = (Argument) prop.getObjectValue();
if (!(arg instanceof HTTPArgument)) {
continue;
}
final String name = arg.getName();
final String metadata = arg.getMetaData();
final String value = StringUtils.trimToNull(arg.getValue());
if ("=".equals(metadata) && value != null) {
final boolean alwaysEncoded = ((HTTPArgument) arg).isAlwaysEncoded();
if (OPERATION_NAME_FIELD.equals(name)) {
operationName = encodedField(value, encoding, alwaysEncoded);
} else if (QUERY_FIELD.equals(name)) {
query = encodedField(value, encoding, alwaysEncoded);
} else if (VARIABLES_FIELD.equals(name)) {
variables = encodedField(value, encoding, alwaysEncoded);
}
}
}
if (isNoQueryOrMutation(query)) {
throw new IllegalArgumentException("Not a valid GraphQL query.");
}
if (isNoJsonObject(variables)) {
throw new IllegalArgumentException("Not a valid object node for GraphQL variables.");
}
return new GraphQLRequestParams(operationName, query, variables);
}
private static String encodedField(final String value, final String encoding, final boolean isEncoded)
throws UnsupportedEncodingException {
if (isEncoded) {
return value;
}
return URLDecoder.decode(value, encoding);
}
private static boolean isNoJsonObject(String variables) {
return StringUtils.isNotEmpty(variables)
&& (!StringUtils.startsWith(variables, "{") || !StringUtils.endsWith(variables, "}"));
}
private static boolean isNoQueryOrMutation(String query) {
return StringUtils.isEmpty(query)
|| (!StringUtils.startsWith(query, QUERY_FIELD) && !StringUtils.startsWith(query, "mutation"));
}
private static String getJsonNodeTextContent(final JsonNode jsonNode, final boolean nullable) {
final JsonNodeType nodeType = jsonNode.getNodeType();
if (nodeType == JsonNodeType.NULL) {
if (nullable) {
return null;
}
throw new IllegalArgumentException("Not a non-null value node.");
}
if (nodeType == JsonNodeType.STRING) {
return jsonNode.asText();
}
throw new IllegalArgumentException("Not a string value node.");
}
}