Java tutorial
/* Copyright (C) 2013-2014 Ian Teune <ian.teune@gmail.com> * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package com.tinspx.util.net; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.base.Optional; import static com.google.common.base.Preconditions.*; import com.google.common.base.Strings; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.Iterables; import com.google.common.collect.ListMultimap; import com.google.common.collect.Multimaps; import com.google.common.io.ByteSource; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.tinspx.util.base.BasicError; import com.tinspx.util.base.Cancellable; import com.tinspx.util.base.Errors; import com.tinspx.util.collect.CollectUtils; import com.tinspx.util.io.ChannelSource; import com.tinspx.util.io.ExtendedCharSequence; import com.tinspx.util.io.StringUtils; import com.tinspx.util.io.charset.DefaultHeaderCharDet; import com.tinspx.util.io.charset.HeaderCharDet; import java.io.IOException; import java.net.CookieHandler; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; import lombok.Getter; import lombok.NonNull; import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; /** * {@code Response} <i>describes</i> the result of accessing a resource using a * {@link Request}. A {@link RequestContext} is responsible executing the * {@code Request} and generating the {@code Response}. * <p> * A {@code Response} can only be created from a {@code Request} using one of * the {@link Request#response(URI, int, String) response(...)} methods. A * {@code Response} is always linked to the {@code Request} from which it was * created. This request can be accessed through {@link #request()}. * {@link #id()}, {@link #tag()}, {@link #properties()}, {@link #errors()}, and * the {@link #isCancelled() cancel} state are all shared with this * {@code Request}. * * @author Ian */ @Accessors(fluent = true) @Slf4j @NotThreadSafe public class Response implements Cancellable, BasicError.Listener { @Getter final Request request; /** * The URI of this Response. Typically will be the same as the URI of the * Request (but could be different if a {@link RequestContext} hides * redirects). */ @Getter final URI uri; /** * The response code of this Response. Only valid on protocols that have a * response code. Typically will be -1 if an error occurs or the protocol * does not have a response code. */ @Getter final int code; /** * The response message of the response. Only valid on protocols that have a * response message. Typically will be the empty String if the protocol does * not have a response message and <i>may</i> have an error message if an * error occurred. */ @Getter @Nonnull final String message; /** * The headers of this {@code Response}. */ @Nullable Headers headers; @Nullable Supplier<? extends ExtendedCharSequence> asCharSequence; @Nullable Supplier<? extends ChannelSource> asByteSource; @Nullable Supplier<? extends Object> asClass; /** * the {@code Charset} used to decode the response, null if not known */ @Nullable Charset decodeCharset; /** * {@code ImmutableList} of redirects that have led up to this * {@code Response}. May be empty if there were no redirects (never null) or * if the {@link RequestContext} hides redirects. */ @Nonnull @Getter final ImmutableList<Response> redirects; /** * The time at which this {@code Response} was constructed obtained from * {@link System#currentTimeMillis()}. This can be used to determine how * old the response is (for cache control for example). */ @Getter final long startMillis = System.currentTimeMillis(); /** * The total time in milliseconds that this {@code Response} took to * complete. This value is set in {@link #onContentComplete()} and is * therefore not set until this response has completed. Retrieving this * value before the response is complete will return 0. */ @Getter long elapsedMillis; /** * Returns the query parameters from the query segment of {@link #uri()} * as a {@code ListMultimap}. */ @Getter final ListMultimap<String, String> queryParams; /** * Creates a new {@code Response}. * * @param request the Request that initiated this response * @param uri the URI of this Response * @param queryParams the query parameters of {@code uri} if already * computed; if null, the params will be extracted from uri. * @param code the response code, if available * @param message the response message, if available. null will be * converted to the empty String. * @param redirects the list of redirects that have led up to this Response. * if null, {@link #redirects()} will return an empty List. A defensive copy * of this List is created, so the caller may continue to use and edit this * List after the constructor is called. */ Response(Request request, URI uri, @Nullable ListMultimap<String, String> queryParams, int code, @Nullable String message, @Nullable Iterable<Response> redirects) { this.request = checkNotNull(request); this.uri = checkNotNull(uri); if (queryParams != null) { this.queryParams = ImmutableListMultimap.copyOf(queryParams); } else { this.queryParams = Multimaps.unmodifiableListMultimap(UriUtil.parseQuery(uri)); } this.code = code; this.message = Strings.nullToEmpty(message); this.redirects = redirects != null ? ImmutableList.copyOf(redirects) : ImmutableList.<Response>of(); checkArgument(CollectUtils.allNonNull(this.redirects)); } /** * Returns the {@link Request#properties() properties} {@code Map} shared * with the {@link #request() request}. */ public Map<Object, Object> properties() { return request.properties(); } /** * Returns the tag associated with the Request of this Response. * * @return non null tag from the Request */ public String tag() { return request.tag(); } /** * Returns the {@code id} of {@link #request()}. * * @see Request#id() */ public long id() { return request.id(); } @Override public boolean cancel() { return request.cancel(); } @Override public boolean isCancelled() { return request.isCancelled(); } /** * Determine if this response or request has encountered any errors. */ public boolean hasErrors() { return request.hasErrors(); } /** * Returns all errors encountered by the request or response. */ public List<BasicError> errors() { return request.errors(); } @Override public void onError(BasicError error) { if (error == null) { error = Errors.create(this); } request.onError(error); } /** * Returns the headers of this {@code Response}. */ public Headers headers() { if (headers == null) { headers = new Headers(); } return headers; } public String header(@Nullable String name) { return headers != null ? headers.last(name) : ""; } public List<String> headers(@Nullable String name) { return headers != null ? headers.get(name) : Collections.<String>emptyList(); } public boolean hasHeader(@Nullable String name) { return headers != null ? headers.contains(name) : false; } /** * Convenience method equivalent to {@code properties().put(key, value)}. */ public Response put(Object key, Object value) { properties().put(key, value); return this; } /** * Convenience method equivalent to {@code properties().get(key)}. */ public Object get(Object key) { return properties().get(key); } /** * Determines if any headers have been added to this {@code Response} * through {@link #headers()}. */ public boolean hasHeaders() { return headers != null && !headers.isEmpty(); } /** * Returns the {@code Charset} <i>actually</i> used to decode the response, * which may differ from {@link Request#decodeCharset()} if a different * {@code Charset} was detected. May be absent if no decoding occurred. * The agent that decoded the response must set the {@code Charset} using * {@link #decodeCharset()}. */ public Optional<Charset> decodeCharset() { return Optional.fromNullable(decodeCharset); } /** * Sets the {@code Charset} that should be used or was used to decode the * {@code Response}. If the response has not been decoded, this * {@code Charset} should be used to decode the response. If the * response has already been decoded {@link #hasCharSequence()} is true}, * this indicates the {@code Charset} actually used to decode the response. * * @param decodeCharset the {@code Charset} to use in decoding * @return this Response instance */ public Response decodeCharset(@NonNull Charset decodeCharset) { this.decodeCharset = decodeCharset; return this; } /** * Determines if a decode {@code Charset} set with * {@link #decodeCharset(Charset)}. If false, then {@link #decodeCharset()} * will be absent. */ public boolean hasDecodeCharset() { return decodeCharset != null; } /** * Sends the {@link RequestCallback#onContentStart(Response) onContentStart} * event to all callbacks registered in {@link #request()}. * * @param isLastResponse true if there are no additional redirects that * will occur after this response. false if there is a pending redirect * and this response will not be the last. * @return true if this Response is canceled */ public boolean onContentStart(boolean isLastResponse) { if (request.hasCallbacks()) { for (RequestCallback rc : request.callbacks()) { if (isLastResponse || !rc.ignoreRedirects()) { rc.onContentStart(this); } } } return request.isCancelled(); } /** * Sends the * {@link RequestCallback#onContent(Response, ByteBuffer) onContent} event * to all callbacks registered in {@link #request()}. {@code content} * position and limit will not be unaltered when this method completes. * * @param isLastResponse true if there are no additional redirects that * will occur after this response. false if there is a pending redirect * and this response will not be the last. * @return true if this Response is canceled */ public boolean onContent(@NonNull ByteBuffer content, boolean isLastResponse) { if (request.hasCallbacks()) { final int pos = content.position(), limit = content.limit(); for (RequestCallback rc : request.callbacks()) { if (isLastResponse || !rc.ignoreRedirects()) { rc.onContent(this, content); content.limit(limit).position(pos); } } } return request.isCancelled(); } private void setElapsedMillis() { if (elapsedMillis == 0) { elapsedMillis = System.currentTimeMillis() - startMillis; } } /** * Sends the * {@link RequestCallback#onContentComplete(Response) onContentComplete} event * to all callbacks registered in {@link #request()}. Also sets the * {@link #elapsedMillis() elapsed milliseconds}. * * @param isLastResponse true if there are no additional redirects that * will occur after this response. false if there is a pending redirect * and this response will not be the last. * @return true if this Response is canceled */ public boolean onContentComplete(boolean isLastResponse) { setElapsedMillis(); if (request.hasCallbacks()) { for (RequestCallback rc : request.callbacks()) { if (isLastResponse || !rc.ignoreRedirects()) { rc.onContentComplete(this); } } } return request.isCancelled(); } /** * Sends the * {@link RequestCallback#onFailure(Response, Throwable) onFailure} event to * all callbacks registered in {@link #request()} * * @param propagate true if any uncaught exception thrown by a callback * should be propagated */ public void onFailure(Throwable exception, boolean propagate) { setElapsedMillis(); if (request.hasCallbacks()) { Throwable error = null; for (RequestCallback rc : request.callbacks()) { try { rc.onFailure(this, exception); } catch (Throwable t) { log.error("onFailure: {}", this, t); error = t; } } if (error != null && propagate) { throw Throwables.propagate(error); } } } /** * Sends the {@link RequestCallback#onSuccess(Response) onSuccess} event to * all callbacks registered in {@link #request()}. * * @param propagate true if any uncaught exception thrown by a callback * should be propagated */ public void onSuccess(boolean propagate) { setElapsedMillis(); if (request.hasCallbacks()) { Throwable error = null; for (RequestCallback rc : request.callbacks()) { try { rc.onSuccess(this); } catch (Throwable t) { log.error("onSuccess: {}", this, t); error = t; } } if (error != null && propagate) { throw Throwables.propagate(error); } } } /** * Sends the {@link RequestCallback#onCancel(Response) onCancel} * event to all callbacks registered in {@link #request()}. * * @param propagate true if any uncaught exception thrown by a callback * should be propagated */ public void onCancel(boolean propagate) { setElapsedMillis(); if (request.hasCallbacks()) { Throwable error = null; for (RequestCallback rc : request.callbacks()) { try { rc.onCancel(this); } catch (Throwable t) { log.error("onCancel: {}", this, t); error = t; } } if (error != null && propagate) { throw Throwables.propagate(error); } } } /** * Sets the decoded content that will be made available in * {@link #asCharSequence()}. */ public Response asCharSequence(@NonNull CharSequence response) { return asCharSequence(Suppliers.ofInstance(response)); } /** * Sets the decoded content that will be made available in * {@link #asCharSequence()}. */ public Response asCharSequence(@NonNull Supplier<? extends CharSequence> response) { this.asCharSequence = Suppliers.compose(StringUtils.asExtendedCharSequence(), response); return this; } /** * Sets the response content that will be made available in * {@link #asByteBuffer()}. */ public Response asByteSource(@NonNull ByteSource response) { return asByteSource(Suppliers.ofInstance(ChannelSource.of(response))); } /** * Sets the response content that will be made available in * {@link #asByteBuffer()}. */ public Response asByteSource(@NonNull Supplier<? extends ChannelSource> response) { this.asByteSource = response; return this; } /** * Sets the response converted to an instance of {@link Request#asClass()} * that will be made available in {@link #asClass(Class)}. */ public Response asClass(@NonNull Supplier<? extends Object> response) { this.asClass = response; return this; } /** * Same as {@link #asCharSequence() asCharSequence().toString()}. */ public String asString() { return asCharSequence().toString(); } /** * Same as {@link #asCharSequence() asCharSequence().toCharBuffer()}. */ public CharBuffer asCharBuffer() { return asCharSequence().toCharBuffer(); } /** * Returns the decoded content of this {@code Response} as an * {@code ExtendedCharSequence}. Ensure that the {@link #request() request} * was configured to produce a {@code CharSequence} through * {@link Request#asCharSequence(boolean)} and that * {@link #hasCharSequence()} is true before invoking this method. * * @return the decoded {@code Response} content as a CharSequence * @throws IllegalStateException if {@link #hasCharSequence()} is false */ public ExtendedCharSequence asCharSequence() { if (asCharSequence == null) { throw new IllegalStateException(); } return asCharSequence.get(); } /** * Returns the content of this {@code Response} as an * {@link ByteSource}. Ensure that the {@link #request() request} * was configured to produce a {@code ByteBuffer} through * {@link Request#asByteSource(boolean)} and that * {@link #hasByteSource()} is true before invoking this method. * * @return the {@code Response} content as a {@code ChannelSource} * @throws IllegalStateException if {@link #hasByteSource()} is false */ public ChannelSource asByteSource() { if (asByteSource == null) { throw new IllegalStateException(); } return asByteSource.get(); } /** * Returns the content of this {@code Response} as an instance of * {@code cls}. Ensure that the {@link #request() request} was configured to * produce an instance of {@code cls} through {@link Request#asClass(Class)} * and that {@link #hasClass(Class) hasClass(cls)} is true before invoking * this method. * * @return the {@code Response} content as an instance of {@code cls} * @throws IllegalStateException if {@link #hasClass(Class) hasClass(cls)} * is false * @throws ClassCastException if the response is not an instance of * {@code T} */ public <T> T asClass(@NonNull Class<T> cls) { if (asClass == null) { throw new IllegalStateException(); } return cls.cast(asClass.get()); } public boolean hasClass(@NonNull Class<?> cls) { return asClass != null && cls.isInstance(asClass.get()); } public boolean hasByteSource() { return asByteSource != null; } public boolean hasCharSequence() { return asCharSequence != null; } /** * Determines if this {@code Response} was caused by a redirect. If true, * {@link #redirects()} will be non-empty. Note that if the * {@link RequestContext} hides redirects, this method may be inaccurate. If * the {@code RequestContext} hides redirects, the only way to detect * redirects is to compare the response {@link #uri() URI} to the request * {@link Request#uri() URI}. */ public boolean isRedirected() { return !redirects.isEmpty(); } /** * Releases all of the {@code Response} content. This clears all of content * that would be available from {@link #asCharSequence()}, * {@link #asByteSource()}, or {@link #asClass(Class)} so that it be * reclaimed by the gc. This allows the memory of a large response to be * freed while keeping the {@code Response} meta-data such as the list of * redirects and headers. * <p> * This releases <i>all</i> redirects available from {@link #redirects()} as * well. * <p> * The {@link #properties() properties} of this Response are <i>not</i> * cleared. Make sure to remove any properties with large memory footprints * a well. The callbacks registered with the {@code Request} of this * {@code Response} are <i>not</i> cleared either. These callbacks may be be * keeping references to large amounts of memory; therefore, consider * clearing the callbacks as well. * * @return this Response */ public Response release() { asCharSequence = null; asByteSource = null; asClass = null; if (!redirects.isEmpty()) { redirects.get(redirects.size() - 1).release(); } return this; } /** * Returns the original {@code Request} used to initiate this * {@code Response}. This will differ from {@link #request()} if there are * any redirects. If there are redirects, the {@code Request} from the first * redirect is returned. * * @return the original {@code Request} that initiated this {@code Response} * (ie the Request from the first redirect) */ public Request originalRequest() { return redirects.isEmpty() ? request : redirects.get(0).request(); } /** * true if the Content-Type header contains the String "xml" or if * {@link Request#isXml()} is true. * * @return true if the response content is xml */ public boolean isXml() { if (headers().contentType().toLowerCase(Locale.US).contains("xml")) { return true; } return request.isXml(); } /** * true if the Content-Type header contains the String "html" or if * {@link Request#isHtml()} is true. * * @return true if the response content is html */ public boolean isHtml() { if (headers().contentType().toLowerCase(Locale.US).contains("html")) { return true; } return request.isHtml(); } /** * true if the Content-Type header contains the String "text", or if * {@link #isHtml()} or {@link #isXml()} is true. * * @return if the response content is text */ public boolean isText() { if (headers().contentType().toLowerCase(Locale.US).contains("text")) { return true; } return isHtml() || isXml(); } /** * Determines if this response requires a redirect. Calls * {@link RedirectHandler#shouldRedirect(Response)} on the * {@code RedirectHandler} of the {@link #request() request}. * * @return if this Response requires a redirect */ public boolean shouldRedirect() { return request.redirectHandler().shouldRedirect(this); } /** * Returns a new {@code Request} identical to {@link #request()} that can be * used as a redirect. {@link Request#isRedirect()} will be true and * {@link Request#cause()} will be set to this {@code Response} for the * returned {@code Request}. The returned {@code Request} shares the * {@link #properties() properties}, {@link Request#callbacks() callbacks}, * and {@link #isCancelled() cancel} state of this {@code Response}. * <p> * The returned {@code Request} is initially identical to * {@link #request()}. Typically, changes should be made to the returned * {@code Request}, such as setting the {@link Request#uri(URI) URI}, before * submitting the redirect. This is usually done by using the * {@link Request#redirectHandler() RedirectHandler} of {@link #request()}. * <p> * Note that this method <i>always</i> creates the redirect request. * {@link #shouldRedirect()} should be called beforehand to check if a * redirect is necessary in first place. The {@link RedirectHandler} of * {@link #request()} must be applied to the returned {@link Request} as * well (which could also cancel the redirect). * * @return a new redirect {@code Request} initially identical to * {@link #request()} */ public Request redirect() throws IOException { return request.redirect(this); } /** * Sets the CookieHandler of the Request to the CookieHandler of this * Response. */ private Request setupRequest(Request request) { if (this.request.hasCookieHandler()) { request.cookieHandler(this.request.cookieHandler); } return request; } /** * Creates a new {@code Request} with the specified {@code URI} that shares * the same {@link CookieHandler} as this {@code Response}. * * @param uri the URI of the new {@code Request} * @return the new {@code Request} */ public Request newRequest(URI uri) { return setupRequest(new Request(uri)); } /** * Creates a new {@code Request} with the specified {@code URI} that shares * the same {@link CookieHandler} as this {@code Response}. * * @param uri the URI of the new {@code Request} * @return the new {@code Request} */ public Request newRequest(String uri) throws URISyntaxException { return setupRequest(new Request(uri)); } /** * Creates a new {@code Request} that shares the same {@code URI} and * {@link CookieHandler} as this {@code Response}. * * @return the new {@code Request} */ public Request newRequest() { return setupRequest(new Request(uri())); } /** * Returns a basic string representation of this {@code Response} that * includes the response {@link #code() code}, {@link #message() message}, * and {@link #uri() uri}. For more detailed information, use * {@link #toDetailString()}. */ @Override public String toString() { return String.format("[%d] [Response] %s%s %s %d %s", id(), Strings.isNullOrEmpty(tag()) ? "" : "[" + tag() + "] ", request.method(), uri(), code(), message()); } /** * Returns a String containing <i>all</i> information of this * {@link Response}. This String will be very long and span multiple lines. */ public String toDetailString() { StringBuilder s = new StringBuilder(1024); s.append(toString()); s.append("\nrequest: ").append(request); s.append("\nstart: ").append(startMillis); s.append(" elapsed: ").append(elapsedMillis); s.append("\ndecodeCharset: ").append(decodeCharset); s.append("\nCharSequence: ").append(hasCharSequence()); s.append(" ByteBuffer: ").append(hasByteSource()); s.append(" Class: ").append(hasClass(Object.class)); s.append(" cancel: ").append(isCancelled()); s.append("\ncookieHandler: ").append(request.cookieHandler); s.append("\nproperties: ").append(properties()); s.append("\nheaders: "); if (hasHeaders()) { for (Map.Entry<String, String> e : headers.asMultimap().entries()) { s.append("\n ").append(e.getKey()).append(": ").append(e.getValue()); } } else { s.append("none"); } s.append("\nredirects: "); if (isRedirected()) { for (Response r : redirects) { s.append("\n ").append(r); } } else { s.append("none"); } s.append("\nerrors: "); if (hasErrors()) { for (BasicError t : errors()) { s.append("\n ").append(t); } } else { s.append("none"); } return s.toString(); } private static final class CharsetRef { private Charset value; public boolean isPresent() { return value != null; } public void set(Charset value) { this.value = checkNotNull(value); } public Charset get() { return value; } } /** * Attempts to detect the {@code Charset} from the * {@link #headers() headers} using {@link DefaultHeaderCharDet} in addition * to any provided detectors. */ public Optional<Charset> detectDecodeCharset(HeaderCharDet... headerCharDets) { return detectDecodeCharset(Arrays.asList(headerCharDets)); } /** * Attempts to detect the {@code Charset} from the * {@link #headers() headers} using {@link DefaultHeaderCharDet} in addition * to any provided detectors. */ public Optional<Charset> detectDecodeCharset(Iterable<? extends HeaderCharDet> headerCharDets) { if (!hasHeaders()) { return Optional.absent(); } final CharsetRef charset = new CharsetRef(); final FutureCallback<String> fc = new FutureCallback<String>() { @Override public void onSuccess(String result) { if (result != null && !charset.isPresent()) { Charset cs = request.charsetFunction().apply(result); if (cs != null) { charset.set(cs); } else { request.onError(Errors.create(Response.this, "unsupported Charset (%s)", result)); } } else { request.onError(Errors.create(Response.this, "ignoring Charset (%s)", result)); } } @Override public void onFailure(Throwable t) { //no longer causes an error if charset detection fails // ErrorIOException.send(request, t); } }; if (request.hasCharsetDetectors()) { headerCharDets = Iterables.concat(headerCharDets, Iterables.filter(request.charsetDetectors(), HeaderCharDet.class)); } boolean useDefaultHeaderCharDet = true; for (HeaderCharDet hcd : headerCharDets) { Futures.addCallback(hcd.charsetFuture(), fc); if (hcd instanceof DefaultHeaderCharDet) { useDefaultHeaderCharDet = false; } } if (useDefaultHeaderCharDet) { HeaderCharDet hcd = new DefaultHeaderCharDet(); Futures.addCallback(hcd.charsetFuture(), fc); headerCharDets = CollectUtils.concat(headerCharDets, hcd); } for (HeaderCharDet hcd : headerCharDets) { if (charset.isPresent()) { break; } hcd.onHeaders(headers.asMultimap()); } for (HeaderCharDet cdet : headerCharDets) { cdet.onHeadersComplete(); } return Optional.fromNullable(charset.get()); } private static boolean p(String m, Object... args) { // System.out.printf("response: " + m, args); // System.out.println(); return true; } boolean isEqual(@Nullable Response r) throws IOException { return r != null && p("r") && UriUtil.uriEqual(uri(), r.uri()) //tests uri and params && p("uri") && byteSourceEquals(r) && p("byte") && charSequenceEquals(r) && p("char") && classEquals(r) && p("class") && p("%d, %d", code, r.code) && code == r.code && p("code") && Objects.equal(decodeCharset, r.decodeCharset) && p("charset") && p("headers: %s, %s", headers, r.headers) && Objects.equal(MoreObjects.firstNonNull(headers, new Headers()), MoreObjects.firstNonNull(r.headers, new Headers())) && p("headers") && Objects.equal(message, r.message) && p("response") && request.isEqual(r.request); } @SuppressWarnings("null") boolean byteSourceEquals(Response r) throws IOException { if (asByteSource == null) { return r.asByteSource == null; } if (r.asByteSource == null) { return false; } return asByteSource.get().contentEquals(r.asByteSource.get()); } @SuppressWarnings("null") boolean charSequenceEquals(Response r) { // p("this: %s, other: %s", asCharSequence.get().toCharArray().length, r.asCharSequence.get().toCharArray().length); if (asCharSequence == null) { return r.asCharSequence == null; } if (r.asCharSequence == null) { return false; } return Arrays.equals(asCharSequence.get().toCharArray(), r.asCharSequence.get().toCharArray()); } @SuppressWarnings("null") boolean classEquals(Response r) { if (asClass == null) { return r.asClass == null; } if (r.asClass == null) { return false; } return Objects.equal(asClass.get(), r.asClass.get()); } }