blob: 99d074b88d5b82a43f9d43340b782f7169ab785f [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.juneau.rest.client;
import static org.apache.juneau.internal.IOUtils.*;
import static org.apache.juneau.internal.StringUtils.*;
import static org.apache.juneau.httppart.HttpPartType.*;
import static org.apache.http.HttpStatus.*;
import java.io.*;
import java.lang.reflect.*;
import java.lang.reflect.Proxy;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.logging.*;
import java.util.regex.*;
import org.apache.http.*;
import org.apache.http.Header;
import org.apache.http.client.*;
import org.apache.http.client.config.*;
import org.apache.http.client.entity.*;
import org.apache.http.client.methods.*;
import org.apache.http.client.utils.*;
import org.apache.http.conn.*;
import org.apache.http.entity.*;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.*;
import org.apache.http.util.*;
import org.apache.juneau.*;
import org.apache.juneau.encoders.*;
import org.apache.juneau.http.*;
import org.apache.juneau.http.annotation.*;
import org.apache.juneau.httppart.*;
import org.apache.juneau.httppart.bean.*;
import org.apache.juneau.internal.*;
import org.apache.juneau.oapi.*;
import org.apache.juneau.parser.*;
import org.apache.juneau.parser.ParseException;
import org.apache.juneau.reflect.*;
import org.apache.juneau.serializer.*;
import org.apache.juneau.utils.*;
/**
* Represents a connection to a remote REST resource.
*
* <p>
* Instances of this class are created by the various {@code doX()} methods on the {@link RestClient} class.
*
* <p>
* This class uses only Java standard APIs. Requests can be built up using a fluent interface with method chaining,
* like so...
* <p class='bcode w800'>
* RestClient client = <jk>new</jk> RestClient();
* RestCall c = client.doPost(<jsf>URL</jsf>).setInput(o).setHeader(x,y);
* MyBean b = c.getResponse(MyBean.<jk>class</jk>);
* </p>
*
* <p>
* The actual connection and request/response transaction occurs when calling one of the <c>getResponseXXX()</c>
* methods.
*
* <ul class='seealso'>
* <li class='link'>{@doc juneau-rest-client}
* </ul>
*/
@SuppressWarnings({ "unchecked" })
public final class RestCall extends BeanSession implements Closeable {
private static final ContentType TEXT_PLAIN = ContentType.create("text/plain");
private final RestClient client; // The client that created this call.
private final HttpRequestBase request; // The request.
private HttpResponse response; // The response.
private List<RestCallInterceptor> interceptors = new ArrayList<>(); // Used for intercepting and altering requests.
private boolean isConnected = false; // connect() has been called.
private boolean allowRedirectsOnPosts;
private int retries = 1;
private int redirectOnPostsTries = 5;
private long retryInterval = -1;
private RetryOn retryOn;
private boolean ignoreErrors;
private boolean byLines = false;
private TeeWriter writers = new TeeWriter();
private StringWriter capturedResponseWriter;
private String capturedResponse;
private TeeOutputStream outputStreams = new TeeOutputStream();
private boolean isClosed = false;
private boolean isFailed = false;
private Object input;
private boolean hasInput; // input() was called, even if it's setting 'null'.
private Serializer serializer;
private Parser parser;
private HttpPartSerializer partSerializer;
private HttpPartParser partParser;
private HttpPartSchema requestBodySchema, responseBodySchema;
private URIBuilder uriBuilder;
private NameValuePairs formData;
private boolean softClose = false; // If true, don't consume response and set isClosed flag, but do call listeners.
/**
* Constructs a REST call with the specified method name.
*
* @param client The client that created this request.
* @param request The wrapped Apache HTTP client request object.
* @param uri The URI for this call.
* @throws RestCallException If an exception or non-200 response code occurred during the connection attempt.
*/
protected RestCall(RestClient client, HttpRequestBase request, URI uri) throws RestCallException {
super(client, BeanSessionArgs.DEFAULT);
this.client = client;
this.request = request;
for (RestCallInterceptor i : this.client.interceptors)
interceptor(i);
this.retryOn = client.retryOn;
this.retries = client.retries;
this.retryInterval = client.retryInterval;
this.serializer = client.serializer;
this.parser = client.parser;
this.partSerializer = client.getPartSerializer();
this.partParser = client.getPartParser();
uriBuilder = new URIBuilder(uri);
}
/**
* Sets the URI for this call.
*
* <p>
* Can be any of the following types:
* <ul>
* <li>{@link URI}
* <li>{@link URL}
* <li>{@link URIBuilder}
* <li>Anything else converted to a string using {@link Object#toString()}.
* </ul>
*
* <p>
* Relative URL strings will be interpreted as relative to the root URL defined on the client.
*
* @param uri
* The URI to use for this call.
* This overrides the URI passed in from the client.
* @return This object (for method chaining).
* @throws RestCallException Invalid URI syntax detected.
*/
public RestCall uri(Object uri) throws RestCallException {
try {
if (uri != null)
uriBuilder = new URIBuilder(client.toURI(uri));
return this;
} catch (URISyntaxException e) {
throw new RestCallException(e);
}
}
/**
* Sets the URI scheme.
*
* @param scheme The new URI host.
* @return This object (for method chaining).
*/
public RestCall scheme(String scheme) {
uriBuilder.setScheme(scheme);
return this;
}
/**
* Sets the URI host.
*
* @param host The new URI host.
* @return This object (for method chaining).
*/
public RestCall host(String host) {
uriBuilder.setHost(host);
return this;
}
/**
* Sets the URI port.
*
* @param port The new URI port.
* @return This object (for method chaining).
*/
public RestCall port(int port) {
uriBuilder.setPort(port);
return this;
}
/**
* Adds a query parameter to the URI query.
*
* @param name
* The parameter name.
* Can be null/blank/* if the value is a {@link Map}, {@link String}, {@link NameValuePairs}, or bean.
* @param value
* The parameter value converted to a string using UON notation.
* Can also be {@link Map}, {@link String}, {@link NameValuePairs}, or bean if the name is null/blank/*.
* If a {@link String} and the name is null/blank/*, then calls {@link URIBuilder#setCustomQuery(String)}.
* @param skipIfEmpty Don't add the pair if the value is empty.
* @param serializer
* The serializer to use for serializing the value to a string value.
* If <jk>null</jk>, then the URL-encoding serializer defined on the client is used.
* @param schema
* The schema object that defines the format of the output.
* <br>If <jk>null</jk>, defaults to the schema defined on the serializer.
* <br>If that's also <jk>null</jk>, defaults to {@link HttpPartSchema#DEFAULT}.
* <br>Only used if serializer is schema-aware (e.g. {@link OpenApiSerializer}).
* @return This object (for method chaining).
* @throws RestCallException Error occurred.
*/
public RestCall query(String name, Object value, boolean skipIfEmpty, HttpPartSerializer serializer, HttpPartSchema schema) throws RestCallException {
if (serializer == null)
serializer = client.getPartSerializer();
if (schema == null)
schema = HttpPartSchema.DEFAULT;
boolean isMulti = isEmpty(name) || "*".equals(name) || value instanceof NameValuePairs;
if (! isMulti) {
if (canAdd(value, schema, skipIfEmpty))
try {
uriBuilder.addParameter(name, serializer.serialize(QUERY, schema, value));
} catch (SchemaValidationException e) {
throw new RestCallException(e, "Validation error on request query parameter ''{0}''=''{1}''", name, value);
} catch (SerializeException e) {
throw new RestCallException(e, "Serialization error on request query parameter ''{0}''", name);
}
} else if (value instanceof NameValuePairs) {
for (NameValuePair p : (NameValuePairs)value) {
String n = p.getName();
String v = p.getValue();
HttpPartSchema s = schema.getProperty(n);
if (canAdd(v, s, skipIfEmpty))
query(n, v, skipIfEmpty, serializer, s);
}
} else if (value instanceof Map) {
for (Map.Entry<String,Object> p : ((Map<String,Object>) value).entrySet()) {
String n = p.getKey();
Object v = p.getValue();
HttpPartSchema s = schema.getProperty(n);
if (canAdd(v, s, skipIfEmpty))
query(n, v, skipIfEmpty, serializer, s);
}
} else if (isBean(value)) {
return query(name, toBeanMap(value), skipIfEmpty, serializer, schema);
} else if (value instanceof Reader || value instanceof InputStream) {
try {
uriBuilder.setCustomQuery(read(value));
} catch (IOException e) {
throw new RestCallException(e);
}
} else if (value instanceof CharSequence) {
String s = value.toString();
if (isNotEmpty(s))
uriBuilder.setCustomQuery(s);
} else {
throw new RestCallException("Invalid name ''{0}'' passed to query(name,value,skipIfEmpty) for data type ''{1}''", name, className(value));
}
return this;
}
/**
* Adds a query parameter to the URI query.
*
* @param name The parameter name.
* @param value The parameter value converted to a string using UON notation.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall query(String name, Object value) throws RestCallException {
return query(name, value, false, null, null);
}
/**
* Adds query parameters to the URI query.
*
* @param params The parameters. Values are converted to a string using UON notation.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall query(Map<String,Object> params) throws RestCallException {
return query(null, params);
}
/**
* Adds a query parameter to the URI query if the parameter value is not <jk>null</jk> or an empty string.
*
* <p>
* NE = "not empty"
*
* @param name The parameter name.
* @param value The parameter value converted to a string using UON notation.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall queryIfNE(String name, Object value) throws RestCallException {
return query(name, value, true, null, null);
}
/**
* Adds query parameters to the URI for any parameters that aren't null/empty.
*
* <p>
* NE = "not empty"
*
* @param params The parameters. Values are converted to a string using UON notation.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall queryIfNE(Map<String,Object> params) throws RestCallException {
return query(null, params, true, null, null);
}
/**
* Sets a custom URI query.
*
* @param query The new URI query string.
* @return This object (for method chaining).
*/
public RestCall query(String query) {
uriBuilder.setCustomQuery(query);
return this;
}
/**
* Adds a form data pair to this request to perform a URL-encoded form post.
*
* @param name
* The parameter name.
* Can be null/blank/* if the value is a {@link Map}, {@link NameValuePairs}, or bean.
* @param value
* The parameter value converted to a string using UON notation.
* Can also be {@link Map}, {@link NameValuePairs}, or bean if the name is null/blank/*.
* @param skipIfEmpty Don't add the pair if the value is empty.
* @param serializer
* The serializer to use for serializing the value to a string value.
* If <jk>null</jk>, then the URL-encoding serializer defined on the client is used.
* @param schema
* The schema object that defines the format of the output.
* <br>If <jk>null</jk>, defaults to the schema defined on the serializer.
* <br>If that's also <jk>null</jk>, defaults to {@link HttpPartSchema#DEFAULT}.
* <br>Only used if serializer is schema-aware (e.g. {@link OpenApiSerializer}).
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall formData(String name, Object value, boolean skipIfEmpty, HttpPartSerializer serializer, HttpPartSchema schema) throws RestCallException {
if (formData == null)
formData = new NameValuePairs();
if (serializer == null)
serializer = client.getPartSerializer();
if (schema == null)
schema = HttpPartSchema.DEFAULT;
boolean isMulti = isEmpty(name) || "*".equals(name) || value instanceof NameValuePairs;
if (! isMulti) {
if (canAdd(value, schema, skipIfEmpty))
formData.add(new SerializedNameValuePair(name, value, serializer, schema));
} else if (value instanceof NameValuePairs) {
for (NameValuePair p : (NameValuePairs)value) {
String n = p.getName();
String v = p.getValue();
HttpPartSchema s = schema.getProperty(n);
if (canAdd(v, s, skipIfEmpty))
formData.add(p);
}
} else if (value instanceof Map) {
for (Map.Entry<String,Object> p : ((Map<String,Object>) value).entrySet()) {
String n = p.getKey();
Object v = p.getValue();
HttpPartSchema s = schema.getProperty(n);
if (canAdd(v, s, skipIfEmpty))
formData(n, v, skipIfEmpty, serializer, s);
}
} else if (isBean(value)) {
return formData(name, toBeanMap(value), skipIfEmpty, serializer, schema);
} else if (value instanceof Reader || value instanceof InputStream) {
contentType("application/x-www-form-urlencoded");
body(value);
} else if (value instanceof CharSequence) {
try {
contentType("application/x-www-form-urlencoded");
body(new StringEntity(value.toString()));
} catch (UnsupportedEncodingException e) {}
} else {
throw new FormattedRuntimeException("Invalid name ''{0}'' passed to formData(name,value,skipIfEmpty) for data type ''{1}''", name, className(value));
}
return this;
}
/**
* Adds a form data pair to this request to perform a URL-encoded form post.
*
* @param name
* The parameter name.
* Can be null/blank if the value is a {@link Map} or {@link NameValuePairs}.
* @param value
* The parameter value converted to a string using UON notation.
* Can also be a {@link Map} or {@link NameValuePairs}.
* @return This object (for method chaining).
* @throws RestCallException If name was null/blank and value wasn't a {@link Map} or {@link NameValuePairs}.
*/
public RestCall formData(String name, Object value) throws RestCallException {
return formData(name, value, false, null, null);
}
/**
* Adds form data pairs to this request to perform a URL-encoded form post.
*
* @param nameValuePairs The name-value pairs of the request.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall formData(NameValuePairs nameValuePairs) throws RestCallException {
return formData(null, nameValuePairs);
}
/**
* Adds form data pairs to this request to perform a URL-encoded form post.
*
* @param params The parameters. Values are converted to a string using UON notation.
* @return This object (for method chaining).
* @throws RestCallException If name was null/blank and value wasn't a {@link Map} or {@link NameValuePairs}.
*/
public RestCall formData(Map<String,Object> params) throws RestCallException {
return formData(null, params);
}
/**
* Adds a form data pair to the request if the parameter value is not <jk>null</jk> or an empty string.
*
* <p>
* NE = "not empty"
*
* @param name The parameter name.
* @param value The parameter value converted to a string using UON notation.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall formDataIfNE(String name, Object value) throws RestCallException {
return formData(name, value, true, null, null);
}
/**
* Adds form data parameters to the request for any parameters that aren't null/empty.
*
* <p>
* NE = "not empty"
*
* @param params The parameters. Values are converted to a string using UON notation.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall formDataIfNE(Map<String,Object> params) throws RestCallException {
return formData(null, params, true, null, null);
}
/**
* Replaces a variable of the form <js>"{name}"</js> in the URL path with the specified value.
*
* @param name The path variable name.
* @param value The replacement value.
* @param serializer
* The serializer to use for serializing the value to a string value.
* If <jk>null</jk>, then the URL-encoding serializer defined on the client is used.
* @param schema
* The schema object that defines the format of the output.
* <br>If <jk>null</jk>, defaults to the schema defined on the serializer.
* <br>If that's also <jk>null</jk>, defaults to {@link HttpPartSchema#DEFAULT}.
* <br>Only used if serializer is schema-aware (e.g. {@link OpenApiSerializer}).
* @return This object (for method chaining).
* @throws RestCallException If variable could not be found in path.
*/
public RestCall path(String name, Object value, HttpPartSerializer serializer, HttpPartSchema schema) throws RestCallException {
String path = uriBuilder.getPath();
if (serializer == null)
serializer = client.getPartSerializer();
if (schema == null)
schema = HttpPartSchema.DEFAULT;
boolean isMulti = isEmpty(name) || "*".equals(name) || value instanceof NameValuePairs;
if (! isMulti) {
String var = "{" + name + "}";
if (path.indexOf(var) == -1 && ! name.equals("/*"))
throw new RestCallException("Path variable {"+name+"} was not found in path.");
try {
String p = null;
if (name.equals("/*"))
p = path.replaceAll("\\/\\*$", serializer.serialize(PATH, schema, value));
else
p = path.replace(var, serializer.serialize(PATH, schema, value));
uriBuilder.setPath(p);
} catch (SchemaValidationException e) {
throw new RestCallException(e, "Validation error on request path parameter ''{0}''=''{1}''", name, value);
} catch (SerializeException e) {
throw new RestCallException(e, "Serialization error on request path parameter ''{0}''", name);
}
} else if (value instanceof NameValuePairs) {
for (NameValuePair p : (NameValuePairs)value) {
String n = p.getName();
String v = p.getValue();
HttpPartSchema s = schema.getProperty(n);
path(n, v, serializer, s);
}
} else if (value instanceof Map) {
for (Map.Entry<String,Object> p : ((Map<String,Object>) value).entrySet()) {
String n = p.getKey();
Object v = p.getValue();
HttpPartSchema s = schema.getProperty(n);
path(n, v, serializer, s);
}
} else if (isBean(value)) {
return path(name, toBeanMap(value), serializer, schema);
} else if (value != null) {
throw new RestCallException("Invalid name ''{0}'' passed to path(name,value) for data type ''{1}''", name, className(value));
}
return this;
}
/**
* Replaces a variable of the form <js>"{name}"</js> in the URL path with the specified value.
*
* @param name The path variable name.
* @param value The replacement value.
* @return This object (for method chaining).
* @throws RestCallException If variable could not be found in path.
*/
public RestCall path(String name, Object value) throws RestCallException {
return path(name, value, null, null);
}
/**
* Sets the URI user info.
*
* @param userInfo The new URI user info.
* @return This object (for method chaining).
*/
public RestCall userInfo(String userInfo) {
uriBuilder.setUserInfo(userInfo);
return this;
}
/**
* Sets the URI user info.
*
* @param username The new URI username.
* @param password The new URI password.
* @return This object (for method chaining).
*/
public RestCall userInfo(String username, String password) {
uriBuilder.setUserInfo(username, password);
return this;
}
/**
* Specifies the part schema for the request body.
*
* <p>
* This is only useful for schema-aware serializers such as {@link OpenApiSerializer}.
*
* @param value
* The new part schema for the request body.
* <br>Can be <jk>null</jk>.
* @return This object (for method chaining).
*/
public RestCall requestBodySchema(HttpPartSchema value) {
this.requestBodySchema = value;
return this;
}
/**
* Specifies the part schema for the response body.
*
* <p>
* This is only useful for schema-aware parsers such as {@link OpenApiParser}.
*
* @param value
* The new part schema for the response body.
* <br>Can be <jk>null</jk>.
* @return This object (for method chaining).
*/
public RestCall responseBodySchema(HttpPartSchema value) {
this.responseBodySchema = value;
return this;
}
/**
* Sets the input for this REST call.
*
* @param input
* The input to be sent to the REST resource (only valid for PUT and POST) requests.
* <br>Can be of the following types:
* <ul class='spaced-list'>
* <li>
* {@link Reader} - Raw contents of {@code Reader} will be serialized to remote resource.
* <li>
* {@link InputStream} - Raw contents of {@code InputStream} will be serialized to remote resource.
* <li>
* {@link Object} - POJO to be converted to text using the {@link Serializer} registered with the
* {@link RestClient}.
* <li>
* {@link HttpEntity} - Bypass Juneau serialization and pass HttpEntity directly to HttpClient.
* <li>
* {@link NameValuePairs} - Converted to a URL-encoded FORM post.
* </ul>
* @return This object (for method chaining).
* @throws RestCallException If a retry was attempted, but the entity was not repeatable.
*/
public RestCall body(Object input) throws RestCallException {
this.input = input;
this.hasInput = true;
this.formData = null;
return this;
}
/**
* Specifies the serializer to use on this call.
*
* <p>
* Overrides the serializer specified on the {@link RestClient}.
*
* @param serializer The serializer used to serialize POJOs to the body of the HTTP request.
* @return This object (for method chaining).
*/
public RestCall serializer(Serializer serializer) {
this.serializer = serializer;
return this;
}
/**
* Specifies the parser to use on this call.
*
* <p>
* Overrides the parser specified on the {@link RestClient}.
*
* @param parser The parser used to parse POJOs from the body of the HTTP response.
* @return This object (for method chaining).
*/
public RestCall parser(Parser parser) {
this.parser = parser;
return this;
}
//-----------------------------------------------------------------------------------------------------------------
// HTTP headers
//-----------------------------------------------------------------------------------------------------------------
/**
* Sets a header on the request.
*
* @param name
* The header name.
* The name can be null/empty if the value is a {@link Map}.
* @param value The header value.
* @param skipIfEmpty Don't add the header if the name is null/empty.
* @param serializer
* The serializer to use for serializing the value to a string value.
* If <jk>null</jk>, then the URL-encoding serializer defined on the client is used.
* @param schema
* The schema object that defines the format of the output.
* <br>If <jk>null</jk>, defaults to the schema defined on the serializer.
* <br>If that's also <jk>null</jk>, defaults to {@link HttpPartSchema#DEFAULT}.
* <br>Only used if serializer is schema-aware (e.g. {@link OpenApiSerializer}).
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall header(String name, Object value, boolean skipIfEmpty, HttpPartSerializer serializer, HttpPartSchema schema) throws RestCallException {
if (serializer == null)
serializer = client.getPartSerializer();
if (schema == null)
schema = HttpPartSchema.DEFAULT;
boolean isMulti = isEmpty(name) || "*".equals(name) || value instanceof NameValuePairs;
if (! isMulti) {
if (canAdd(value, schema, skipIfEmpty))
try {
request.setHeader(name, serializer.serialize(HEADER, schema, value));
} catch (SchemaValidationException e) {
throw new RestCallException(e, "Validation error on request header parameter ''{0}''=''{1}''", name, value);
} catch (SerializeException e) {
throw new RestCallException(e, "Serialization error on request header parameter ''{0}''", name);
}
} else if (value instanceof NameValuePairs) {
for (NameValuePair p : (NameValuePairs)value) {
String n = p.getName();
String v = p.getValue();
HttpPartSchema s = schema.getProperty(n);
if (canAdd(v, s, skipIfEmpty))
header(n, v, skipIfEmpty, serializer, s);
}
} else if (value instanceof Map) {
for (Map.Entry<String,Object> p : ((Map<String,Object>) value).entrySet()) {
String n = p.getKey();
Object v = p.getValue();
HttpPartSchema s = schema.getProperty(n);
if (canAdd(v, s, skipIfEmpty))
header(n, v, skipIfEmpty, serializer, s);
}
} else if (isBean(value)) {
return header(name, toBeanMap(value), skipIfEmpty, serializer, schema);
} else {
throw new RestCallException("Invalid name ''{0}'' passed to header(name,value,skipIfEmpty) for data type ''{1}''", name, className(value));
}
return this;
}
/**
* Sets a header on the request.
*
* @param name
* The header name.
* The name can be null/empty if the value is a {@link Map}.
* @param value The header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall header(String name, Object value) throws RestCallException {
return header(name, value, false, null, null);
}
/**
* Sets headers on the request.
*
* @param values The header values.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall headers(Map<String,Object> values) throws RestCallException {
return header(null, values, false, null, null);
}
/**
* Sets a header on the request if the value is not null/empty.
*
* <p>
* NE = "not empty"
*
* @param name
* The header name.
* The name can be null/empty if the value is a {@link Map}.
* @param value The header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall headerIfNE(String name, Object value) throws RestCallException {
return header(name, value, true, null, null);
}
/**
* Sets headers on the request if the values are not null/empty.
*
* <p>
* NE = "not empty"
*
* @param values The header values.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall headersIfNE(Map<String,Object> values) throws RestCallException {
return header(null, values, true, null, null);
}
/**
* Sets the value for the <c>Accept</c> request header.
*
* <p>
* This overrides the media type specified on the parser, but is overridden by calling
* <code>header(<js>"Accept"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall accept(Object value) throws RestCallException {
return header("Accept", value);
}
/**
* Sets the value for the <c>Accept-Charset</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"Accept-Charset"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall acceptCharset(Object value) throws RestCallException {
return header("Accept-Charset", value);
}
/**
* Sets the value for the <c>Accept-Encoding</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"Accept-Encoding"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall acceptEncoding(Object value) throws RestCallException {
return header("Accept-Encoding", value);
}
/**
* Sets the value for the <c>Accept-Language</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"Accept-Language"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall acceptLanguage(Object value) throws RestCallException {
return header("Accept-Language", value);
}
/**
* Sets the value for the <c>Authorization</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"Authorization"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall authorization(Object value) throws RestCallException {
return header("Authorization", value);
}
/**
* Sets the value for the <c>Cache-Control</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"Cache-Control"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall cacheControl(Object value) throws RestCallException {
return header("Cache-Control", value);
}
/**
* Sets the value for the <c>Connection</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"Connection"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall connection(Object value) throws RestCallException {
return header("Connection", value);
}
/**
* Sets the value for the <c>Content-Length</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"Content-Length"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall contentLength(Object value) throws RestCallException {
return header("Content-Length", value);
}
/**
* Sets the value for the <c>Content-Type</c> request header.
*
* <p>
* This overrides the media type specified on the serializer, but is overridden by calling
* <code>header(<js>"Content-Type"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall contentType(Object value) throws RestCallException {
return header("Content-Type", value);
}
/**
* Sets the value for the <c>Date</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"Date"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall date(Object value) throws RestCallException {
return header("Date", value);
}
/**
* Sets the value for the <c>Expect</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"Expect"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall expect(Object value) throws RestCallException {
return header("Expect", value);
}
/**
* Sets the value for the <c>Forwarded</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"Forwarded"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall forwarded(Object value) throws RestCallException {
return header("Forwarded", value);
}
/**
* Sets the value for the <c>From</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"From"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall from(Object value) throws RestCallException {
return header("From", value);
}
/**
* Sets the value for the <c>Host</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"Host"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall host(Object value) throws RestCallException {
return header("Host", value);
}
/**
* Sets the value for the <c>If-Match</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"If-Match"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall ifMatch(Object value) throws RestCallException {
return header("If-Match", value);
}
/**
* Sets the value for the <c>If-Modified-Since</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"If-Modified-Since"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall ifModifiedSince(Object value) throws RestCallException {
return header("If-Modified-Since", value);
}
/**
* Sets the value for the <c>If-None-Match</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"If-None-Match"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall ifNoneMatch(Object value) throws RestCallException {
return header("If-None-Match", value);
}
/**
* Sets the value for the <c>If-Range</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"If-Range"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall ifRange(Object value) throws RestCallException {
return header("If-Range", value);
}
/**
* Sets the value for the <c>If-Unmodified-Since</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"If-Unmodified-Since"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall ifUnmodifiedSince(Object value) throws RestCallException {
return header("If-Unmodified-Since", value);
}
/**
* Sets the value for the <c>Max-Forwards</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"Max-Forwards"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall maxForwards(Object value) throws RestCallException {
return header("Max-Forwards", value);
}
/**
* Sets the value for the <c>Origin</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"Origin"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall origin(Object value) throws RestCallException {
return header("Origin", value);
}
/**
* Sets the value for the <c>Pragma</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"Pragma"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall pragma(Object value) throws RestCallException {
return header("Pragma", value);
}
/**
* Sets the value for the <c>Proxy-Authorization</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"Proxy-Authorization"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall proxyAuthorization(Object value) throws RestCallException {
return header("Proxy-Authorization", value);
}
/**
* Sets the value for the <c>Range</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"Range"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall range(Object value) throws RestCallException {
return header("Range", value);
}
/**
* Sets the value for the <c>Referer</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"Referer"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall referer(Object value) throws RestCallException {
return header("Referer", value);
}
/**
* Sets the value for the <c>TE</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"TE"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall te(Object value) throws RestCallException {
return header("TE", value);
}
/**
* Sets the value for the <c>User-Agent</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"User-Agent"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall userAgent(Object value) throws RestCallException {
return header("User-Agent", value);
}
/**
* Sets the value for the <c>Upgrade</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"Upgrade"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall upgrade(Object value) throws RestCallException {
return header("Upgrade", value);
}
/**
* Sets the value for the <c>Via</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"Via"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall via(Object value) throws RestCallException {
return header("Via", value);
}
/**
* Sets the value for the <c>Warning</c> request header.
*
* <p>
* This is a shortcut for calling <code>header(<js>"Warning"</js>, value);</code>
*
* @param value The new header value.
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall warning(Object value) throws RestCallException {
return header("Warning", value);
}
/**
* Sets the client version by setting the value for the <js>"X-Client-Version"</js> header.
*
* @param version The version string (e.g. <js>"1.2.3"</js>)
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall clientVersion(String version) throws RestCallException {
return header("X-Client-Version", version);
}
/**
* Make this call retryable if an error response (>=400) is received.
*
* @param retries The number of retries to attempt.
* @param interval The time in milliseconds between attempts.
* @param retryOn
* Optional object used for determining whether a retry should be attempted.
* If <jk>null</jk>, uses {@link RetryOn#DEFAULT}.
* @return This object (for method chaining).
* @throws RestCallException If current entity is not repeatable.
*/
public RestCall retryable(int retries, long interval, RetryOn retryOn) throws RestCallException {
if (request instanceof HttpEntityEnclosingRequestBase) {
if (input != null && input instanceof HttpEntity) {
HttpEntity e = (HttpEntity)input;
if (e != null && ! e.isRepeatable())
throw new RestCallException("Attempt to make call retryable, but entity is not repeatable.");
}
}
this.retries = retries;
this.retryInterval = interval;
this.retryOn = (retryOn == null ? RetryOn.DEFAULT : retryOn);
return this;
}
/**
* For this call, allow automatic redirects when a 302 or 307 occurs when performing a POST.
*
* <p>
* Note that this can be inefficient since the POST body needs to be serialized twice.
* The preferred approach if possible is to use the {@link LaxRedirectStrategy} strategy on the underlying HTTP
* client.
* However, this method is provided if you don't have access to the underlying client.
*
* @param b Redirect flag.
* @return This object (for method chaining).
*/
public RestCall allowRedirectsOnPosts(boolean b) {
this.allowRedirectsOnPosts = b;
return this;
}
/**
* Specify the number of redirects to follow before throwing an exception.
*
* @param maxAttempts Allow a redirect to occur this number of times.
* @return This object (for method chaining).
*/
public RestCall redirectMaxAttempts(int maxAttempts) {
this.redirectOnPostsTries = maxAttempts;
return this;
}
/**
* Add an interceptor for this call only.
*
* @param interceptor The interceptor to add to this call.
* @return This object (for method chaining).
*/
public RestCall interceptor(RestCallInterceptor interceptor) {
interceptors.add(interceptor);
interceptor.onInit(this);
return this;
}
/**
* Pipes the request output to the specified writer when {@link #run()} is called.
*
* <p>
* The writer is not closed.
*
* <p>
* This method can be called multiple times to pipe to multiple writers.
*
* @param w The writer to pipe the output to.
* @return This object (for method chaining).
*/
public RestCall pipeTo(Writer w) {
return pipeTo(w, false);
}
/**
* Pipe output from response to the specified writer when {@link #run()} is called.
*
* <p>
* This method can be called multiple times to pipe to multiple writers.
*
* @param w The writer to write the output to.
* @param close Close the writer when {@link #close()} is called.
* @return This object (for method chaining).
*/
public RestCall pipeTo(Writer w, boolean close) {
return pipeTo(null, w, close);
}
/**
* Pipe output from response to the specified writer when {@link #run()} is called and associate that writer with an
* ID so it can be retrieved through {@link #getWriter(String)}.
*
* <p>
* This method can be called multiple times to pipe to multiple writers.
*
* @param id A string identifier that can be used to retrieve the writer using {@link #getWriter(String)}
* @param w The writer to write the output to.
* @param close Close the writer when {@link #close()} is called.
* @return This object (for method chaining).
*/
public RestCall pipeTo(String id, Writer w, boolean close) {
writers.add(id, w, close);
return this;
}
/**
* Retrieves a writer associated with an ID via {@link #pipeTo(String, Writer, boolean)}
*
* @param id A string identifier that can be used to retrieve the writer using {@link #getWriter(String)}
* @return The writer, or <jk>null</jk> if no writer is associated with that ID.
*/
public Writer getWriter(String id) {
return writers.getWriter(id);
}
/**
* When output is piped to writers, flush the writers after every line of output.
*
* @return This object (for method chaining).
*/
public RestCall byLines() {
this.byLines = true;
return this;
}
/**
* Pipes the request output to the specified output stream when {@link #run()} is called.
*
* <p>
* The output stream is not closed.
*
* <p>
* This method can be called multiple times to pipe to multiple output streams.
*
* @param os The output stream to pipe the output to.
* @return This object (for method chaining).
*/
public RestCall pipeTo(OutputStream os) {
return pipeTo(os, false);
}
/**
* Pipe output from response to the specified output stream when {@link #run()} is called.
*
* <p>
* This method can be called multiple times to pipe to multiple output stream.
*
* @param os The output stream to write the output to.
* @param close Close the output stream when {@link #close()} is called.
* @return This object (for method chaining).
*/
public RestCall pipeTo(OutputStream os, boolean close) {
return pipeTo(null, os, close);
}
/**
* Pipe output from response to the specified output stream when {@link #run()} is called and associate
* that output stream with an ID so it can be retrieved through {@link #getOutputStream(String)}.
*
* <p>
* This method can be called multiple times to pipe to multiple output stream.
*
* @param id A string identifier that can be used to retrieve the output stream using {@link #getOutputStream(String)}
* @param os The output stream to write the output to.
* @param close Close the output stream when {@link #close()} is called.
* @return This object (for method chaining).
*/
public RestCall pipeTo(String id, OutputStream os, boolean close) {
outputStreams.add(id, os, close);
return this;
}
/**
* Retrieves an output stream associated with an ID via {@link #pipeTo(String, OutputStream, boolean)}
*
* @param id A string identifier that can be used to retrieve the writer using {@link #getWriter(String)}
* @return The writer, or <jk>null</jk> if no writer is associated with that ID.
*/
public OutputStream getOutputStream(String id) {
return outputStreams.getOutputStream(id);
}
/**
* Prevent {@link RestCallException RestCallExceptions} from being thrown when HTTP status 400+ is encountered.
*
* @return This object (for method chaining).
*/
public RestCall ignoreErrors() {
this.ignoreErrors = true;
return this;
}
/**
* Stores the response text so that it can later be captured using {@link #getCapturedResponse()}.
*
* <p>
* This method should only be called once. Multiple calls to this method are ignored.
*
* @return This object (for method chaining).
*/
public RestCall captureResponse() {
if (capturedResponseWriter == null) {
capturedResponseWriter = new StringWriter();
writers.add(capturedResponseWriter, false);
}
return this;
}
/**
* Look for the specified regular expression pattern in the response output.
*
* <p>
* Causes a {@link RestCallException} to be thrown if the specified pattern is found in the output.
*
* <p>
* This method uses {@link #getCapturedResponse()} to read the response text and so does not affect the other output
* methods such as {@link #getResponseAsString()}.
*
* <h5 class='section'>Example:</h5>
* <p class='bcode w800'>
* <jc>// Throw a RestCallException if FAILURE or ERROR is found in the output.</jc>
* restClient.doGet(<jsf>URL</jsf>)
* .failurePattern(<js>"FAILURE|ERROR"</js>)
* .run();
* </p>
*
* @param errorPattern A regular expression to look for in the response output.
* @return This object (for method chaining).
*/
public RestCall failurePattern(final String errorPattern) {
responsePattern(
new ResponsePattern(errorPattern) {
@Override
public void onMatch(RestCall rc, Matcher m) throws RestCallException {
throw new RestCallException("Failure pattern detected.");
}
}
);
return this;
}
/**
* Look for the specified regular expression pattern in the response output.
*
* <p>
* Causes a {@link RestCallException} to be thrown if the specified pattern is not found in the output.
*
* <p>
* This method uses {@link #getCapturedResponse()} to read the response text and so does not affect the other output
* methods such as {@link #getResponseAsString()}.
*
* <h5 class='section'>Example:</h5>
* <p class='bcode w800'>
* <jc>// Throw a RestCallException if SUCCESS is not found in the output.</jc>
* restClient.doGet(<jsf>URL</jsf>)
* .successPattern(<js>"SUCCESS"</js>)
* .run();
* </p>
*
* @param successPattern A regular expression to look for in the response output.
* @return This object (for method chaining).
*/
public RestCall successPattern(String successPattern) {
responsePattern(
new ResponsePattern(successPattern) {
@Override
public void onNoMatch(RestCall rc) throws RestCallException {
throw new RestCallException("Success pattern not detected.");
}
}
);
return this;
}
/**
* Adds a response pattern finder to look for regular expression matches in the response output.
*
* <p>
* This method can be called multiple times to add multiple response pattern finders.
*
* <p>
* {@link ResponsePattern ResponsePatterns} use the {@link #getCapturedResponse()} to read the response text and so
* does not affect the other output methods such as {@link #getResponseAsString()}.
*
* @param responsePattern The response pattern finder.
* @return This object (for method chaining).
*/
public RestCall responsePattern(final ResponsePattern responsePattern) {
captureResponse();
interceptor(
new RestCallInterceptor() {
@Override
public void onClose(RestCall restCall) throws RestCallException {
responsePattern.match(RestCall.this);
}
}
);
return this;
}
/**
* Set configuration settings on this request.
*
* <p>
* Use {@link RequestConfig#custom()} to create configuration parameters for the request.
*
* @param config The new configuration settings for this request.
* @return This object (for method chaining).
*/
public RestCall setConfig(RequestConfig config) {
this.request.setConfig(config);
return this;
}
/**
* Method used to execute an HTTP response where you're only interested in the HTTP response code.
*
* <p>
* The response entity is discarded unless one of the pipe methods have been specified to pipe the output to an
* output stream or writer.
*
* <h5 class='section'>Example:</h5>
* <p class='bcode w800'>
* <jk>try</jk> {
* RestClient client = <jk>new</jk> RestClient();
* <jk>int</jk> rc = client.doGet(url).execute();
* <jc>// Succeeded!</jc>
* } <jk>catch</jk> (RestCallException e) {
* <jc>// Failed!</jc>
* }
* </p>
*
* @return The HTTP status code.
* @throws RestCallException If an exception or non-200 response code occurred during the connection attempt.
*/
public int run() throws RestCallException {
connect();
try {
StatusLine status = response.getStatusLine();
int sc = status.getStatusCode();
if (sc >= 400 && ! ignoreErrors)
throw new RestCallException(sc, status.getReasonPhrase(), request.getMethod(), request.getURI(), getResponseAsString()).setHttpResponse(response);
if (outputStreams.size() > 0 || writers.size() > 0)
getReader();
return sc;
} catch (RestCallException e) {
isFailed = true;
throw e;
} catch (IOException e) {
isFailed = true;
throw new RestCallException(e).setHttpResponse(response);
} finally {
close();
}
}
/**
* Same as {@link #run()} but allows you to run the call asynchronously.
*
* @return The HTTP status code.
* @throws RestCallException If the executor service was not defined.
* @see RestClientBuilder#executorService(ExecutorService, boolean) for defining the executor service for creating
* {@link Future Futures}.
*/
public Future<Integer> runFuture() throws RestCallException {
return client.getExecutorService(true).submit(
new Callable<Integer>() {
@Override /* Callable */
public Integer call() throws Exception {
return run();
}
}
);
}
/**
* Connects to the REST resource.
*
* <p>
* If this is a <c>PUT</c> or <c>POST</c>, also sends the input to the remote resource.<br>
*
* <p>
* Typically, you would only call this method if you're not interested in retrieving the body of the HTTP response.
* Otherwise, you're better off just calling one of the {@link #getReader()}/{@link #getResponse(Class)}/{@link #pipeTo(Writer)}
* methods directly which automatically call this method already.
*
* @return This object (for method chaining).
* @throws RestCallException If an exception or <c>400+</c> HTTP status code occurred during the connection attempt.
*/
public RestCall connect() throws RestCallException {
return connect(null);
}
private RestCall connect(ClassMeta<?> bodyType) throws RestCallException {
if (isConnected)
return this;
isConnected = true;
try {
request.setURI(uriBuilder.build());
if (hasInput || formData != null) {
if (hasInput && formData != null)
throw new RestCallException("Both input and form data found on same request.");
if (! (request instanceof HttpEntityEnclosingRequestBase))
throw new RestCallException(0, "Method does not support content entity.", request.getMethod(), request.getURI(), null);
HttpEntity entity = null;
if (formData != null)
entity = new UrlEncodedFormEntity(formData);
else if (input instanceof NameValuePairs)
entity = new UrlEncodedFormEntity((NameValuePairs)input);
else if (input instanceof HttpEntity)
entity = (HttpEntity)input;
else if (input instanceof Reader)
entity = new StringEntity(IOUtils.read((Reader)input), getRequestContentType(TEXT_PLAIN));
else if (input instanceof InputStream)
entity = new InputStreamEntity((InputStream)input, getRequestContentType(ContentType.APPLICATION_OCTET_STREAM));
else if (serializer != null)
entity = new RestRequestEntity(input, serializer, requestBodySchema);
else if (partSerializer != null)
entity = new StringEntity(partSerializer.serialize((HttpPartSchema)null, input), getRequestContentType(TEXT_PLAIN));
else
entity = new StringEntity(getBeanContext().getClassMetaForObject(input).toString(input), getRequestContentType(TEXT_PLAIN));
if (retries > 1 && ! entity.isRepeatable())
throw new RestCallException("Rest call set to retryable, but entity is not repeatable.");
((HttpEntityEnclosingRequestBase)request).setEntity(entity);
}
int sc = 0;
while (retries > 0) {
retries--;
Exception ex = null;
try {
response = client.execute(request);
sc = (response == null || response.getStatusLine() == null) ? -1 : response.getStatusLine().getStatusCode();
} catch (Exception e) {
ex = e;
sc = -1;
if (response != null)
EntityUtils.consumeQuietly(response.getEntity());
}
if (! retryOn.onResponse(response))
retries = 0;
if (retries > 0) {
for (RestCallInterceptor rci : interceptors)
rci.onRetry(this, sc, request, response, ex);
request.reset();
long w = retryInterval;
synchronized(this) {
wait(w);
}
} else if (ex != null) {
throw ex;
}
}
for (RestCallInterceptor rci : interceptors)
rci.onConnect(this, sc, request, response);
if (response == null)
throw new RestCallException("HttpClient returned a null response");
StatusLine sl = response.getStatusLine();
String method = request.getMethod();
sc = sl.getStatusCode(); // Read it again in case it was changed by one of the interceptors.
int[] expected = new int[0];
if (bodyType != null && bodyType.hasAnnotation(Response.class))
expected = bodyType.getAnnotation(Response.class).code();
if (sc >= 400 && ! ignoreErrors && ! ArrayUtils.contains(sc, expected)) {
throw new RestCallException(sc, sl.getReasonPhrase(), method, request.getURI(), getResponseAsString())
.setServerException(response.getFirstHeader("Exception-Name"), response.getFirstHeader("Exception-Message"), response.getFirstHeader("Exception-Trace"))
.setHttpResponse(response);
}
if ((sc == 307 || sc == 302) && allowRedirectsOnPosts && method.equalsIgnoreCase("POST") && ! ArrayUtils.contains(sc, expected)) {
if (redirectOnPostsTries-- < 1)
throw new RestCallException(sc, "Maximum number of redirects occurred. Location header: " + response.getFirstHeader("Location"), method, request.getURI(), getResponseAsString());
Header h = response.getFirstHeader("Location");
if (h != null) {
reset();
request.setURI(URI.create(h.getValue()));
retries++; // Redirects should affect retries.
connect();
}
}
} catch (RestCallException e) {
isFailed = true;
close();
throw e;
} catch (Exception e) {
isFailed = true;
close();
throw new RestCallException(e).setHttpResponse(response);
}
return this;
}
private ContentType getRequestContentType(ContentType def) {
Header h = request.getFirstHeader("Content-Type");
if (h != null) {
String s = h.getValue();
if (! isEmpty(s))
return ContentType.create(s);
}
return def;
}
private void reset() {
if (response != null)
EntityUtils.consumeQuietly(response.getEntity());
request.reset();
isConnected = false;
isClosed = false;
isFailed = false;
if (capturedResponseWriter != null)
capturedResponseWriter.getBuffer().setLength(0);
}
/**
* Connects to the remote resource (if <c>connect()</c> hasn't already been called) and returns the HTTP
* response message body as a reader.
*
* <p>
* If an {@link Encoder} has been registered with the {@link RestClient}, then the underlying input stream will be
* wrapped in the encoded stream (e.g. a <c>GZIPInputStream</c>).
*
* <p>
* If present, automatically handles the <c>charset</c> value in the <c>Content-Type</c> response header.
*
* <p>
* <b>IMPORTANT:</b> It is your responsibility to close this reader once you have finished with it.
*
* @return
* The HTTP response message body reader.
* <jk>null</jk> if response was successful but didn't contain a body (e.g. HTTP 204).
* @throws IOException If an exception occurred while streaming was already occurring.
*/
public Reader getReader() throws IOException {
InputStream is = getInputStream();
if (is == null)
return null;
// Figure out what the charset of the response is.
String cs = null;
Header contentType = response.getLastHeader("Content-Type");
String ct = contentType == null ? null : contentType.getValue();
// First look for "charset=" in Content-Type header of response.
if (ct != null && ct.contains("charset="))
cs = ct.substring(ct.indexOf("charset=")+8).trim();
if (cs == null)
cs = "UTF-8";
if (writers.size() > 0) {
try (Reader isr = new InputStreamReader(is, cs)) {
StringWriter sw = new StringWriter();
writers.add(sw, true);
IOPipe.create(isr, writers).byLines(byLines).run();
return new StringReader(sw.toString());
}
}
return new InputStreamReader(is, cs);
}
/**
* Returns the response text as a string if {@link #captureResponse()} was called on this object.
*
* <p>
* Note that while similar to {@link #getResponseAsString()}, this method can be called multiple times to retrieve
* the response text multiple times.
*
* <p>
* Note that this method returns <jk>null</jk> if you have not called one of the methods that cause the response to
* be processed. (e.g. {@link #run()}, {@link #getResponse()}, {@link #getResponseAsString()}.
*
* @return The captured response, or <jk>null</jk> if {@link #captureResponse()} has not been called.
* @throws IllegalStateException If trying to call this method before the response is consumed.
*/
public String getCapturedResponse() {
if (! isClosed)
throw new IllegalStateException("This method cannot be called until the response has been consumed.");
if (capturedResponse == null && capturedResponseWriter != null && capturedResponseWriter.getBuffer().length() > 0)
capturedResponse = capturedResponseWriter.toString();
return capturedResponse;
}
/**
* Returns the value of the <c>Content-Length</c> header.
*
* @return The value of the <c>Content-Length</c> header, or <c>-1</c> if header is not present.
* @throws IOException Thrown by underlying stream.
*/
public int getContentLength() throws IOException {
connect();
Header h = response.getLastHeader("Content-Length");
if (h == null)
return -1;
long l = Long.parseLong(h.getValue());
if (l > Integer.MAX_VALUE)
return Integer.MAX_VALUE;
return (int)l;
}
/**
* Connects to the remote resource (if <c>connect()</c> hasn't already been called) and returns the HTTP
* response message body as an input stream.
*
* <p>
* If an {@link Encoder} has been registered with the {@link RestClient}, then the underlying input stream will be
* wrapped in the encoded stream (e.g. a <c>GZIPInputStream</c>).
*
* <p>
* <b>IMPORTANT:</b> It is your responsibility to close this reader once you have finished with it.
*
* @return
* The HTTP response message body input stream. <jk>null</jk> if response was successful but didn't contain
* a body (e.g. HTTP 204).
* @throws IOException If an exception occurred while streaming was already occurring.
* @throws IllegalStateException If an attempt is made to read the response more than once.
*/
@SuppressWarnings("resource")
public InputStream getInputStream() throws IOException {
if (isClosed)
throw new IllegalStateException("Method cannot be called. Response has already been consumed.");
connect();
if (response == null)
throw new RestCallException("Response was null");
if (response.getEntity() == null) // HTTP 204 results in no content.
return null;
softClose();
InputStream is = new EofSensorInputStream(response.getEntity().getContent(), new EofSensorWatcher() {
@Override
public boolean eofDetected(InputStream wrapped) throws IOException {
RestCall.this.forceClose();
return true;
}
@Override
public boolean streamClosed(InputStream wrapped) throws IOException {
RestCall.this.forceClose();
return true;
}
@Override
public boolean streamAbort(InputStream wrapped) throws IOException {
RestCall.this.forceClose();
return true;
}
});
if (outputStreams.size() > 0) {
ByteArrayInOutStream baios = new ByteArrayInOutStream();
outputStreams.add(baios, true);
IOPipe.create(is, baios).run();
is.close();
return baios.getInputStream();
}
return is;
}
/**
* Connects to the remote resource (if {@code connect()} hasn't already been called) and returns the HTTP response
* message body as plain text.
*
* <p>
* The response entity is discarded unless one of the pipe methods have been specified to pipe the output to an
* output stream or writer.
*
* @return The response as a string.
* @throws RestCallException If an exception or non-200 response code occurred during the connection attempt.
* @throws IOException If an exception occurred while streaming was already occurring.
*/
public String getResponseAsString() throws IOException {
try (Reader r = getReader()) {
return read(r).toString();
} catch (IOException e) {
isFailed = true;
close();
throw e;
}
}
/**
* Connects to the remote resource (if {@code connect()} hasn't already been called) and returns the value of
* an HTTP header on the response.
*
* <p>
* Useful if you're only interested in a particular header value from the response and not the body of the response.
*
* <p>
* The response entity is discarded unless one of the pipe methods have been specified to pipe the output to an
* output stream or writer.
*
* @param name The header name.
* @return The response header as a string, or <jk>null</jk> if the header was not found.
* @throws RestCallException If an exception or non-200 response code occurred during the connection attempt.
* @throws IOException If an exception occurred while streaming was already occurring.
*/
public String getResponseHeader(String name) throws IOException {
try {
HttpResponse r = getResponse();
Header h = r.getFirstHeader(name);
return h == null ? null : h.getValue();
} catch (IOException e) {
isFailed = true;
close();
throw e;
}
}
/**
* Same as {@link #getResponseHeader(String)} except parses the header value using the specified part parser and schema.
*
* @param name The header name.
* @param partParser The part parser to use for parsing the header.
* @param schema The part schema. Can be <jk>null</jk>.
* @param c The type to convert the part into.
* @return The parsed part.
* @throws IOException Thrown by underlying stream.
* @throws ParseException Header value could not be parsed into the specified type.
*/
public <T> T getResponseHeader(HttpPartParser partParser, HttpPartSchema schema, String name, Class<T> c) throws IOException, ParseException {
return getResponseHeader(partParser, schema, name, (Type)c);
}
/**
* Same as {@link #getResponseHeader(String)} except parses the header value using the specified part parser and schema.
*
* @param name The header name.
* @param partParser The part parser to use for parsing the header.
* @param schema The part schema. Can be <jk>null</jk>.
* @param type The type to convert the part into.
* @param args The type arguments to convert the part into.
* @return The parsed part.
* @throws IOException Thrown by underlying stream.
* @throws ParseException Header value could not be parsed into the specified type.
*/
public <T> T getResponseHeader(HttpPartParser partParser, HttpPartSchema schema, String name, Type type, Type...args) throws IOException, ParseException {
try {
HttpResponse r = getResponse();
Header h = r.getFirstHeader(name);
if (h == null)
return null;
String hs = h.getValue();
if (partParser == null)
partParser = client.getPartParser();
return partParser.parse(schema, hs, type, args);
} catch (IOException e) {
isFailed = true;
close();
throw e;
}
}
/**
* Connects to the remote resource (if {@code connect()} hasn't already been called) and returns the HTTP response code.
*
* <p>
* Useful if you're only interested in the status code and not the body of the response.
*
* <p>
* The response entity is discarded unless one of the pipe methods have been specified to pipe the output to an
* output stream or writer.
*
* @return The response code.
* @throws RestCallException If an exception or non-200 response code occurred during the connection attempt.
* @throws IOException If an exception occurred while streaming was already occurring.
*/
public int getResponseCode() throws IOException {
return run();
}
/**
* Same as {@link #getResponse(Class)} but allows you to run the call asynchronously.
*
* @return The response as a string.
* @throws RestCallException If the executor service was not defined.
* @see
* RestClientBuilder#executorService(ExecutorService, boolean) for defining the executor service for creating
* {@link Future Futures}.
*/
public Future<String> getResponseAsStringFuture() throws RestCallException {
return client.getExecutorService(true).submit(
new Callable<String>() {
@Override /* Callable */
public String call() throws Exception {
return getResponseAsString();
}
}
);
}
/**
* Same as {@link #getResponse(Type, Type...)} except optimized for a non-parameterized class.
*
* <p>
* This is the preferred parse method for simple types since you don't need to cast the results.
*
* <h5 class='section'>Examples:</h5>
* <p class='bcode w800'>
* <jc>// Parse into a string.</jc>
* String s = restClient.doGet(url).getResponse(String.<jk>class</jk>);
*
* <jc>// Parse into a bean.</jc>
* MyBean b = restClient.doGet(url).getResponse(MyBean.<jk>class</jk>);
*
* <jc>// Parse into a bean array.</jc>
* MyBean[] ba = restClient.doGet(url).getResponse(MyBean[].<jk>class</jk>);
*
* <jc>// Parse into a linked-list of objects.</jc>
* List l = restClient.doGet(url).getResponse(LinkedList.<jk>class</jk>);
*
* <jc>// Parse into a map of object keys/values.</jc>
* Map m = restClient.doGet(url).getResponse(TreeMap.<jk>class</jk>);
* </p>
*
* <ul class='notes'>
* <li>
* You can also specify any of the following types:
* <ul>
* <li>{@link HttpResponse} - Returns the raw <c>HttpResponse</c> returned by the inner <c>HttpClient</c>.
* <li>{@link Reader} - Returns access to the raw reader of the response.
* <li>{@link InputStream} - Returns access to the raw input stream of the response.
* </ul>
* </ul>
*
* @param <T>
* The class type of the object being created.
* See {@link #getResponse(Type, Type...)} for details.
* @param type The object type to create.
* @return The parsed object.
* @throws ParseException
* If the input contains a syntax error or is malformed, or is not valid for the specified type.
* @throws IOException If a connection error occurred.
*/
public <T> T getResponse(Class<T> type) throws IOException, ParseException {
BeanContext bc = parser;
if (bc == null)
bc = BeanContext.DEFAULT;
return getResponseInner(bc.getClassMeta(type));
}
/**
* Same as {@link #getResponse(Class)} but allows you to run the call asynchronously.
*
* @param <T>
* The class type of the object being created.
* See {@link #getResponse(Type, Type...)} for details.
* @param type The object type to create.
* @return The parsed object.
* @throws RestCallException If the executor service was not defined.
* @see
* RestClientBuilder#executorService(ExecutorService, boolean) for defining the executor service for creating
* {@link Future Futures}.
*/
public <T> Future<T> getResponseFuture(final Class<T> type) throws RestCallException {
return client.getExecutorService(true).submit(
new Callable<T>() {
@Override /* Callable */
public T call() throws Exception {
return getResponse(type);
}
}
);
}
/**
* Parses HTTP body into the specified object type.
*
* <p>
* The type can be a simple type (e.g. beans, strings, numbers) or parameterized type (collections/maps).
*
* <h5 class='section'>Examples:</h5>
* <p class='bcode w800'>
* <jc>// Parse into a linked-list of strings.</jc>
* List l = restClient.doGet(url).getResponse(LinkedList.<jk>class</jk>, String.<jk>class</jk>);
*
* <jc>// Parse into a linked-list of beans.</jc>
* List l = restClient.doGet(url).getResponse(LinkedList.<jk>class</jk>, MyBean.<jk>class</jk>);
*
* <jc>// Parse into a linked-list of linked-lists of strings.</jc>
* List l = restClient.doGet(url).getResponse(LinkedList.<jk>class</jk>, LinkedList.<jk>class</jk>, String.<jk>class</jk>);
*
* <jc>// Parse into a map of string keys/values.</jc>
* Map m = restClient.doGet(url).getResponse(TreeMap.<jk>class</jk>, String.<jk>class</jk>, String.<jk>class</jk>);
*
* <jc>// Parse into a map containing string keys and values of lists containing beans.</jc>
* Map m = restClient.doGet(url).getResponse(TreeMap.<jk>class</jk>, String.<jk>class</jk>, List.<jk>class</jk>, MyBean.<jk>class</jk>);
* </p>
*
* <p>
* <c>Collection</c> classes are assumed to be followed by zero or one objects indicating the element type.
*
* <p>
* <c>Map</c> classes are assumed to be followed by zero or two meta objects indicating the key and value types.
*
* <p>
* The array can be arbitrarily long to indicate arbitrarily complex data structures.
*
* <ul class='notes'>
* <li>
* Use the {@link #getResponse(Class)} method instead if you don't need a parameterized map/collection.
* <li>
* You can also specify any of the following types:
* <ul>
* <li>{@link HttpResponse} - Returns the raw <c>HttpResponse</c> returned by the inner <c>HttpClient</c>.
* <li>{@link Reader} - Returns access to the raw reader of the response.
* <li>{@link InputStream} - Returns access to the raw input stream of the response.
* </ul>
* </ul>
*
* @param <T> The class type of the object to create.
* @param type
* The object type to create.
* <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
* @param args
* The type arguments of the class if it's a collection or map.
* <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
* <br>Ignored if the main type is not a map or collection.
* @return The parsed object.
* @throws ParseException
* If the input contains a syntax error or is malformed, or is not valid for the specified type.
* @throws IOException If a connection error occurred.
* @see BeanSession#getClassMeta(Class) for argument syntax for maps and collections.
*/
public <T> T getResponse(Type type, Type...args) throws IOException, ParseException {
BeanContext bc = parser;
if (bc == null)
bc = BeanContext.DEFAULT;
return (T)getResponseInner(bc.getClassMeta(type, args));
}
/**
* Same as {@link #getResponse(Type, Type...)} but allows you to specify a part parser to use for parsing the response.
*
* @param <T> The class type of the object to create.
* @param type
* The object type to create.
* <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
* @param args
* The type arguments of the class if it's a collection or map.
* <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
* <br>Ignored if the main type is not a map or collection.
* @return The parsed object.
* @throws ParseException
* If the input contains a syntax error or is malformed, or is not valid for the specified type.
* @throws IOException If a connection error occurred.
* @see BeanSession#getClassMeta(Class) for argument syntax for maps and collections.
*/
public <T> T getResponseBody(Type type, Type...args) throws IOException, ParseException {
BeanContext bc = parser;
if (bc == null)
bc = BeanContext.DEFAULT;
return (T)getResponseInner(bc.getClassMeta(type, args));
}
/**
* Same as {@link #getResponse(Class)} but allows you to run the call asynchronously.
*
* @param <T>
* The class type of the object being created.
* See {@link #getResponse(Type, Type...)} for details.
* @param type
* The object type to create.
* <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
* @param args
* The type arguments of the class if it's a collection or map.
* <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
* <br>Ignored if the main type is not a map or collection.
* @return The parsed object.
* @throws RestCallException If the executor service was not defined.
* @see
* RestClientBuilder#executorService(ExecutorService, boolean) for defining the executor service for creating
* {@link Future Futures}.
*/
public <T> Future<T> getResponseFuture(final Type type, final Type...args) throws RestCallException {
return client.getExecutorService(true).submit(
new Callable<T>() {
@Override /* Callable */
public T call() throws Exception {
return getResponse(type, args);
}
}
);
}
/**
* Parses the output from the connection into the specified type and then wraps that in a {@link PojoRest}.
*
* <p>
* Useful if you want to quickly retrieve a single value from inside of a larger JSON document.
*
* @param innerType The class type of the POJO being wrapped.
* @return The parsed output wrapped in a {@link PojoRest}.
* @throws IOException If a connection error occurred.
* @throws ParseException
* If the input contains a syntax error or is malformed for the <c>Content-Type</c> header.
*/
public PojoRest getResponsePojoRest(Class<?> innerType) throws IOException, ParseException {
return new PojoRest(getResponse(innerType));
}
/**
* Converts the output from the connection into an {@link ObjectMap} and then wraps that in a {@link PojoRest}.
*
* <p>
* Useful if you want to quickly retrieve a single value from inside of a larger JSON document.
*
* @return The parsed output wrapped in a {@link PojoRest}.
* @throws IOException If a connection error occurred.
* @throws ParseException
* If the input contains a syntax error or is malformed for the <c>Content-Type</c> header.
*/
public PojoRest getResponsePojoRest() throws IOException, ParseException {
return getResponsePojoRest(ObjectMap.class);
}
<T> T getResponseInner(ClassMeta<T> type) throws IOException, ParseException {
try {
if (response == null)
connect(type);
Class<?> ic = type.getInnerClass();
if (ic.equals(HttpResponse.class)) {
softClose();
return (T)response;
}
if (ic.equals(Reader.class))
return (T)getReader();
if (ic.equals(InputStream.class))
return (T)getInputStream();
if (type.isType(ReaderResource.class) || type.isType(StreamResource.class)) {
String mediaType = null;
ObjectMap headers = new ObjectMap();
for (Header h : response.getAllHeaders()) {
if (h.getName().equalsIgnoreCase("Content-Type"))
mediaType = h.getValue();
else
headers.put(h.getName(), h.getValue());
}
if (type.isType(ReaderResource.class))
return (T)ReaderResource.create().headers(headers).mediaType(mediaType).contents(getReader()).build();
return (T)StreamResource.create().headers(headers).mediaType(mediaType).contents(getInputStream()).build();
}
connect(type);
Header h = response.getFirstHeader("Content-Type");
int sc = response.getStatusLine().getStatusCode();
String ct = firstNonEmpty(h == null ? null : h.getValue(), "text/plain");
MediaType mt = MediaType.forString(ct);
if (parser == null || (mt.toString().equals("text/plain") && ! parser.canHandle(ct))) {
if (type.hasStringMutater())
return type.getStringMutater().mutate(getResponseAsString());
}
if (parser != null) {
try (Closeable in = parser.isReaderParser() ? getReader() : getInputStream()) {
// HttpClient automatically ignores the content body for certain HTTP status codes.
// So instantiate the object anyway if it has a no-arg constructor.
// This allows a remote resource method to return a NoContent object for example.
if (in == null && (sc < SC_OK || sc == SC_NO_CONTENT || sc == SC_NOT_MODIFIED || sc == SC_RESET_CONTENT)) {
ConstructorInfo c = type.getInfo().getPublicConstructor();
if (c != null) {
try {
return c.<T>invoke();
} catch (ExecutableException e) {
throw new ParseException(e);
}
}
}
ParserSessionArgs pArgs =
ParserSessionArgs
.create()
.properties(new ObjectMap().setInner(getProperties()))
.locale(response.getLocale())
.mediaType(mt)
.schema(responseBodySchema);
return parser.createSession(pArgs).parse(in, type);
}
}
if (type.hasReaderMutater())
return type.getReaderMutater().mutate(getReader());
if (type.hasInputStreamMutater())
return type.getInputStreamMutater().mutate(getInputStream());
throw new ParseException(
"Unsupported media-type in request header ''Content-Type'': ''{0}''\n\tSupported media-types: {1}",
getResponseHeader("Content-Type"), parser == null ? null : parser.getMediaTypes()
);
} catch (ParseException | IOException e) {
isFailed = true;
close();
throw e;
}
}
BeanContext getBeanContext() {
BeanContext bc = parser;
if (bc == null)
bc = BeanContext.DEFAULT;
return bc;
}
/**
* Converts the response from this call into a response bean.
*
* @param rbm The metadata used to construct the response bean.
* @return A new response bean.
*/
public <T> T getResponse(final ResponseBeanMeta rbm) {
try {
softClose();
Class<T> c = (Class<T>)rbm.getClassMeta().getInnerClass();
final RestClient rc = this.client;
final HttpPartParser p = ObjectUtils.firstNonNull(partParser, rc.getPartParser());
return (T)Proxy.newProxyInstance(
c.getClassLoader(),
new Class[] { c },
new InvocationHandler() {
@Override /* InvocationHandler */
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
ResponseBeanPropertyMeta pm = rbm.getProperty(method.getName());
if (pm != null) {
HttpPartParser pp = pm.getParser(p);
HttpPartSchema schema = pm.getSchema();
String name = pm.getPartName();
ClassMeta<?> type = rc.getClassMeta(method.getGenericReturnType());
HttpPartType pt = pm.getPartType();
if (pt == RESPONSE_BODY) {
responseBodySchema(schema);
return getResponseBody(type);
}
if (pt == RESPONSE_HEADER)
return getResponseHeader(pp, schema, name, type);
if (pt == RESPONSE_STATUS)
return getResponseCode();
}
return null;
}
});
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Returns access to the {@link HttpUriRequest} passed to {@link HttpClient#execute(HttpUriRequest)}.
*
* @return The {@link HttpUriRequest} object.
*/
public HttpUriRequest getRequest() {
return request;
}
/**
* Returns access to the {@link HttpResponse} returned by {@link HttpClient#execute(HttpUriRequest)}.
*
* <p>
* Returns <jk>null</jk> if {@link #connect()} has not yet been called.
*
* @return The HTTP response object.
* @throws IOException Thrown by underlying stream.
*/
public HttpResponse getResponse() throws IOException {
connect();
return response;
}
/**
* Shortcut for calling <c>getRequest().setHeader(header)</c>
*
* @param header The header to set on the request.
* @return This object (for method chaining).
*/
public RestCall header(Header header) {
request.setHeader(header);
return this;
}
/**
* Cleans up this HTTP call.
*
* @throws RestCallException Can be thrown by one of the {@link RestCallInterceptor#onClose(RestCall)} calls.
*/
@Override /* Closeable */
public void close() throws RestCallException {
if (response != null && ! softClose)
EntityUtils.consumeQuietly(response.getEntity());
if (! softClose)
isClosed = true;
if (! isFailed)
for (RestCallInterceptor r : interceptors)
r.onClose(this);
}
void forceClose() throws RestCallException {
softClose = false;
close();
}
/**
* Adds a {@link RestCallLogger} to the list of interceptors on this class.
*
* @param level The log level to log events at.
* @param log The logger.
* @return This object (for method chaining).
*/
public RestCall logTo(Level level, Logger log) {
interceptor(new RestCallLogger(level, log));
return this;
}
/**
* Sets <c>Debug: value</c> header on this request.
*
* @return This object (for method chaining).
* @throws RestCallException Invalid input.
*/
public RestCall debug() throws RestCallException {
header("Debug", true);
return this;
}
/**
* If called, the underlying response stream will not be closed when you call {@link #close()}.
*
* <p>
* This is useful in cases where you want access to that stream after you've already cleaned up this object.
* However, it is your responsibility to close that stream yourself.
*
* @return This object (for method chaining).
*/
public RestCall softClose() {
this.softClose = true;
return this;
}
//-----------------------------------------------------------------------------------------------------------------
// Utility methods
//-----------------------------------------------------------------------------------------------------------------
/**
* Specifies that the following value can be added as an HTTP part.
*/
private boolean canAdd(Object value, HttpPartSchema schema, boolean skipIfEmpty) {
if (value != null) {
if (ObjectUtils.isEmpty(value) && skipIfEmpty)
return false;
return true;
}
if (schema == null)
return false;
if (schema.isRequired())
return true;
String def = schema.getDefault();
if (def == null)
return false;
if (StringUtils.isEmpty(def) && skipIfEmpty)
return false;
return true;
}
private static String className(Object o) {
return ClassInfo.of(o).getFullName();
}
//-----------------------------------------------------------------------------------------------------------------
// Other methods
//-----------------------------------------------------------------------------------------------------------------
@Override /* Session */
public ObjectMap toMap() {
return super.toMap()
.append("RestCall", new DefaultFilteringObjectMap()
.append("allowRedirectsOnPosts", allowRedirectsOnPosts)
.append("byLines", byLines)
.append("capturedResponse", capturedResponse)
.append("client", client)
.append("hasInput", hasInput)
.append("ignoreErrors", ignoreErrors)
.append("interceptors", interceptors)
.append("isClosed", isClosed)
.append("isConnected", isConnected)
.append("isFailed", isFailed)
.append("parser", parser)
.append("partParser", partParser)
.append("partSerializer", partSerializer)
.append("redirectOnPostsTries", redirectOnPostsTries)
.append("requestBodySchema", requestBodySchema)
.append("response", response)
.append("responseBodySchema", responseBodySchema)
.append("retries", retries)
.append("retryInterval", retryInterval)
.append("retryOn", retryOn)
.append("serializer", serializer)
.append("softClose", softClose)
.append("uriBuilder", uriBuilder)
);
}
}