Request.java
package com.goebl.david;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Builder for an HTTP request.
* <br>
* You can some "real-life" usage examples at
* <a href="https://github.com/hgoebl/DavidWebb">github.com/hgoebl/DavidWebb</a>.
* <br>
*
* @author hgoebl
*/
public class Request {
public enum Method {
GET, POST, PUT, DELETE
}
private final Webb webb;
final Method method;
final String uri;
Map<String, Object> params;
boolean multipleValues;
Map<String, Object> headers;
Object payload;
boolean streamPayload;
boolean useCaches;
Integer connectTimeout;
Integer readTimeout;
Long ifModifiedSince;
Boolean followRedirects;
boolean ensureSuccess;
boolean compress;
int retryCount;
boolean waitExponential;
Request(Webb webb, Method method, String uri) {
this.webb = webb;
this.method = method;
this.uri = uri;
this.followRedirects = webb.followRedirects;
}
/**
* Turn on a mode where one parameter key can have multiple values.
* <br>
* Example: <code>order.php?fruit=orange&fruit=apple&fruit=banana</code>
* <br>
* This is only necessary when you want to call {@link #param(String, Object)} multiple
* times with the same parameter name and this should lead to having multiple values.
* If you call {@link #param(String, Iterable)} or already provide an Array as value parameter,
* you don't have to call this method and it should work as expected.
*
* @return <code>this</code> for method chaining (fluent API)
* @since 1.3.0
*/
public Request multipleValues() {
multipleValues = true;
return this;
}
/**
* Set (or overwrite) a parameter.
* <br>
* The parameter will be used to create a query string for GET-requests and as the body for POST-requests
* with MIME-type <code>application/x-www-form-urlencoded</code>.
* <br>
* Please see {@link #multipleValues()} if you have to deal with parameters carrying multiple values.
* <br>
* Handling of multi-valued parameters exists since version 1.3.0
*
* @param name the name of the parameter (it's better to use only contain ASCII characters)
* @param value the value of the parameter; <code>null</code> will be converted to empty string,
* Arrays of Objects are expanded to multiple valued parameters, for all other
* objects to <code>toString()</code> method converts it to String
* @return <code>this</code> for method chaining (fluent API)
*/
public Request param(String name, Object value) {
if (params == null) {
params = new LinkedHashMap<String, Object>();
}
if (multipleValues) {
Object currentValue = params.get(name);
if (currentValue != null) {
if (currentValue instanceof Collection) {
Collection<Object> values = (Collection) currentValue;
values.add(value);
} else {
// upgrade single value to set of values
Collection<Object> values = new ArrayList<Object>();
values.add(currentValue);
values.add(value);
params.put(name, values);
}
return this;
}
}
params.put(name, value);
return this;
}
/**
* Set (or overwrite) a parameter with multiple values.
* <br>
* The parameter will be used to create a query string for GET-requests and as the body for POST-requests
* with MIME-type <code>application/x-www-form-urlencoded</code>.
* <br>
* If you use this method, you don't have to call {@link #multipleValues()}, but you should not mix
* using {@link #param(String, Object)} and this method for the same parameter name as this might cause
* unexpected behaviour or exceptions.
*
* @param name the name of the parameter (it's better to use only contain ASCII characters)
* @param values the values of the parameter; will be expanded to multiple valued parameters.
* @return <code>this</code> for method chaining (fluent API)
* @since 1.3.0
*/
public Request param(String name, Iterable<Object> values) {
if (params == null) {
params = new LinkedHashMap<String, Object>();
}
params.put(name, values);
return this;
}
/**
* Set (or overwrite) many parameters via a map.
* <br>
* @param valueByName a Map of name-value pairs,<br>
* the name of the parameter (it's better to use only contain ASCII characters)<br>
* the value of the parameter; <code>null</code> will be converted to empty string, for all other
* objects to <code>toString()</code> method converts it to String
* @return <code>this</code> for method chaining (fluent API)
*/
public Request params(Map<String, Object> valueByName) {
if (params == null) {
params = new LinkedHashMap<String, Object>();
}
params.putAll(valueByName);
return this;
}
/**
* Get the URI of this request.
*
* @return URI
*/
public String getUri() {
return uri;
}
/**
* Set (or overwrite) a HTTP header value.
* <br>
* Setting a header this way has the highest precedence and overrides a header value set on a {@link Webb}
* instance ({@link Webb#setDefaultHeader(String, Object)}) or a global header
* ({@link Webb#setGlobalHeader(String, Object)}).
* <br>
* Using <code>null</code> or empty String is not allowed for name and value.
*
* @param name name of the header (HTTP-headers are not case-sensitive, but if you want to override your own
* headers, you have to use identical strings for the name. There are some frequently used header
* names as constants in {@link Webb}, see HDR_xxx.
* @param value the value for the header. Following types are supported, all other types use <code>toString</code>
* of the given object:
* <ul>
* <li>{@link java.util.Date} is converted to RFC1123 compliant String</li>
* <li>{@link java.util.Calendar} is converted to RFC1123 compliant String</li>
* </ul>
* @return <code>this</code> for method chaining (fluent API)
*/
public Request header(String name, Object value) {
if (headers == null) {
headers = new LinkedHashMap<String, Object>();
}
headers.put(name, value);
return this;
}
/**
* Set the payload for the request.
* <br>
* Using this method together with {@link #param(String, Object)} has the effect of <code>body</code> being
* ignored without notice. The method can be called more than once: the value will be stored and converted
* to bytes later.
* <br>
* Following types are supported for the body:
* <ul>
* <li>
* <code>null</code> clears the body
* </li>
* <li>
* {@link org.json.JSONObject}, HTTP header 'Content-Type' will be set to JSON, if not set
* </li>
* <li>
* {@link org.json.JSONArray}, HTTP header 'Content-Type' will be set to JSON, if not set
* </li>
* <li>
* {@link java.lang.String}, HTTP header 'Content-Type' will be set to TEXT, if not set;
* Text will be converted to UTF-8 bytes.
* </li>
* <li>
* <code>byte[]</code> the easiest way for DavidWebb - it's just passed through.
* HTTP header 'Content-Type' will be set to BINARY, if not set.
* </li>
* <li>
* {@link java.io.File}, HTTP header 'Content-Type' will be set to BINARY, if not set;
* The file gets streamed to the web-server and 'Content-Length' will be set to the number
* of bytes of the file. There is absolutely no conversion done. So if you want to upload
* e.g. a text-file and convert it to another encoding than stored on disk, you have to do
* it by yourself.
* </li>
* <li>
* {@link java.io.InputStream}, HTTP header 'Content-Type' will be set to BINARY, if not set;
* Similar to <code>File</code>. Content-Length cannot be set (which has some drawbacks compared
* to knowing the size of the body in advance).<br>
* <strong>You have to care for closing the stream!</strong>
* </li>
* </ul>
*
* @param body the payload
* @return <code>this</code> for method chaining (fluent API)
*/
public Request body(Object body) {
if (method == Method.GET || method == Method.DELETE) {
throw new IllegalStateException("body not allowed for request method " + method);
}
this.payload = body;
this.streamPayload = body instanceof File || body instanceof InputStream;
return this;
}
/**
* Enable compression for uploaded data.<br>
* <br>
* Before you enable compression, you should find out, whether the web server you are talking to
* supports this. As compression has not to be implemented for HTTP and standard RFC2616 had only
* compression for downloaded resources in mind, in special cases it makes absolutely sense to
* compress the posted data.<br>
* Your web application should inspect the 'Content-Encoding' header and implement the compression
* token provided by this client. By now only 'gzip' encoding token is used. If you need 'deflate'
* create an issue.
*
* @return <code>this</code> for method chaining (fluent API)
*/
public Request compress() {
compress = true;
return this;
}
/**
* See <a href="http://docs.oracle.com/javase/7/docs/api/java/net/URLConnection.html#useCaches">
* URLConnection.useCaches</a>
* <br>
* If you don't want your requests delivered from a cache, you don't have to call this method,
* because <code>false</code> is the default.
*
* @param useCaches If <code>true</code>, the protocol is allowed to use caching whenever it can.
* @return <code>this</code> for method chaining (fluent API)
*/
public Request useCaches(boolean useCaches) {
this.useCaches = useCaches;
return this;
}
/**
* See <a href="http://docs.oracle.com/javase/7/docs/api/java/net/URLConnection.html#setIfModifiedSince(long)">
* URLConnection.setIfModifiedSince()</a>
* @param ifModifiedSince A nonzero value gives a time as the number of milliseconds since January 1, 1970, GMT.
* The object is fetched only if it has been modified more recently than that time.
* @return <code>this</code> for method chaining (fluent API)
*/
public Request ifModifiedSince(long ifModifiedSince) {
this.ifModifiedSince = ifModifiedSince;
return this;
}
/**
* See <a href="http://docs.oracle.com/javase/7/docs/api/java/net/URLConnection.html#setConnectTimeout(int)">
* URLConnection.setConnectTimeout</a>
* @param connectTimeout sets a specified timeout value, in milliseconds. <code>0</code> means infinite timeout.
* @return <code>this</code> for method chaining (fluent API)
*/
public Request connectTimeout(int connectTimeout) {
this.connectTimeout = connectTimeout;
return this;
}
/**
* See <a href="http://docs.oracle.com/javase/7/docs/api/java/net/URLConnection.html#setReadTimeout(int)">
* </a>
* @param readTimeout Sets the read timeout to a specified timeout, in milliseconds.
* <code>0</code> means infinite timeout.
* @return <code>this</code> for method chaining (fluent API)
*/
public Request readTimeout(int readTimeout) {
this.readTimeout = readTimeout;
return this;
}
/**
* See <a href="http://docs.oracle.com/javase/7/docs/api/java/net/HttpURLConnection.html#setInstanceFollowRedirects(boolean)">
* </a>.
* <br>
* Use this method to set the behaviour for this single request when receiving redirect responses.
* If you want to change the behaviour for all your requests, call {@link Webb#setFollowRedirects(boolean)}.
* @param auto <code>true</code> to automatically follow redirects (HTTP status code 3xx).
* Default value comes from HttpURLConnection and should be <code>true</code>.
* @return <code>this</code> for method chaining (fluent API)
*/
public Request followRedirects(boolean auto) {
this.followRedirects = auto;
return this;
}
/**
* By calling this method, the HTTP status code is checked and a <code>WebbException</code> is thrown if
* the status code is not something like 2xx.<br>
* <br>
* Be careful! If you request resources e.g. with {@link #ifModifiedSince(long)}, an exception will also be
* thrown in the positive case of <code>304 Not Modified</code>.
*
* @return <code>this</code> for method chaining (fluent API)
*/
public Request ensureSuccess() {
this.ensureSuccess = true;
return this;
}
/**
* Set the number of retries after the first request failed.
* <br>
* When `waitExponential` is set, then there will be {@link Thread#sleep(long)} between
* the retries. If the thread is interrupted, there will be an `InterruptedException`
* in the thrown `WebbException`. You can check this with {@link WebbException#getCause()}.
* The `interrupted` flag will be set to true in this case.
*
* @param retryCount This parameter holds the number of retries that will be made AFTER the
* initial send in the event of a error. If an error occurs on the last
* attempt an exception will be raised.<br>
* Values > 10 are ignored (we're not gatling)
* @param waitExponential sleep during retry attempts (exponential backoff).
* For retry-counts more than 3, <tt>true</tt> is mandatory.
* @return <code>this</code> for method chaining (fluent API)
*/
public Request retry(int retryCount, boolean waitExponential) {
if (retryCount < 0) {
retryCount = 0;
}
if (retryCount > 10) {
retryCount = 10;
}
if (retryCount > 3 && !waitExponential) {
throw new IllegalArgumentException("retries > 3 only valid with wait");
}
this.retryCount = retryCount;
this.waitExponential = waitExponential;
return this;
}
/**
* Execute the request and expect the result to be convertible to <code>String</code>.
* @return the created <code>Response</code> object carrying the payload from the server as <code>String</code>
*/
public Response<String> asString() {
return webb.execute(this, String.class);
}
/**
* Execute the request and expect the result to be convertible to <code>JSONObject</code>.
* @return the created <code>Response</code> object carrying the payload from the server as <code>JSONObject</code>
*/
public Response<JSONObject> asJsonObject() {
return webb.execute(this, JSONObject.class);
}
/**
* Execute the request and expect the result to be convertible to <code>JSONArray</code>.
* @return the created <code>Response</code> object carrying the payload from the server as <code>JSONArray</code>
*/
public Response<JSONArray> asJsonArray() {
return webb.execute(this, JSONArray.class);
}
/**
* Execute the request and expect the result to be convertible to <code>byte[]</code>.
* @return the created <code>Response</code> object carrying the payload from the server as <code>byte[]</code>
*/
public Response<byte[]> asBytes() {
return (Response<byte[]>) webb.execute(this, Const.BYTE_ARRAY_CLASS);
}
/**
* Execute the request and expect the result to be convertible to <code>InputStream</code>.
* @return the created <code>Response</code> object carrying the payload from the server as <code>InputStream</code>
*/
public Response<InputStream> asStream() {
return webb.execute(this, InputStream.class);
}
/**
* Execute the request and expect no result payload (only status-code and headers).
* @return the created <code>Response</code> object where no payload is expected or simply will be ignored.
*/
public Response<Void> asVoid() {
return webb.execute(this, Void.class);
}
}