| /* |
| * 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."); |
| } |
| } |