/*
* Copyright (c) 2013-2016. Urban Airship and Contributors
*/
package com.urbanairship.api.client;
import com.google.common.base.Optional;
import com.urbanairship.api.client.parse.RequestErrorObjectMapper;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.type.TypeReference;
import java.io.IOException;
import java.util.Map;
/**
* Error object for API requests.
* This is populated the as many details as are possible at the time of the
* error. Optional values from Google Guava are used in place of values
* that may not be present.
*/
public final class RequestError {
/* Header keys, values */
private final static String CONTENT_TYPE_TEXT_HTML = "text/html";
private final static String CONTENT_TYPE_JSON = "application/json";
private final static String UA_APPLICATION_JSON = "application/vnd.urbanairship+json";
private final static String UA_APPLICATION_JSON_V3 = "application/vnd.urbanairship+json;version=3";
private final boolean ok;
private final Optional<String> operationId;
private final String error;
private final Optional<Number> errorCode;
private final Optional<RequestErrorDetails> details;
private RequestError(boolean ok, Optional<String> operationId, String error, Optional<Number> errorCode,
Optional<RequestErrorDetails> details) {
this.ok = ok;
this.operationId = operationId;
if (error == null || error.isEmpty()) {
throw new IllegalArgumentException("Error cannot be null or empty");
}
this.error = error;
this.errorCode = errorCode;
this.details = details;
}
/**
* Create an APIError from the response if it conforms to the API v3
* Currently, three types of error bodies are returned, text/html strings,
* basic JSON errors {"error":"message"} and the v3 error spec. This method
* parses between three, and returns a best effort response.
*
* @param body Response body for the request that caused the exception
* @return APIError
* @throws IOException
*/
public static RequestError errorFromResponse(String body, String contentType) throws IOException {
// Text/html
if (contentType.equalsIgnoreCase(CONTENT_TYPE_TEXT_HTML)) {
return nonJSONError(body);
}
// JSON but not v3
else if (contentType.equalsIgnoreCase(CONTENT_TYPE_JSON)) {
return nonV3JSONError(body);
}
// v3 JSON parsing
else if (contentType.equalsIgnoreCase(UA_APPLICATION_JSON) || contentType.equalsIgnoreCase(UA_APPLICATION_JSON_V3)) {
ObjectMapper mapper = RequestErrorObjectMapper.getInstance();
return mapper.readValue(body, RequestError.class);
}
// wut?
else {
return RequestError.newBuilder()
.setError("Unknown response parsing error")
.build();
}
}
/*
Currently sending text/plain errors for some requests, currently 404's
do this. API-291, 12JUL13
*/
@Deprecated
private static RequestError nonJSONError(String body) throws
IOException {
return RequestError.newBuilder()
.setError(body)
.build();
}
/*
Currently returning JSON, but not API v3 JSON for some requests.
API-281, 12JUL13
*/
@Deprecated
private static RequestError nonV3JSONError(String body) throws IOException {
ObjectMapper mapper = RequestErrorObjectMapper.getInstance();
Map<String, String> errorMsg =
mapper.readValue(body,
new TypeReference<Map<String, String>>() {
});
return RequestError.newBuilder()
.setError(errorMsg.get("message"))
.build();
}
/**
* Returns a APIError Builder
*
* @return Builder
*/
public static Builder newBuilder() {
return new Builder();
}
public boolean getOk() {
return ok;
}
/**
* Returns the operation id for the error. This value is useful for debugging
* errors within the Urban Airship system, and should be sent to UA support
* in cases where there is an issue. This value may be absent.
*
* @return Optional Operation ID
*/
public Optional<String> getOperationId() {
return operationId;
}
/**
* Returns a description of the error
*
* @return Optional error description.
*/
public String getError() {
return error;
}
/**
* Returns an error code specific to the Urban Airship. This value may
* be absent.
*
* @return Optional error code
*/
public Optional<Number> getErrorCode() {
return errorCode;
}
/**
* Returns the details of this error if error details are available from the
* API. Currently errors from requests that are syntactically valid but
* otherwise malformed, (missing fields, incompatible parameters, etc) should
* return details to help identify the issue.
*
* @return Optional details object
*/
public Optional<RequestErrorDetails> getDetails() {
return details;
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("ok:");
stringBuilder.append(getOk());
stringBuilder.append("\nRequestError:");
stringBuilder.append(getError());
if (errorCode.isPresent()) {
stringBuilder.append("\nCode:");
stringBuilder.append(getErrorCode());
}
if (details.isPresent()) {
stringBuilder.append("\nDetails:");
stringBuilder.append(details.get().toString());
}
return stringBuilder.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof RequestError)) return false;
RequestError that = (RequestError) o;
if (ok != that.ok) return false;
if (details != null ? !details.equals(that.details) : that.details != null) return false;
if (error != null ? !error.equals(that.error) : that.error != null) return false;
if (errorCode != null ? !errorCode.equals(that.errorCode) : that.errorCode != null) return false;
if (operationId != null ? !operationId.equals(that.operationId) : that.operationId != null) return false;
return true;
}
@Override
public int hashCode() {
int result = (ok ? 1 : 0);
result = 31 * result + (operationId != null ? operationId.hashCode() : 0);
result = 31 * result + (error != null ? error.hashCode() : 0);
result = 31 * result + (errorCode != null ? errorCode.hashCode() : 0);
result = 31 * result + (details != null ? details.hashCode() : 0);
return result;
}
/**
* Builds an APIError.
*/
public static class Builder {
private boolean ok;
private String operationId;
private String error;
private Number errorCode;
private RequestErrorDetails details;
public Builder setOk(boolean ok) {
this.ok = ok;
return this;
}
/**
* Set the operation id. This is optional
*
* @param operationId Operation id
* @return This Builder
*/
public Builder setOperationId(String operationId) {
this.operationId = operationId;
return this;
}
/**
* Set a description for the error. This is required.
*
* @param error Human readable error description
* @return This Builder
*/
public Builder setError(String error) {
this.error = error;
return this;
}
/**
* Set the error code.
*
* @param errorCode The error code.
* @return Build
*/
public Builder setErrorCode(Number errorCode) {
this.errorCode = errorCode;
return this;
}
/**
* Set the error details.
*
* @param details The RequestErrorDetails.
* @return Builder
*/
public Builder setDetails(RequestErrorDetails details) {
this.details = details;
return this;
}
/**
* Build the RequestError instance.
*
* @return The RequestError instance.
*/
public RequestError build() {
return new RequestError(ok,
Optional.fromNullable(operationId),
error,
Optional.fromNullable(errorCode),
Optional.fromNullable(details));
}
}
}