io.inversion.Response Maven / Gradle / Ivy
The newest version!
/*
* Copyright (c) 2015-2018 Rocket Partners, LLC
* https://github.com/inversion-api
*
* Licensed 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 io.inversion;
import io.inversion.json.*;
import io.inversion.utils.MimeTypes;
import io.inversion.utils.StreamBuffer;
import io.inversion.utils.Utils;
import io.inversion.utils.ListMap;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* This class serves as holder for the Response returned from a RestClient call AND as the object
* used to construct your own response to an Engine request.
*/
public class Response implements JSFind {
protected Chain chain = null;
protected Request request = null;
protected int statusCode = 200;
protected String statusMesg = "OK";
protected String url = null;
protected String fileName = null;
protected final ListMap headers = new ListMap<>();
protected JSNode json = new JSMap("meta", new JSMap(), "data", new JSList());
protected String text = null;
protected StreamBuffer stream = null;
protected Throwable error = null;
protected final StringBuilder debug = new StringBuilder();
protected final List changes = new ArrayList<>();
protected long startAt = System.currentTimeMillis();
protected long endAt = -1;
public Response() {
}
public Response(String url) {
withUrl(url);
}
/**
* Sets the root output json document...you should use withData and withMeta
* instead unless you REALLY want to change to wrapper document structure.
*
* @param json the json to set
* @return this
*/
public Response withJson(JSNode json) {
this.json = json;
this.text = null;
this.stream = null;
return this;
}
public Response withJson(String json) {
if (json == null)
withJson((JSNode) null);
else
withJson((JSNode) JSParser.parseJson(json));
return this;
}
public Response withText(String text) {
this.text = text;
this.json = null;
this.stream = null;
return this;
}
public Response withBody(StreamBuffer stream, String fileName) {
withBody(stream);
withFileName(fileName);
return this;
}
public Response withBody(StreamBuffer stream) {
this.text = null;
this.json = null;
this.stream = stream;
if (getContentType() == null && stream.getContentType() != null)
withContentType(stream.getContentType());
return this;
}
public JSNode getJson() {
if (stream != null) {
if (!MimeTypes.TYPE_APPLICATION_JSON.equalsIgnoreCase(getContentType()))
return null;
try {
json = (JSNode) JSParser.parseJson(stream.getInputStream());
} catch (Exception e) {
throw new ApiException(e);
} finally {
stream = null;
}
}
if (json == null && text != null) {
try
{
json = (JSNode) JSParser.parseJson(text);
text = null;
}
catch(Exception ex){
//maybe not json, OK
}
}
return json;
}
public String getText() {
if (stream != null) {
try {
text = Utils.read(stream.getInputStream());
} catch (Exception e) {
throw new ApiException(e);
} finally {
stream = null;
}
}
if (text == null && json != null)
return json.toString();
return text;
}
public StreamBuffer getBody() {
boolean explain = false;
Request req = getRequest();
if (req == null && Chain.getDepth() > 0)
req = Chain.peek().getRequest();
if (req != null)
explain = req.isDebug() && req.isExplain();
return getBody(explain);
}
public StreamBuffer getBody(boolean explain) {
StreamBuffer output = stream;
try {
if (output == null) {
if (json != null) {
output = new StreamBuffer();
output.write(json.toString().getBytes(StandardCharsets.UTF_8));
output.withContentType(MimeTypes.TYPE_APPLICATION_JSON);
} else if (text != null) {
output = new StreamBuffer();
output.write(text.getBytes(StandardCharsets.UTF_8));
String contentType = getContentType();
if (contentType == null)
contentType = MimeTypes.TYPE_TEXT_PLAIN;
output.withContentType(contentType);
}
}
if (explain) {
StringBuilder buff = new StringBuilder("");
buff.append(debug.toString());
if (error != null) {
buff.append("\r\n<< error ----------------");
buff.append("\r\n").append(Utils.getShortCause(error));
}
buff.append("\r\n<< response -------------");
buff.append("\r\n").append(getStatus());
buff.append("\r\n");
for (String key : getHeaders().keySet()) {
buff.append("\r\n").append(key).append(" ").append(getHeader(key));
}
buff.append("\r\n");
if (output != null) {
String text = Utils.read(output.getInputStream());
buff.append(text);
}
output = new StreamBuffer();
output.withContentType(MimeTypes.TYPE_TEXT_PLAIN);
output.write(buff.toString().getBytes(StandardCharsets.UTF_8));
}
} catch (IOException e) {
throw new ApiException(e);
}
return output;
}
public Response debug(String format, Object... args) {
debug.append(Utils.format(format, args)).append("\r\n");
return this;
}
public String getDebug() {
try {
return Utils.read(getBody(true).getInputStream());
} catch (IOException ex) {
throw new ApiException(ex);
}
}
public Response dump() {
System.out.println(getDebug());
return this;
}
public long getStartAt() {
return startAt;
}
public Response withStartAt(long startAt) {
this.startAt = startAt;
return this;
}
public long getEndAt() {
return endAt;
}
public Response withEndAt(long endAt) {
this.endAt = endAt;
return this;
}
public Response withUrl(String url) {
if (!Utils.empty(url)) {
url = url.trim();
url = url.replaceAll(" ", "%20");
}
this.url = url;
if (Utils.empty(fileName)) {
try {
String fileName = new URL(url).getFile();
withFileName(fileName);
} catch (Exception ex) {
//intentionally blank
}
}
return this;
}
public Response withFileName(String fileName) {
this.fileName = fileName;
return this;
}
public String getUrl() {
if (url == null && request != null)
return request.getUrl().toString();
return url;
}
public Response withRequest(Request request) {
this.request = request;
return this;
}
public Request getRequest() {
return request;
}
public Op getOp() {
return request != null ? request.getOp() : null;
}
public Chain getChain() {
if (chain == null && request != null)
return request.getChain();
return chain;
}
public Response withChain(Chain chain) {
this.chain = chain;
return this;
}
public Engine getEngine() {
return getChain() != null ? getChain().getEngine() : null;
}
@Override
public String toString() {
return debug.toString();
}
public Response withError(Throwable ex) {
this.error = ex;
return this;
}
public Throwable getError() {
return error;
}
//----------------------------------------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------------------------------------
//Meta Construction
public Response withMeta(String key, Object value) {
JSNode json = getJson();
JSNode meta = json.getMap("meta");
if (meta == null)
meta = json;
meta.put(key, value);
return this;
}
public JSNode getMeta() {
JSNode json = getJson();
JSNode meta = json.getMap("meta");
if (meta == null)
meta = json;
return meta;
}
public Response withFoundRows(int foundRows) {
withMeta("foundRows", foundRows);
updatePageCount();
return this;
}
public int getFoundRows() {
return getMeta().findInt("foundRows");
}
public Response withLastKey(String lastKey) {
withMeta("lastKey", lastKey);
return this;
}
public String getLastKey() {
return getMeta().findString("lastKey");
}
public Response withPageSize(int pageSize) {
withMeta("pageSize", pageSize);
return this;
}
public int getPageSize() {
return getMeta().findInt("pageSize");
}
public Response withPageNum(int pageNum) {
withMeta("pageNum", pageNum);
updatePageCount();
return this;
}
public int getPageNum() {
return getMeta().findInt("pageNum");
}
public Response withPageCount(int pageCount) {
withMeta("pageCount", pageCount);
return this;
}
public int getPageCount() {
return getMeta().findInt("pageCount");
}
protected void updatePageCount() {
int ps = getPageSize();
int fr = getFoundRows();
if (ps > 0 && fr > 0) {
int pageCount = fr / ps + (fr % ps == 0 ? 0 : 1);
withPageCount(pageCount);
}
}
public Response withLink(String name, String url) {
JSNode links = getJson().findMap("_links");
if (links != null) {
links.put(name, new JSMap("href", url));
} else {
getMeta().put(name, url);
}
return this;
}
public String getLink(String name) {
JSNode links = getJson().findMap("_links");
if (links != null) {
JSNode link = links.getNode(name);
if (link != null)
return link.getString("href");
} else {
Object link = getMeta().get(name);
if (link instanceof String)
return (String) link;
}
return null;
}
public String getSelf() {
return getLink("self");
}
public Response withSelf(String url) {
withLink("self", url);
return this;
}
public String getNext() {
return getLink("next");
}
public Response withNext(String url) {
withLink("next", url);
return this;
}
public String getPrev() {
return getLink("prev");
}
public Response withPrev(String url) {
withLink("prev", url);
return this;
}
public String getFirst() {
return getLink("first");
}
public Response withFirst(String url) {
withLink("first", url);
return this;
}
public String getLast() {
return getLink("last");
}
public Response withLast(String url) {
withLink("last", url);
return this;
}
//----------------------------------------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------------------------------------
//Data Construction
public JSList data() {
JSNode json = getJson();
if (json == null)
return null;
if (json instanceof JSList)
return (JSList) json;
if (json.get("data") instanceof JSList)
return json.getList("data");
if (json.get("_embedded") instanceof JSList)
return json.getList("_embedded");
if (json.get("items") instanceof JSList)
return json.getList("items");
//-- there is a single object in the payload without a meta or payload section
//-- this could mess up some callers that try to add to the returned
//-- JSList instead of calling withRecord
return new JSList(json);
}
public JSMap getFirstRecordAsMap() {
JSList data = data();
if (data == null || data.size() == 0)
return null;
return data.getMap(0);
}
public Response withRecord(Object record) {
JSList data = data();
if (data == null) {
data = new JSList();
getJson().put("data", data);
}
data.add(record);
return this;
}
public Response withRecords(List records) {
for (Object record : records)
withRecord(record);
return this;
}
//----------------------------------------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------------------------------------
//Status & Headers
public boolean isSuccess() {
return statusCode >= 200 && statusCode <= 300 && error == null;
}
/**
* @param status - one of the SC constants ex "200 OK"
* @return this
*/
public Response withStatus(String status) {
statusMesg = status;
try {
statusCode = Integer.parseInt(status.substring(0, 3));
if (statusMesg.length() > 4) {
statusMesg = status.substring(4);
}
} catch (Exception ex) {
//the status message did not start with numeric status code.
//this can be ignored.
}
return this;
}
public boolean hasStatus(int... statusCodes) {
for (int statusCode : statusCodes) {
if (this.statusCode == statusCode)
return true;
}
return false;
}
public String getStatus() {
return statusCode + " " + statusMesg;
}
public Response withStatusCode(int statusCode) {
this.statusCode = statusCode;
return this;
}
/**
* @return the statusCode
*/
public int getStatusCode() {
return statusCode;
}
/**
* @return the statusMesg
*/
public String getStatusMesg() {
return statusMesg;
}
public Response withStatusMesg(String statusMesg) {
this.statusMesg = statusMesg;
return this;
}
/**
* @return the headers
*/
public ListMap getHeaders() {
return headers;
}
public void withHeaders(ListMap headers) {
this.headers.putAll(headers);
}
public String getHeader(String key) {
List vals = headers.get(key);
if (vals != null && vals.size() > 0)
return Utils.implode(",", vals);
return null;
}
public void withHeader(String key, String value) {
if (!headers.containsMapping(key, value))
headers.put(key, value);
}
public void clearHeaders(){
this.headers.clear();
}
public String getRedirect() {
return getHeader("Location");
}
public Response withRedirect(String redirect) {
if (redirect == null) {
headers.remove("Location");
if (308 == getStatusCode())
withStatus(Status.SC_200_OK);
} else {
withHeader("Location", redirect);
withStatus(Status.SC_308_PERMANENT_REDIRECT);
}
return this;
}
public Response withContentType(String contentType) {
headers.remove("Content-Type");
withHeader("Content-Type", contentType);
return this;
}
public String getContentType() {
String contentType = getHeader("Content-Type");
if (contentType != null)
return contentType;
if (json != null) {
return MimeTypes.TYPE_APPLICATION_JSON;
}
if (fileName != null) {
int dot = fileName.lastIndexOf('.');
if (dot > 0) {
String ext = fileName.substring(dot + 1);
contentType = MimeTypes.getMimeType(ext);
if (contentType != null)
return contentType;
}
}
if (stream != null)
return stream.getContentType();
return contentType;
}
/**
* This is the value returned from the server via the "Content-Length" header
* NOTE: this will not match file length, for partial downloads, consider also using ContentRangeSize
*
* @return the value of the Content-Length header if it exists else 0
*/
public long getContentLength() {
String length = getHeader("Content-Length");
if (length != null) {
return Long.parseLong(length);
}
return 0;
}
//----------------------------------------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------------------------------------
//Changes
public List getChanges() {
return changes;
}
public Response withChanges(java.util.Collection changes) {
this.changes.addAll(changes);
return this;
}
public Response withChange(String method, String collectionKey, Object resourceKey) {
if (resourceKey instanceof List) {
List deletedIds = (List) resourceKey;
for (Object id : deletedIds) {
changes.add(new Change(method, collectionKey, id));
}
} else {
changes.add(new Change(method, collectionKey, resourceKey));
}
return this;
}
public Response withChange(String method, String collectionKey, String... resourceKeys) {
for (int i = 0; resourceKeys != null && i < resourceKeys.length; i++)
withChange(method, collectionKey, resourceKeys[i]);
return this;
}
//----------------------------------------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------------------------------------
//TEST ASSERTION CONVENIENCE METHODS
public Response assertOk(String... messages) {
if (statusCode < 200 || statusCode > 299) {
rethrow(statusCode, messages);
}
return this;
}
public void rethrow() {
rethrow(statusCode);
}
public void rethrow(int statusCode) {
rethrow(statusCode, (String[]) null);
}
public void rethrow(String... messages) {
rethrow(statusCode, messages);
}
public void rethrow(int statusCode, String... messages) {
if (error != null)
Utils.rethrow(error);
statusCode = statusCode > 399 ? statusCode : 500;
StringBuilder msg = new StringBuilder();
for (int i = 0; messages != null && i < messages.length; i++)
msg.append(messages[i]).append("\r\n ");
String message = getText();
try {
while (message != null && message.startsWith("{") && message.contains("\\\"message\\\"")) {
message = JSParser.asJSNode(message).getString("message");
}
} catch (Exception ex) {
//igore
}
if (message != null)
msg.append(" ").append(message.trim());
throw new ApiException(statusCode + "", (msg != null ? msg.toString() : null));
}
public Response assertStatus(int... statusCodes) {
return assertStatus(null, statusCodes);
}
public Response assertStatus(String message, int... statusCodes) {
boolean matched = false;
for (int statusCode : statusCodes) {
if (statusCode == this.statusCode) {
matched = true;
break;
}
}
if (!matched) {
Object[] args = null;
if (message == null) {
message = "The returned status '{}' was not in the approved list '{}'";
List debugList = new ArrayList<>();
for (int code : statusCodes) debugList.add(code);
args = new Object[]{this.statusCode, debugList};
}
throw ApiException.new500InternalServerError(message, args);
}
return this;
}
public Response assertDebug(String lineMatch, String... matches) {
if (matches == null || matches.length == 0) {
matches = new String[]{lineMatch.substring(lineMatch.indexOf(" ") + 1)};
lineMatch = lineMatch.substring(0, lineMatch.indexOf(" "));
}
String debug = getDebug();
debug = debug.substring(0, debug.indexOf("<< response"));
int idx = debug.indexOf(" " + lineMatch + " ");
if (idx < 0) {
System.err.println("SKIPPING DEBUG MATCH: " + lineMatch + " " + Arrays.asList(matches));
return this;
}
String debugLine = debug.substring(idx, debug.indexOf("\n", idx)).trim();
for (String match : matches) {
List matchTokens = Utils.split(match, ' ', '\'', '"', '{', '}');
for (String matchToken : matchTokens) {
if (!debugLine.contains(matchToken)) {
String msg = "ERROR: Can't find match token in debug line";
msg += "\r\n" + " - debug line : " + debugLine;
msg += "\r\n" + " - missing token : " + matchToken;
msg += "\r\n" + debug;
System.err.println(msg);
Utils.error(msg);
}
}
}
return this;
}
}