cj.restspecs.core.RestSpec Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of rest-specs-core Show documentation
Show all versions of rest-specs-core Show documentation
A test-friendly mechanism for expressing RESTful http contracts. Core software module.
/**
* Copyright (C) Commission Junction Inc.
*
* This file is part of rest-specs.
*
* rest-specs is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* rest-specs is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with rest-specs; see the file COPYING. If not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA.
*
* Linking this library statically or dynamically with other modules is
* making a combined work based on this library. Thus, the terms and
* conditions of the GNU General Public License cover the whole
* combination.
*
* As a special exception, the copyright holders of this library give you
* permission to link this library with independent modules to produce an
* executable, regardless of the license terms of these independent
* modules, and to copy and distribute the resulting executable under
* terms of your choice, provided that you also meet, for each linked
* independent module, the terms and conditions of the license of that
* module. An independent module is a module which is not derived from
* or based on this library. If you modify this library, you may extend
* this exception to your version of the library, but you are not
* obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
package cj.restspecs.core;
import cj.restspecs.core.io.ClasspathLoader;
import cj.restspecs.core.io.Loader;
import cj.restspecs.core.model.Header;
import cj.restspecs.core.model.Representation;
import cj.restspecs.core.model.Request;
import cj.restspecs.core.model.Response;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.*;
public class RestSpec {
private JsonNode root;
private final String name, url;
private final Loader loader;
private final Map replacements;
private final QueryParameters queryParameters;
private RestSpec(RestSpec originalSpec, Map replacements) {
this.root = originalSpec.root;
this.name = originalSpec.name;
this.url = originalSpec.url;
this.loader = originalSpec.loader;
this.replacements = new HashMap();
this.replacements.putAll(originalSpec.replacements);
this.replacements.putAll(replacements);
this.queryParameters = new QueryParameters(queryString());
}
public RestSpec(String specName) {
this(specName, new ClasspathLoader());
}
public RestSpec(String specName, Loader loader) {
this.loader = loader;
this.replacements = new HashMap();
InputStream is = loader.load(specName);
if (is == null) {
throw new RuntimeException("Could not find file named " + specName);
}
try {
ObjectMapper mapper = new ObjectMapper();
root = mapper.readValue(is, JsonNode.class);
} catch (Exception e) {
throw new RuntimeException(e);
}
name = root.path("name").asText();
url = root.path("url").asText();
if (url == null || url.isEmpty()) {
throw new RuntimeException("Spec is missing a 'url'");
}
blowUpIfThereAreFieldsBesidesThese(root, Arrays.asList(
"name",
"url",
"request",
"response"
));
blowUpIfThereAreFieldsBesidesThese(root.path("response"), Arrays.asList(
"statusCode",
"header",
"representation",
"representation-ref"
));
queryParameters = new QueryParameters(queryString());
}
private void blowUpIfThereAreFieldsBesidesThese(JsonNode root, List allowedNodes) {
Iterator fields = root.fieldNames();
while (fields.hasNext()) {
String field = fields.next();
if (!allowedNodes.contains(field)) {
throw new RuntimeException("Field '" + field + "' is not allowed");
}
}
}
public String name() {
return name;
}
public String pathMinusQueryStringAndFragment() {
return parseUrl()[0];
}
private String[] parseUrl() {
String replacedUrl = replacedPath();
int delimiterPos = replacedUrl.indexOf('?');
String path = replacedUrl;
String queryString = "";
if (delimiterPos > -1) {
path = replacedUrl.substring(0, delimiterPos);
queryString = replacedUrl.substring(delimiterPos);
}
return new String[] {
path,
queryString
};
}
public class QueryParameters {
private final List namesInOrder;
private final Map> memoizedQueryParameters;
private QueryParameters(String queryString) {
namesInOrder = new ArrayList();
memoizedQueryParameters = new HashMap>();
if (!"".equals(queryString)) {
String queryWithoutInitialDelimiter = queryString.substring(1);
String[] parameters = queryWithoutInitialDelimiter.split("&");
for (String parameter : parameters) {
String[] pair = parameter.split("=");
String key = decodeUrlString(pair[0]);
String value = "";
if (pair.length > 1) {
value = decodeUrlString(pair[1]);
}
if (!memoizedQueryParameters.containsKey(key)) {
memoizedQueryParameters.put(key, new ArrayList());
namesInOrder.add(key);
}
List values;
values = memoizedQueryParameters.get(key);
values.add(value);
}
}
}
public List names() {
return namesInOrder;
}
public String value(String name) {
return values(name).get(0);
}
public List values(String name) {
if (memoizedQueryParameters.containsKey(name)) {
return memoizedQueryParameters.get(name);
} else {
String parameterNotFoundMessage = String.format("Parameter name '%s' not found in specification.", name);
throw new RuntimeException(parameterNotFoundMessage);
}
}
}
private String decodeUrlString(String input) {
try {
String result;
result = URLDecoder.decode(input, "UTF-8");
return result;
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Could not decode: " + input, e);
}
}
public String queryParameterValue(String parameterName) {
return queryParameters.value(parameterName);
}
public QueryParameters queryParameters() {
return queryParameters;
}
/**
* @deprecated This design does not take into account multi-valued parameters.
* @see #queryParameters()
* @see QueryParameters#values
*/
@Deprecated
public Map queryParams() {
Map queryParams = new HashMap();
String query = queryString();
if (!"".equals(query)) {
String queryWithoutInitialDelimiter = query.substring(1);
String[] parameters = queryWithoutInitialDelimiter.split("&");
for (String parameter : parameters) {
String[] pair = parameter.split("=");
String key = decodeUrlString(pair[0]);
String value = null;
if (pair.length > 1) {
value = decodeUrlString(pair[1]);
}
queryParams.put(key, value);
}
}
return queryParams;
}
public String queryString() {
return parseUrl()[1];
}
public String path() {
return url;
}
public String getPathReplacedWith(Map replacements) {
String urlWithReplacedValues = url;
for (Map.Entry entry : replacements.entrySet()) {
urlWithReplacedValues = urlWithReplacedValues.replace(entry.getKey().toString(), entry.getValue().toString());
}
return urlWithReplacedValues;
}
public String getPathReplacedWith(String firstKey, Object firstValue, Object ... moreKeysAndValues) {
if (firstKey == null) {
throw new IllegalArgumentException("replacement keys cannot be null.");
}
Map map = new HashMap();
map.put(firstKey,firstValue);
if (moreKeysAndValues.length %2 != 0) {
throw new IllegalArgumentException("Must have an even number of arguments, you sent me " + (moreKeysAndValues.length + 2));
}
for (int i = 0; i< moreKeysAndValues.length; i += 2) {
Object key = moreKeysAndValues[i];
Object value = moreKeysAndValues[i+1];
if (!(key instanceof String)) {
String className = (key == null) ? "java.lang.Object" : key.getClass().getName();
String message = String.format("Key is not a String: %s (%s)", key, className);
throw new IllegalArgumentException(message);
}
map.put((String)key, value);
}
return getPathReplacedWith(map);
}
public Request request() {
return new RequestFromRestSpec(root, loader);
}
public Response response() {
final JsonNode responseNode = root.path("response");
if (responseNode.isMissingNode()) {
throw new RuntimeException("Spec is missing a 'response'");
}
return new ResponseFromRestSpec(responseNode, loader);
}
public RestSpec withParameter(String parameterName, Object parameterValue) {
if (url.contains(parameterName)) {
HashMap replacements;
replacements = new HashMap();
replacements.put(parameterName, parameterValue);
return new RestSpec(this, replacements);
} else {
String message = String.format("%s does not exist in url", parameterName);
throw new RuntimeException(message);
}
}
public String replacedPath() {
return getPathReplacedWith(replacements);
}
}
class HeaderImpl implements Header {
JsonNode headerNode;
HeaderImpl(JsonNode headerNode) {
this.headerNode = headerNode;
}
public List fieldNames() {
List results = new ArrayList();
Iterator names = headerNode.fieldNames();
while (names.hasNext()) {
results.add(names.next());
}
return results;
}
public List fieldsNamed(String name) {
List results = new ArrayList();
final Iterator> items = headerNode.fields();
while (items.hasNext()) {
Map.Entry field = items.next();
if (field.getKey().equals(name)) {
results.add(field.getValue().asText());
}
}
return results;
}
}
//helpers
class RepresentationFactory {
static Representation createRepresentation(final JsonNode node, final Loader loader, final String contentType) {
if (node.path("representation").isMissingNode() && node.path("representation-ref").isMissingNode()) {
return null;
}
return new Representation() {
public String contentType() {
return contentType;
}
public InputStream data() {
JsonNode representation = node.path("representation");
if (!representation.isMissingNode()) {
return IOUtils.toInputStream(representation.asText());
}
JsonNode repRef = node.path("representation-ref");
if (!repRef.isMissingNode()) {
String resourcePath = repRef.asText();
return loader.load(resourcePath);
}
throw new IllegalStateException("rest spec does not have representation or representation-ref response node");
}
public String asText() {
try {
String textRepresentation = IOUtils.toString(data());
textRepresentation = textRepresentation.replaceAll("\n\\$", ""); //remove newline from last line of response spec
return textRepresentation;
} catch (IOException ioe) { /*can't do that*/ }
return null;
}
};
}
}
class RequestFromRestSpec implements Request {
private final JsonNode requestNode;
private final Loader loader;
private final Header theHeader;
RequestFromRestSpec(JsonNode root, Loader loader) {
this.requestNode = root.path("request");
if (this.requestNode.isMissingNode()) {
throw new RuntimeException("Spec is missing a 'request'");
}
this.loader = loader;
this.theHeader = new HeaderImpl(requestNode.path("header"));
}
public String method() {
JsonNode method = requestNode.path("method");
if (method.isMissingNode()) {
throw new RuntimeException("Spec is missing a 'request.method'");
}
return method.asText();
}
public Representation representation() {
return RepresentationFactory.createRepresentation(requestNode, loader, "");
}
public Header header() {
return theHeader;
}
}
class ResponseFromRestSpec implements Response {
private final Loader loader;
private final JsonNode responseNode;
private final Header theHeader;
public ResponseFromRestSpec(JsonNode responseNode, Loader loader) {
this.loader = loader;
this.responseNode = responseNode;
this.theHeader = new HeaderImpl(responseNode.path("header"));
}
public int statusCode() {
return responseNode.path("statusCode").intValue();
}
private T nthOrElse(int n, T defaultValue, List list) {
if (list.size() > 0) {
return list.get(n);
} else {
return defaultValue;
}
}
public Representation representation() {
if (responseNode.path("representation").isMissingNode() && responseNode.path("representation-ref").isMissingNode()) {
return null;
}
String contentType = nthOrElse(0, "", theHeader.fieldsNamed("Content-Type"));
return RepresentationFactory.createRepresentation(responseNode, loader, contentType);
}
public Header header() {
return theHeader;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy