![JAR search and dependency download from the Maven repository](/logo.png)
org.apache.juneau.rest.client.RestResponse Maven / Gradle / Ivy
// ***************************************************************************************************************************
// * 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 org.apache.juneau.parser.*;
import org.apache.juneau.rest.client.assertion.*;
import static org.apache.juneau.httppart.HttpPartType.*;
import java.lang.reflect.*;
import java.text.*;
import java.util.*;
import java.util.logging.*;
import org.apache.http.*;
import org.apache.http.message.*;
import org.apache.http.params.*;
import org.apache.http.util.*;
import org.apache.juneau.*;
import org.apache.juneau.assertions.*;
import org.apache.juneau.common.internal.*;
import org.apache.juneau.http.header.*;
import org.apache.juneau.httppart.*;
import org.apache.juneau.httppart.bean.*;
import org.apache.juneau.internal.*;
/**
* Represents a response from a remote REST resource.
*
*
* Instances of this class are created by calling the {@link RestRequest#run()} method.
*
*
See Also:
*/
@FluentSetters
public class RestResponse implements HttpResponse {
private final RestClient client;
private final RestRequest request;
private final HttpResponse response;
private final Parser parser;
private ResponseContent responseContent;
private boolean isClosed;
private HeaderList headers;
private Map partParserSessions = new IdentityHashMap<>();
private HttpPartParserSession partParserSession;
/**
* Constructor.
*
* @param client The RestClient that created this response.
* @param request The REST request.
* @param response The HTTP response. Can be null .
* @param parser The overridden parser passed into {@link RestRequest#parser(Parser)}.
*/
protected RestResponse(RestClient client, RestRequest request, HttpResponse response, Parser parser) {
this.client = client;
this.request = request;
this.parser = parser;
this.response = response == null ? new BasicHttpResponse(null, 0, null) : response;
this.responseContent = new ResponseContent(client, request, this, parser);
this.headers = HeaderList.of(this.response.getAllHeaders());
}
/**
* Returns the request object that created this response object.
*
* @return The request object that created this response object.
*/
public RestRequest getRequest() {
return request;
}
//------------------------------------------------------------------------------------------------------------------
// Setters
//------------------------------------------------------------------------------------------------------------------
/**
* Consumes the response body.
*
*
* This is equivalent to closing the input stream.
*
* @return This object.
* @throws RestCallException If one of the {@link RestCallInterceptor RestCallInterceptors} threw an exception.
*/
@FluentSetter
public RestResponse consume() throws RestCallException {
close();
return this;
}
//------------------------------------------------------------------------------------------------------------------
// Status line
//------------------------------------------------------------------------------------------------------------------
/**
* Returns the status code of the response.
*
* Shortcut for calling getStatusLine().getStatusCode()
.
*
* @return The status code of the response.
*/
public int getStatusCode() {
return getStatusLine().getStatusCode();
}
/**
* Returns the status line reason phrase of the response.
*
* Shortcut for calling getStatusLine().getReasonPhrase()
.
*
* @return The status line reason phrase of the response.
*/
public String getReasonPhrase() {
return getStatusLine().getReasonPhrase();
}
//------------------------------------------------------------------------------------------------------------------
// Status line assertions
//------------------------------------------------------------------------------------------------------------------
/**
* Provides the ability to perform fluent-style assertions on the response {@link StatusLine} object.
*
*
Examples:
*
* MyBean bean = client
* .get(URI )
* .run()
* .assertStatus().asCode().is(200)
* .getContent().as(MyBean.class );
*
*
* @return A new fluent assertion object.
*/
public FluentResponseStatusLineAssertion assertStatus() {
return new FluentResponseStatusLineAssertion<>(getStatusLine(), this);
}
/**
* Provides the ability to perform fluent-style assertions on the response status code.
*
* Examples:
*
* MyBean bean = client
* .get(URI )
* .run()
* .assertStatus(200)
* .getContent().as(MyBean.class );
*
*
* @param value The value to assert.
* @return A new fluent assertion object.
*/
public RestResponse assertStatus(int value) {
assertStatus().asCode().is(value);
return this;
}
//------------------------------------------------------------------------------------------------------------------
// Headers
//------------------------------------------------------------------------------------------------------------------
/**
* Shortcut for calling getHeader(name).asString()
.
*
* @param name The header name.
* @return The header value, never null
*/
public Optional getStringHeader(String name) {
return getHeader(name).asString();
}
/**
* Shortcut for retrieving the response charset from the Content-Type header.
*
* @return The response charset.
* @throws RestCallException If REST call failed.
*/
public String getCharacterEncoding() throws RestCallException {
Optional ct = getContentType();
String s = null;
if (ct.isPresent())
s = getContentType().get().getParameter("charset");
return StringUtils.isEmpty(s) ? "utf-8" : s;
}
/**
* Shortcut for retrieving the response content type from the Content-Type header.
*
*
* This is equivalent to calling getHeader("Content-Type" ).as(ContentType.class ) .
*
* @return The response charset.
* @throws RestCallException If REST call failed.
*/
public Optional getContentType() throws RestCallException {
return getHeader("Content-Type").as(ContentType.class);
}
/**
* Provides the ability to perform fluent-style assertions on the response character encoding.
*
* Examples:
*
* // Validates that the response content charset is UTF-8.
* client
* .get(URI )
* .run()
* .assertCharset().is("utf-8" );
*
*
* @return A new fluent assertion object.
* @throws RestCallException If REST call failed.
*/
public FluentStringAssertion assertCharset() throws RestCallException {
return new FluentStringAssertion<>(getCharacterEncoding(), this);
}
/**
* Provides the ability to perform fluent-style assertions on a response header.
*
* Examples:
*
* // Validates the content type header is provided.
* client
* .get(URI )
* .run()
* .assertHeader("Content-Type" ).exists();
*
* // Validates the content type is JSON.
* client
* .get(URI )
* .run()
* .assertHeader("Content-Type" ).is("application/json" );
*
* // Validates the content type is JSON using test predicate.
* client
* .get(URI )
* .run()
* .assertHeader("Content-Type" ).is(x -> x .equals("application/json" ));
*
* // Validates the content type is JSON by just checking for substring.
* client
* .get(URI )
* .run()
* .assertHeader("Content-Type" ).contains("json" );
*
* // Validates the content type is JSON using regular expression.
* client
* .get(URI )
* .run()
* .assertHeader("Content-Type" ).isPattern(".*json.*" );
*
* // Validates the content type is JSON using case-insensitive regular expression.
* client
* .get(URI )
* .run()
* .assertHeader("Content-Type" ).isPattern(".*json.*" , CASE_INSENSITIVE );
*
*
*
* The assertion test returns the original response object allowing you to chain multiple requests like so:
*
* // Validates the header and converts it to a bean.
* MediaType mediaType = client
* .get(URI )
* .run()
* .assertHeader("Content-Type" ).isNotEmpty()
* .assertHeader("Content-Type" ).isPattern(".*json.*" )
* .getHeader("Content-Type" ).as(MediaType.class );
*
*
* @param name The header name.
* @return A new fluent assertion object.
*/
public FluentResponseHeaderAssertion assertHeader(String name) {
return new FluentResponseHeaderAssertion<>(getHeader(name), this);
}
//------------------------------------------------------------------------------------------------------------------
// Body
//------------------------------------------------------------------------------------------------------------------
/**
* Returns the body of the response.
*
* This method can be called multiple times returning the same response body each time.
*
* @return The body of the response.
*/
public ResponseContent getContent() {
return responseContent;
}
/**
* Provides the ability to perform fluent-style assertions on this response body.
*
* Examples:
*
* // Validates the response body equals the text "OK".
* client
* .get(URI )
* .run()
* .assertContent().is("OK" );
*
* // Validates the response body contains the text "OK".
* client
* .get(URI )
* .run()
* .assertContent().isContains("OK" );
*
* // Validates the response body passes a predicate test.
* client
* .get(URI )
* .run()
* .assertContent().is(x -> x .contains("OK" ));
*
* // Validates the response body matches a regular expression.
* client
* .get(URI )
* .run()
* .assertContent().isPattern(".*OK.*" );
*
* // Validates the response body matches a regular expression using regex flags.
* client
* .get(URI )
* .run()
* .assertContent().isPattern(".*OK.*" , MULTILINE & CASE_INSENSITIVE );
*
* // Validates the response body matches a regular expression in the form of an existing Pattern.
* Pattern pattern = Pattern.compile (".*OK.*" );
* client
* .get(URI )
* .run()
* .assertContent().isPattern(pattern );
*
*
*
* The assertion test returns the original response object allowing you to chain multiple requests like so:
*
* // Validates the response body matches a regular expression.
* MyBean bean = client
* .get(URI )
* .run()
* .assertContent().isPattern(".*OK.*" );
* .assertContent().isNotPattern(".*ERROR.*" )
* .getContent().as(MyBean.class );
*
*
* Notes:
* -
* If no charset was found on the
Content-Type
response header, "UTF-8" is assumed.
* -
* When using this method, the body is automatically cached by calling the {@link ResponseContent#cache()}.
*
-
* The input stream is automatically closed after this call.
*
*
* @return A new fluent assertion object.
*/
public FluentResponseBodyAssertion assertContent() {
return new FluentResponseBodyAssertion<>(responseContent, this);
}
/**
* Provides the ability to perform fluent-style assertions on this response body.
*
*
* A shortcut for calling assertContent().is(value ) .
*
*
Examples:
*
* // Validates the response body equals the text "OK".
* client
* .get(URI )
* .run()
* .assertContent("OK" );
*
*
* @param value The value to assert.
* @return This object.
*/
public RestResponse assertContent(String value) {
assertContent().is(value);
return this;
}
/**
* Provides the ability to perform fluent-style assertions on this response body.
*
*
* A shortcut for calling assertContent().asString().isMatches(value ) .
*
* @see FluentStringAssertion#isMatches(String)
* @param value The value to assert.
* @return This object.
*/
public RestResponse assertContentMatches(String value) {
assertContent().asString().isMatches(value);
return this;
}
/**
* Caches the response body so that it can be read as a stream multiple times.
*
* This is equivalent to calling the following:
*
* getContent().cache();
*
*
* @return The body of the response.
*/
@FluentSetter
public RestResponse cacheContent() {
responseContent.cache();
return this;
}
@SuppressWarnings("unchecked")
T as(ResponseBeanMeta rbm) {
Class c = (Class)rbm.getClassMeta().getInnerClass();
final RestClient rc = this.client;
return (T)Proxy.newProxyInstance(
c.getClassLoader(),
new Class[] { c },
(InvocationHandler) (proxy, method, args) -> {
ResponseBeanPropertyMeta pm = rbm.getProperty(method.getName());
HttpPartParserSession pp = getPartParserSession(pm.getParser().orElse(rc.getPartParser()));
HttpPartSchema schema = pm.getSchema();
HttpPartType pt = pm.getPartType();
String name = pm.getPartName().orElse(null);
ClassMeta> type = rc.getBeanContext().getClassMeta(method.getGenericReturnType());
if (pt == RESPONSE_HEADER)
return getHeader(name).parser(pp).schema(schema).as(type).orElse(null);
if (pt == RESPONSE_STATUS)
return getStatusCode();
return getContent().schema(schema).as(type);
});
}
/**
* Logs a message.
*
* @param level The log level.
* @param t The throwable cause.
* @param msg The message with {@link MessageFormat}-style arguments.
* @param args The arguments.
* @return This object.
*/
public RestResponse log(Level level, Throwable t, String msg, Object...args) {
client.log(level, t, msg, args);
return this;
}
/**
* Logs a message.
*
* @param level The log level.
* @param msg The message with {@link MessageFormat}-style arguments.
* @param args The arguments.
* @return This object.
*/
public RestResponse log(Level level, String msg, Object...args) {
client.log(level, msg, args);
return this;
}
// -----------------------------------------------------------------------------------------------------------------
// HttpResponse pass-through methods.
// -----------------------------------------------------------------------------------------------------------------
/**
* Obtains the status line of this response.
*
* The status line can be set using one of the setStatusLine methods, or it can be initialized in a constructor.
*
* @return The status line, or null if not yet set.
*/
@Override /* HttpResponse */
public ResponseStatusLine getStatusLine() {
return new ResponseStatusLine(this, response.getStatusLine());
}
/**
* Sets the status line of this response.
*
* @param statusline The status line of this response
*/
@Override /* HttpResponse */
public void setStatusLine(StatusLine statusline) {
response.setStatusLine(statusline);
}
/**
* Sets the status line of this response.
*
*
* The reason phrase will be determined based on the current locale.
*
* @param ver The HTTP version.
* @param code The status code.
*/
@Override /* HttpResponse */
public void setStatusLine(ProtocolVersion ver, int code) {
response.setStatusLine(ver, code);
}
/**
* Sets the status line of this response with a reason phrase.
*
* @param ver The HTTP version.
* @param code The status code.
* @param reason The reason phrase, or null to omit.
*/
@Override /* HttpResponse */
public void setStatusLine(ProtocolVersion ver, int code, String reason) {
response.setStatusLine(ver, code, reason);
}
/**
* Updates the status line of this response with a new status code.
*
* @param code The HTTP status code.
* @throws IllegalStateException If the status line has not be set.
*/
@Override /* HttpResponse */
public void setStatusCode(int code) {
response.setStatusCode(code);
}
/**
* Updates the status line of this response with a new reason phrase.
*
* @param reason The new reason phrase as a single-line string, or null to unset the reason phrase.
* @throws IllegalStateException If the status line has not be set.
*/
@Override /* HttpResponse */
public void setReasonPhrase(String reason) {
response.setReasonPhrase(reason);
}
/**
* Obtains the message entity of this response.
*
*
* The entity is provided by calling setEntity.
*
*
Notes:
* - Unlike the {@link HttpResponse#getEntity()} method, this method never returns a
null response.
* Instead, getContent().isPresent() can be used to determine whether the response has a body.
*
*
* @return The response entity. Never null .
*/
@Override /* HttpResponse */
public ResponseContent getEntity() {
return responseContent;
}
/**
* Associates a response entity with this response.
*
* Notes:
* - If an entity has already been set for this response and it depends on an input stream
* ({@link HttpEntity#isStreaming()} returns
true ), it must be fully consumed in order to ensure
* release of resources.
*
*
* @param entity The entity to associate with this response, or null to unset.
*/
@Override /* HttpResponse */
public void setEntity(HttpEntity entity) {
response.setEntity(entity);
this.responseContent = new ResponseContent(client, request, this, parser);
}
/**
* Obtains the locale of this response.
*
* The locale is used to determine the reason phrase for the status code.
* It can be changed using {@link #setLocale(Locale)}.
*
* @return The locale of this response, never null .
*/
@Override /* HttpResponse */
public Locale getLocale() {
return response.getLocale();
}
/**
* Changes the locale of this response.
*
* @param loc The new locale.
*/
@Override /* HttpResponse */
public void setLocale(Locale loc) {
response.setLocale(loc);
}
/**
* Returns the protocol version this message is compatible with.
*
* @return The protocol version this message is compatible with.
*/
@Override /* HttpMessage */
public ProtocolVersion getProtocolVersion() {
return response.getProtocolVersion();
}
/**
* Checks if a certain header is present in this message.
*
*
* Header values are ignored.
*
* @param name The header name to check for.
* @return true if at least one header with this name is present.
*/
@Override /* HttpMessage */
public boolean containsHeader(String name) {
return response.containsHeader(name);
}
/**
* Returns all the headers with a specified name of this message.
*
* Header values are ignored.
*
Headers are ordered in the sequence they were sent over a connection.
*
* @param name The name of the headers to return.
* @return All the headers with a specified name of this message.
*/
@Override /* HttpMessage */
public ResponseHeader[] getHeaders(String name) {
return headers.stream(name).map(x -> new ResponseHeader(name, request, this, x).parser(getPartParserSession())).toArray(ResponseHeader[]::new);
}
/**
* Returns the first header with a specified name of this message.
*
*
* If there is more than one matching header in the message the first element of {@link #getHeaders(String)} is returned.
*
* This method always returns a value so that you can perform assertions on the result.
*
* @param name The name of the header to return.
* @return The header, never null .
*/
@Override /* HttpMessage */
public ResponseHeader getFirstHeader(String name) {
return new ResponseHeader(name, request, this, headers.getFirst(name).orElse(null)).parser(getPartParserSession());
}
/**
* Returns the last header with a specified name of this message.
*
*
* If there is more than one matching header in the message the last element of {@link #getHeaders(String)} is returned.
*
* This method always returns a value so that you can perform assertions on the result.
*
* @param name The name of the header to return.
* @return The header, never null .
*/
@Override /* HttpMessage */
public ResponseHeader getLastHeader(String name) {
return new ResponseHeader(name, request, this, headers.getLast(name).orElse(null)).parser(getPartParserSession());
}
/**
* Returns the response header with the specified name.
*
*
* If more that one header with the given name exists the values will be combined with ", " as per RFC 2616 Section 4.2.
*
* @param name The name of the header to return.
* @return The header, never null .
*/
public ResponseHeader getHeader(String name) {
return new ResponseHeader(name, request, this, headers.get(name).orElse(null)).parser(getPartParserSession());
}
/**
* Returns all the headers of this message.
*
* Headers are ordered in the sequence they were sent over a connection.
*
* @return All the headers of this message.
*/
@Override /* HttpMessage */
public ResponseHeader[] getAllHeaders() {
return headers.stream().map(x -> new ResponseHeader(x.getName(), request, this, x).parser(getPartParserSession())).toArray(ResponseHeader[]::new);
}
/**
* Adds a header to this message.
*
* The header will be appended to the end of the list.
*
* @param header The header to append.
*/
@Override /* HttpMessage */
public void addHeader(Header header) {
headers.append(header);
}
/**
* Adds a header to this message.
*
* The header will be appended to the end of the list.
*
* @param name The name of the header.
* @param value The value of the header.
*/
@Override /* HttpMessage */
public void addHeader(String name, String value) {
headers.append(name, value);
}
/**
* Overwrites the first header with the same name.
*
* The new header will be appended to the end of the list, if no header with the given name can be found.
*
* @param header The header to set.
*/
@Override /* HttpMessage */
public void setHeader(Header header) {
headers.set(header);
}
/**
* Overwrites the first header with the same name.
*
* The new header will be appended to the end of the list, if no header with the given name can be found.
*
* @param name The name of the header.
* @param value The value of the header.
*/
@Override /* HttpMessage */
public void setHeader(String name, String value) {
headers.set(name, value);
}
/**
* Overwrites all the headers in the message.
*
* @param headers The array of headers to set.
*/
@Override /* HttpMessage */
public void setHeaders(Header[] headers) {
this.headers = HeaderList.of(headers);
}
/**
* Removes a header from this message.
*
* @param header The header to remove.
*/
@Override /* HttpMessage */
public void removeHeader(Header header) {
headers.remove(header);
}
/**
* Removes all headers with a certain name from this message.
*
* @param name The name of the headers to remove.
*/
@Override /* HttpMessage */
public void removeHeaders(String name) {
headers.remove(name);
}
/**
* Returns an iterator of all the headers.
*
* @return {@link Iterator} that returns {@link Header} objects in the sequence they are sent over a connection.
*/
@Override /* HttpMessage */
public HeaderIterator headerIterator() {
return headers.headerIterator();
}
/**
* Returns an iterator of the headers with a given name.
*
* @param name The name of the headers over which to iterate, or null for all headers.
* @return {@link Iterator} that returns {@link Header} objects with the argument name in the sequence they are sent over a connection.
*/
@Override /* HttpMessage */
public HeaderIterator headerIterator(String name) {
return headers.headerIterator(name);
}
/**
* Returns the parameters effective for this message as set by {@link #setParams(HttpParams)}.
*
* @return The parameters effective for this message as set by {@link #setParams(HttpParams)}.
* @deprecated Use configuration classes provided org.apache.http.config and org.apache.http.client.config .
*/
@Override /* HttpMessage */
@Deprecated
public HttpParams getParams() {
return response.getParams();
}
/**
* Provides parameters to be used for the processing of this message.
*
* @param params The parameters.
* @deprecated Use configuration classes provided org.apache.http.config and org.apache.http.client.config .
*/
@Override /* HttpMessage */
@Deprecated
public void setParams(HttpParams params) {
response.setParams(params);
}
void close() throws RestCallException {
if (isClosed)
return;
isClosed = true;
EntityUtils.consumeQuietly(response.getEntity());
if (!request.isLoggingSuppressed() && (request.isDebug() || client.logRequestsPredicate.test(request, this))) {
if (client.logRequests == DetailLevel.SIMPLE) {
client.log(client.logRequestsLevel, "HTTP {0} {1}, {2}", request.getMethod(), request.getURI(), this.getStatusLine());
} else if (request.isDebug() || client.logRequests == DetailLevel.FULL) {
String output = getContent().asString();
StringBuilder sb = new StringBuilder();
sb.append("\n=== HTTP Call (outgoing) ======================================================");
sb.append("\n=== REQUEST ===\n");
sb.append(request.getMethod()).append(" ").append(request.getURI());
sb.append("\n---request headers---");
request.getHeaders().forEach(x -> sb.append("\n\t").append(x));
if (request.hasHttpEntity()) {
sb.append("\n---request entity---");
HttpEntity e = request.getHttpEntity();
if (e.getContentType() != null)
sb.append("\n\t").append(e.getContentType());
if (e.isRepeatable()) {
try {
sb.append("\n---request content---\n").append(EntityUtils.toString(e));
} catch (Exception ex) {
sb.append("\n---request content exception---\n").append(ex.getMessage());
}
}
}
sb.append("\n=== RESPONSE ===\n").append(getStatusLine());
sb.append("\n---response headers---");
for (Header h : getAllHeaders())
sb.append("\n\t").append(h);
sb.append("\n---response content---\n").append(output);
sb.append("\n=== END =======================================================================");
client.log(client.logRequestsLevel, sb.toString());
}
}
for (RestCallInterceptor r : request.interceptors) {
try {
r.onClose(request, this);
} catch (RuntimeException | RestCallException e) {
throw e;
} catch (Exception e) {
throw new RestCallException(this, e, "Interceptor throw exception on close");
}
}
client.onCallClose(request, this);
}
//------------------------------------------------------------------------------------------------------------------
// Other methods
//------------------------------------------------------------------------------------------------------------------
/**
* Creates a session of the specified part parser.
*
* @param parser The parser to create a session for.
* @return A session of the specified parser.
*/
protected HttpPartParserSession getPartParserSession(HttpPartParser parser) {
HttpPartParserSession s = partParserSessions.get(parser);
if (s == null) {
s = parser.getPartSession();
partParserSessions.put(parser, s);
}
return s;
}
/**
* Creates a session of the client-default parat parser.
*
* @return A session of the specified parser.
*/
protected HttpPartParserSession getPartParserSession() {
if (partParserSession == null)
partParserSession = client.getPartParser().getPartSession();
return partParserSession;
}
HttpResponse asHttpResponse() {
return response;
}
//-----------------------------------------------------------------------------------------------------------------
// Fluent setters
//-----------------------------------------------------------------------------------------------------------------
//
//
}