com.tinspx.util.net.Request.java Source code

Java tutorial

Introduction

Here is the source code for com.tinspx.util.net.Request.java

Source

/* 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.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import static com.google.common.base.Preconditions.*;
import com.google.common.base.Predicates;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.escape.Escaper;
import com.google.common.io.ByteSource;
import com.google.common.net.UrlEscapers;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Atomics;
import com.google.common.util.concurrent.ExecutionList;
import com.google.common.util.concurrent.MoreExecutors;
import com.tinspx.util.base.Base;
import com.tinspx.util.base.BasicError;
import com.tinspx.util.base.Cancellable;
import com.tinspx.util.base.ContentCallback;
import com.tinspx.util.base.ContentListener;
import com.tinspx.util.base.Errors;
import com.tinspx.util.collect.CollectUtils;
import com.tinspx.util.collect.Listenable;
import com.tinspx.util.collect.NotNull;
import com.tinspx.util.collect.Predicated;
import com.tinspx.util.io.callbacks.FileCallback;
import com.tinspx.util.io.charset.CharsetDetector;
import com.tinspx.util.io.charset.CharsetUtils;
import com.tinspx.util.io.charset.DefaultHeaderCharDet;
import com.tinspx.util.io.charset.HtmlCharDet;
import com.tinspx.util.io.charset.XmlCharDet;
import java.io.IOException;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.experimental.Delegate;
import lombok.extern.slf4j.Slf4j;

/**
 * {@code Request} <i>describes</i> a request for a resource at some
 * {@link URI}. A {@link RequestContext} is responsible for actually executing
 * the {@code Request} and generating a {@link Response}.
 * <p>
 * A {@code Request} is <i>not</i> thread safe. After being submitted to a
 * {@code RequestContext}, a {@code Request} may not be modified except in
 * the callback methods of any {@link #callback(ContentListener) registered}
 * {@link ContentListener}, {@link ContentCallback}, or {@link RequestCallback}.
 * <p>
 * {@link #properties() properties()} returns a modifiable {@code Map} so that
 * may be used to store request specific data in the request itself. This
 * property map is shared with all associated redirect requests and responses.
 * There are several standard {@code String} keys that provide additional
 * features such as redirect and request delay/rate limiting (ie
 * {@link #REDIRECT_DELAY_MILLIS} or {@link #USE_CACHE}). Attributes stored in
 * the property map are features that a {@code RequestContext} is not required
 * to implement (but is encouraged to do so if possible).
 * <br><br>
 * <hr>
 * Information for {@link RequestContext} implementations:
 * <p>
 * When a {@code Request} is submitted, ensure that
 * {@link #start(AsyncFunction)} is called, this ensures that the request cannot
 * be executed by any other {@code RequestContext}. A {@code RequestContext} can
 * be notified when this request is canceled via {@link #cancel() cancel()} by
 * {@link #addCancelListener(Runnable, Executor) registering} cancel listeners.
 * <p>
 * This class provides hints on how the user would like to access the response
 * content. If {@link #asByteSource() asByteSource()} is true, the raw buffered
 * byte content should be made available in the {@code Response}. If
 * {@link #asCharSequence() asCharSequence()} is true the decoded content should
 * be made available in the {@code Response}. Unless
 * {@link #detectCharset() detectCharset()} is false, a best effort attempt
 * should be made to detect the appropriate {@code Charset} to decode the
 * content. The {@link CharsetDetector}s returned from
 * {@link #charsetDetectors()} should should be used in addition to any standard
 * or default detectors. If the {@code Charset} could not be detected, default
 * to {@link #decodeCharset() decodeCharset()}. If neither
 * {@link #asByteSource() asByteSource()}, or
 * {@link #asCharSequence() asCharSequence()} is true, the response content
 * should not be buffer in memory; instead, there will typically be some
 * registered {@link #callbacks() callbacks} that will process the content (such
 * as {@link FileCallback} saving the content to a {@code File}).
 * {@link #asClass()} returns a Class the user would like the response converted
 * into. This is typically used with some external library (such as gson or
 * jackson) to convert some data format (such as json or xml) into a POJO.
 * {@code RequestContext}s are not required to support this feature.
 * 
 * @see Response
 * @see RequestFunction
 * @see RequestContext
 * @see RequestCallback
 * @author Ian
 */
@Accessors(fluent = true)
@Slf4j
@NotThreadSafe
public class Request implements Cancellable, BasicError.Listener {

    private static final AtomicLong ID_COUNTER = new AtomicLong();

    /**
     * The millis time (acquired from {@link System#currentTimeMillis()}) that
     * the {@code Request} was first submitted to a {@link RequestContext}. A
     * decorating {@code RequestContext} may set this property to indicate the
     * actual time the request was submitted for the delegate
     * {@code RequestContext}.
     */
    public static final String SUBMISSION_MILLIS = "SUBMISSION_MILLIS";
    /**
     * Sets a redirect delay in milliseconds. If no redirect delay is set, any
     * redirect should be executed immediately.
     */
    public static final String REDIRECT_DELAY_MILLIS = "REDIRECT_DELAY_MILLIS";
    /**
     * Sets a delay for the {@code Request} in milliseconds. If no request
     * delay is set, the {@code Request} should be executed immediately.
     */
    public static final String REQUEST_DELAY_MILLIS = "REQUEST_DELAY_MILLIS";
    /**
     * If set in {@link #properties()}, enables or disables any local cache
     * employed by a {@code RequestContext}. Setting this {@code Boolean.FALSE}
     * forces the {@code Request} to be executed and not be generated from any
     * local cache.
     */
    public static final String USE_CACHE = "USE_CACHE";
    /**
     * If set in {@link #properties()}, determines if the
     * {@link RequestCallback} callbacks should receive redirect events. Must be
     * a {@code Boolean}. If {@code Boolean.TRUE}, callbacks will not receive
     * redirect events and redirects <i>may</i> not be made available through
     * {@link Response#redirect()}.
     */
    public static final String HIDE_REDIRECTS = "HIDE_REDIRECTS";
    /**
     * The read timeout in milliseconds.
     */
    public static final String READ_TIMEOUT_MILLIS = "READ_TIMEOUT_MILLIS";
    /**
     * The connect timeout in milliseconds.
     */
    public static final String CONNET_TIMEOUT_MILLIS = "CONNET_TIMEOUT_MILLIS";
    /**
     * The write timeout in milliseconds.
     */
    public static final String WRITE_TIMEOUT_MILLIS = "WRITE_TIMEOUT_MILLIS";

    /**
     * Returns a positive long {@code id} for this request that is shared with
     * all redirects and responses associated with this request. This {@code id}
     * is therefore <i>not</i> unique to all other requests, but rather allows
     * requests to be linked to their redirects and responses.
     */
    @Getter
    final long id;
    /**
     * An arbitrary, user-defined tag. Has no effect on the {@code Request}.
     * Note that any arbitrary {@code Object} may be set on this {@code Request}
     * using the {@link #properties()} {@code Map}.
     */
    @Nullable
    @Setter
    String tag;
    /**
     * the URI supplied from the user, it is used to build the {@link #actual}
     * URI
     */
    @Nonnull
    URI uri;
    /**
     * The HTTP method for this {@code Request}. Defaults to {@code "GET"}.
     */
    @Nonnull
    @Getter
    String method = "GET";
    /**
     * Determines if and how redirects are followed. Defaults to
     * {@link Requests#defaultRedirectHandler()}.
     */
    @Nonnull
    @Getter
    @Setter
    RedirectHandler redirectHandler = Requests.defaultRedirectHandler();
    /**
     * Determines if input should be read from the response. Defaults to
     * {@code true}. Set to {@code false} if reading the response is not
     * necessary.
     */
    @Getter
    boolean doInput = true;
    /**
     * The {@code Escaper} used to escape the query parameters. Defaults to
     * {@link UrlEscapers#urlFormParameterEscaper()} (and should not typically
     * be altered).
     */
    @Nonnull
    @Getter
    @Setter
    Escaper queryEscaper = UrlEscapers.urlFormParameterEscaper();

    /**
     * Default {@code Charset} to decode the response with. By default the
     * decode {@code Charset} will be detected from the response, but if the
     * {@code Charset} cannot be detected, it will default to his
     * {@code Charset}. Defaults to {@code UTF-8} if no {@code Charset} can be
     * detected. To prevent {@code Charset} detection, set
     * {@link #detectCharset(boolean)} to {@code false}.
     */
    @Nonnull
    @Setter
    @Getter
    Charset decodeCharset = Charsets.UTF_8;
    /**
     * Determines if the decode {@code Charset} should be detected. Defaults to
     * {@code true}. Set to false to force the decode {@code Charset} set in
     * {@link #decodeCharset(Charset)} to be used. If true,
     * {@link CharsetDetector}s can be added through
     * {@link #charsetDetectors()}.
     */
    @Getter
    @Setter
    boolean detectCharset = true;
    /**
     * explicit {@code CharsetDetector}s
     */
    @Nullable
    Set<CharsetDetector> detectors;
    /**
     * the query params, null until first param added
     */
    @Nullable
    ListMultimap<String, String> params;
    /**
     * wraps {@link #params} to observe modifications
     */
    @Nullable
    ListMultimap<String, String> paramsView;
    /**
     * Returns the headers of this {@code Request}.
     */
    final @Getter Headers headers;
    /**
     * A modifiable Set of callbacks registered with this {@code Request}.
     * Changes made to the returned Set will affect what callbacks are
     * registered with this {@code Request}. Callbacks may be added or removed
     * at any time. However, if callbacks are added after the request as
     * started, there is no guarantee that it will receive all callback events.
     * <p>
     * Use {@link #callback(ContentListener)} or
     * {@link Requests#wrap(ContentListener) Requests.wrap} in order to
     * add {@link ContentListener} or {@link ContentCallback} instances.
     * 
     * @return a non-null, modifiable Set of registered callbacks
     */
    @Nonnull
    @Getter
    final Set<RequestCallback> callbacks;
    /**
     * Determines if the input should be read fully into a {@code ByteSource}
     * and made available through {@link Response#asByteSource()}.
     */
    @Getter
    boolean asByteSource;
    /**
     * Determines if the input should be read fully into a
     * {@code ExtendedCharSequence} and made available through
     * {@link Response#asCharSequence()}.
     */
    @Getter
    boolean asCharSequence;
    /**
     * determines if the input should be converted into an instance of this
     * {@code Class} by some undefined means
     */
    @Nullable
    Class<?> asClass;
    /**
     * an explicit request body
     */
    @Nullable
    RequestBody body;
    /**
     * current CookieHandler, null if none set
     */
    @Nullable
    CookieHandler cookieHandler;
    /**
     * Returns a {@code Map} on which any property may be set. These properties
     * can be used to instruct {@link RequestContext}s to perform optional
     * operations. It may also be used store any arbitrary data associated with
     * the request that can be retrieved from is response.
     * <p>
     * Note that this property map is shared with this request's response
     * and all redirect requests and responses.
     */
    final @Getter Map<Object, Object> properties;
    /**
     * the actual/effective uri. This uri will differ from the {@link #uri}
     * field in that the query segment will be updated
     */
    @Nullable
    URI actual;
    /**
     * shared StringBuilder for building query strings and the uri
     */
    @Nullable
    StringBuilder qb;
    /**
     * actual query, will reflect the params field
     */
    @Nullable
    String query;
    /**
     * the previous response if this request is a redirect
     */
    final @Nullable Response cause;
    /**
     * When input needs to be decoded into text, this function is used to
     * convert the {@code Charset} into a {@code CharsetDecoder} for decoding.
     * Defaults to {@link CharsetUtils#replaceDecoderFunction()}. Note that
     * using {@code CharsetDecoder} with {@link CodingErrorAction#REPORT} as one
     * of its actions will likely cause its associated
     * {@link CharacterCodingException} to be thrown if a decoding error is
     * encountered.
     */
    @Nonnull
    @Getter
    @Setter
    Function<? super Charset, ? extends CharsetDecoder> decoderFunction = CharsetUtils.replaceDecoderFunction();
    /**
     * If {@link #detectCharset()} is true and the {@code Charset} is detected,
     * this Function is used to convert the {@code Charset} name into a
     * {@link Charset}. Defaults to
     * {@link CharsetUtils#defaultCharsetFunction()}.
     */
    @Nonnull
    @Getter
    @Setter
    Function<? super String, ? extends Charset> charsetFunction = CharsetUtils.defaultCharsetFunction();

    final List<BasicError> errors = Lists.newCopyOnWriteArrayList();
    /**
     * unmodifiable view of {@link #errors}
     */
    @Nullable
    List<BasicError> errorsView;

    final AtomicReference<AsyncFunction<? super Request, Response>> requestContext = Atomics.newReference();

    private static class CancelList implements Cancellable {
        volatile boolean cancelled;
        final ExecutionList listeners = new ExecutionList();

        @Override
        public boolean cancel() {
            cancelled = true;
            listeners.execute();
            return true;
        }

        @Override
        public boolean isCancelled() {
            return cancelled;
        }
    }

    @Delegate(types = Cancellable.class)
    final CancelList cancel;

    /**
     * Creates a {@code Request} with the specified {@code URI}. The query
     * parameters in {@code uri} are decoded using {@code UTF-8}.
     */
    public Request(String uri) throws URISyntaxException {
        this(new URI(uri), Charsets.UTF_8);
    }

    /**
     * Creates a {@code Request} with the specified {@code URI}. The query
     * parameters in {@code uri} are decoded using {@code encoding}.
     */
    public Request(String uri, Charset encoding) throws URISyntaxException {
        this(new URI(uri), encoding);
    }

    /**
     * Creates a {@code Request} with the specified {@code URI}. The query
     * parameters in {@code uri} are decoded using {@code UTF-8}.
     */
    public Request(URI uri) {
        this();
        setUri(uri, Charsets.UTF_8);
    }

    /**
     * Creates a {@code Request} with the specified {@code URI}. The query
     * parameters in {@code uri} are decoded using {@code encoding}.
     */
    public Request(URI uri, Charset encoding) {
        this();
        setUri(uri, encoding);
    }

    /**
     * Sets the default values of the final fields. The uri must still be set
     * after calling this constructor.
     */
    private Request() {
        callbacks = NotNull.set(Sets.<RequestCallback>newCopyOnWriteArraySet());
        cancel = new CancelList();
        cause = null;
        headers = new Headers();
        id = ID_COUNTER.incrementAndGet();
        properties = Maps.newHashMap();
    }

    /**
     * duplicate/copy constructor used by {@link #duplicate()}.
     */
    @SuppressWarnings("null")
    private Request(Request r) throws IOException {
        //actual will be rebuilt
        asByteSource = r.asByteSource;
        asCharSequence = r.asCharSequence;
        asClass = r.asClass;
        if (r.body != null) {
            body = r.body.duplicate();
        }
        callbacks = NotNull.set(Sets.newCopyOnWriteArraySet(r.callbacks));
        cancel = new CancelList();
        cause = null;
        charsetFunction = r.charsetFunction;
        cookieHandler = r.cookieHandler;
        decodeCharset = r.decodeCharset;
        decoderFunction = r.decoderFunction;
        detectCharset = r.detectCharset;
        if (r.hasCharsetDetectors()) {
            detectors = NotNull
                    .set(Sets.newHashSet(Iterables.transform(r.detectors, CharsetUtils.duplicateFunction())));
        }
        doInput = r.doInput;
        //errors not copied
        //errorsView not copied
        headers = new Headers(r.headers);
        id = ID_COUNTER.incrementAndGet();
        method = r.method;
        if (r.hasQueryParams()) {
            params = LinkedListMultimap.create(r.params);
        }
        //paramsView not copied
        properties = Maps.newHashMap(r.properties);
        properties.remove(SUBMISSION_MILLIS);
        //qb not copied
        //query will be rebuilt
        queryEscaper = r.queryEscaper;
        redirectHandler = r.redirectHandler;
        //requestContext not copied
        tag = r.tag;
        uri = r.uri;
    }

    /**
     * Returns a request essentially equivalent to this request. The returned
     * request will have exactly the same settings and properties as this
     * request. However, the returned request will have no
     * {@link #errors() errors} and no {@link #cause() cause}
     * ({@link #isRedirect()} will return false), regardless of whether this
     * request has errors or is redirected. The returned request will have not
     * been {@link #isStarted() started} and may be submitted to a
     * {@code RequestContext} regardless of whether this request has been
     * started.
     */
    public Request duplicate() throws IOException {
        return new Request(this);
    }

    /**
     * Used when creating a redirect request. Essentially makes a copy of the
     * specified request. However, {@link #callbacks}, {@link #cancel}, and
     * {@link #properties} are not copied, but are shared with this Request.
     * This links the cancel state, listeners, and properties of all redirects
     * (and their responses}.
     */
    @SuppressWarnings("null")
    private Request(Request r, Response cause) throws IOException {
        //actual will be rebuilt
        asByteSource = r.asByteSource;
        asCharSequence = r.asCharSequence;
        asClass = r.asClass;
        if (r.body != null) {
            body = r.body.duplicate();
        }
        callbacks = r.callbacks;
        cancel = r.cancel;
        this.cause = checkNotNull(cause);
        charsetFunction = r.charsetFunction;
        cookieHandler = r.cookieHandler;
        decodeCharset = r.decodeCharset;
        decoderFunction = r.decoderFunction;
        detectCharset = r.detectCharset;
        if (r.hasCharsetDetectors()) {
            detectors = NotNull
                    .set(Sets.newHashSet(Iterables.transform(r.detectors, CharsetUtils.duplicateFunction())));
        }
        doInput = r.doInput;
        //errors not copied
        //errorsView not copied
        headers = new Headers(r.headers);
        id = r.id;
        method = r.method;
        if (r.hasQueryParams()) {
            params = LinkedListMultimap.create(r.params);
        }
        //paramsView not copied
        properties = r.properties;
        //qb not copied
        //query will be rebuilt
        queryEscaper = r.queryEscaper;
        redirectHandler = r.redirectHandler;
        //requestContext not copied
        tag = r.tag;
        uri = r.uri;
    }

    /**
     * Creates a copy of this {@code Request} to be used as a redirect. The
     * returned {@code Request} is independent from but equivalent to this
     * {@code Request}. However, the returned {@code Request} shares the
     * {@link #callbacks() callbacks}, {@link #properties() properties}, and
     * {@link #isCancelled() cancel} state. Callbacks or listeners added to this
     * request or the returned request are added to both. If one request is
     * cancelled, both are. A property added to one request is added to both.
     * All other fields of the returned {@code Request} are are independent from
     * this request.
     */
    Request redirect(Response cause) throws IOException {
        return new Request(this, cause);
    }

    /**
     * Determines if any headers have been added to this {@code Request} through
     * {@link #headers()}.
     */
    public boolean hasHeaders() {
        return !headers.isEmpty();
    }

    /**
     * Sets the given header as the <i>only</i> header mapped to {@code name}.
     * Replaces any headers already mapped to {@code name}. Equivalent to
     * {@code headers().set(name, value)}.
     */
    public Request header(String name, String value) {
        headers.set(name, value);
        return this;
    }

    /**
     * Adds the specified header to {@link #headers()}. Unlike
     * {@link #header(String, String)}, this header is added to any other
     * headers mapped to {@code name} rather than replacing them. Equivalent to
     * {@code headers().add(name, value)}.
     */
    public Request addHeader(String name, String value) {
        headers.add(name, value);
        return this;
    }

    /**
     * Convenience method equivalent to {@code properties().put(key, value)}.
     */
    public Request 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);
    }

    /**
     * Same as {@link #addCancelListener(Runnable, Executor) 
     * addCancelListener(listener, MoreExecutors.directExecutor())}.
     */
    public Request addCancelListener(Runnable listener) {
        return addCancelListener(listener, MoreExecutors.directExecutor());
    }

    /**
     * Adds a listener when this request is cancelled through
     * {@link #cancel() cancel()}. The listener <i>may</i> not be notified if
     * this request is cancelled through is response future returned from a
     * {@link RequestContext}. This method is thread safe and may be called at
     * any time.
     */
    public Request addCancelListener(Runnable listener, Executor executor) {
        cancel.listeners.add(listener, executor);
        return this;
    }

    /**
     * same as {@link #onError(BasicError, boolean) onError(error, true)}.
     */
    @Override
    public void onError(BasicError error) {
        onError(error, true);
    }

    /**
     * Indicates the given error was encountered and sends the error to all
     * listeners. This error is made available in {@link #errors()}.
     * 
     * @param error the error to send
     * @param propagate true if any uncaught exception thrown by a callback
     * should be propagated
     */
    public void onError(BasicError error, boolean propagate) {
        if (error == null) {
            error = Errors.create(this);
        }
        errors.add(error);
        if (hasCallbacks()) {
            Throwable throwable = null;
            for (RequestCallback rc : callbacks) {
                try {
                    rc.onError(error);
                } catch (Throwable t) {
                    log.error("onError: {}", this, t);
                    errors.add(Errors.create(t));
                    throwable = t;
                }
            }
            if (throwable != null && propagate) {
                throw Throwables.propagate(throwable);
            }
        }
    }

    /**
     * Determines if any error has been encountered.
     * @see #errors()
     */
    public boolean hasErrors() {
        return !errors.isEmpty();
    }

    /**
     * Returns all encountered errors.
     */
    public List<BasicError> errors() {
        if (errorsView == null) {
            errorsView = Collections.unmodifiableList(errors);
        }
        return errorsView;
    }

    /**
     * returns the {@code RequestContext} running this request. Will only be
     * available if {@link #isStarted()} is true. This method is thread safe and
     * may be called at any time.
     */
    public Optional<? extends AsyncFunction<? super Request, Response>> requestContext() {
        return Optional.fromNullable(requestContext.get());
    }

    /**
     * Determines if this request has been submitted to a {@link RequestContext}
     * for execution. A {@code RequestContext} executing this request must
     * invoke {@link #start(AsyncFunction) start} when it
     * begins the request.
     * This method is thread safe and may be called at any time.
     */
    public boolean isStarted() {
        return requestContext.get() != null;
    }

    /**
     * Indicates that {@code requestContext} has started executing this request.
     * This method is thread safe and may be called at any time.
     * 
     * @throws IllegalStateException if this request is already running
     * ({@link #isStarted()} is true).
     */
    public void start(@NonNull AsyncFunction<? super Request, Response> requestContext) {
        if (!this.requestContext.compareAndSet(null, requestContext)) {
            throwStartedException();
        }
    }

    /**
     * Utility function that allows a {@code RequestContext} to cancel this
     * request if it has been cancelled before it has been started. Sets
     * {@code requestContext} as the context executing this request and
     * immediately calls the {@code onCancel} method of all callbacks.
     * 
     * @param propagate true if any uncaught exception thrown by a callback
     * should be propagated
     * @throws IllegalStateException if {@link #isStarted() started} or not
     * {@link #isCancelled() cancelled}
     */
    public void onCancel(@NonNull AsyncFunction<? super Request, Response> requestContext, boolean propagate) {
        if (!this.requestContext.compareAndSet(null, requestContext)) {
            throwStartedException();
        }
        if (hasCallbacks()) {
            response().onCancel(propagate);
        }
    }

    /**
     * Throws an {@code IllegalStateException} if this request has been
     * {@link #isStarted() stared}.
     */
    public void checkNotStarted() {
        if (isStarted()) {
            throwStartedException();
        }
    }

    private void throwStartedException() {
        throw new IllegalStateException(this + " has already been started by " + requestContext.get());
    }

    /**
     * Returns a non-null, modifiable Set of {@code CharsetDetector}s that have
     * been added to this {@code Request}. Registering {@code CharsetDetector}s
     * is useful when non-standard {@code CharsetDetector}s need to be used or
     * if a certain {@code CharsetDetector} <i>must</i> be used. For example, if
     * it is known that the response will be xml, an {@link XmlCharDet} could be
     * added.
     * <p>
     * By default, {@code Charset} detection will be attempted from HTTP headers
     * ({@link DefaultHeaderCharDet}), html meta tags ({@link HtmlCharDet}) if
     * the response {@link Response#isXml() is html}, and xml prolog
     * ({@link XmlCharDet}) if the response {@link Response#isXml() is xml}. If
     * no other detectors are required, there is no need to add any using this
     * method.
     */
    public Set<CharsetDetector> charsetDetectors() {
        if (detectors == null) {
            detectors = NotNull.set(Sets.<CharsetDetector>newHashSet());
        }
        return detectors;
    }

    /**
     * Determines if this {@code Request} has any registered
     * {@code CharsetDetector}s.
     */
    public boolean hasCharsetDetectors() {
        return detectors != null && !detectors.isEmpty();
    }

    private void checkDoInputTrue(boolean check) {
        if (check) {
            checkState(doInput, "doInput() is false");
        }
    }

    private void checkDoInput(boolean doInput) {
        if (!doInput) {
            if (asByteSource) {
                throw new IllegalStateException("Request.asByteSource() is true");
            }
            if (asCharSequence) {
                throw new IllegalStateException("Request.asCharSequence() is true");
            }
            if (asClass != null) {
                throw new IllegalStateException("Request.asClass() is " + asClass);
            }
        }
    }

    /**
     * Returns the request tag set through {@link #tag(String)} or the empty
     * string if there is no tag.
     */
    public String tag() {
        return Strings.nullToEmpty(tag);
    }

    /**
     * Utility method for {@link RequestContext}s. Sends the
     * {@link RequestCallback#onSetup(Request) onSetup} event to all callbacks.
     * If this request is a {@link #isRedirect() redirect}, callbacks with
     * {@link RequestCallback#ignoreRedirects() ignoreRedirects()} true will not
     * be notified.
     *
     * @return true if this Request is canceled
     */
    public boolean onSetup() {
        if (hasCallbacks()) {
            for (RequestCallback rc : callbacks) {
                if (cause == null || !rc.ignoreRedirects()) {
                    rc.onSetup(this);
                }
            }
        }
        return isCancelled();
    }

    /**
     * Sends the
     * {@link RequestCallback#onRedirect(Response, Request) onRedirect} event to
     * all callbacks. Callbacks with
     * {@link RequestCallback#ignoreRedirects() ignoreRedirects()} true will not
     * be notified.
     *
     * @return true if this Request is canceled
     * @throws IllegalStateException if {@link #isRedirect()} is false
     */
    public boolean onRedirect() {
        checkState(cause != null, "not a redirect");
        if (hasCallbacks()) {
            for (RequestCallback rc : callbacks) {
                if (!rc.ignoreRedirects()) {
                    rc.onRedirect(cause, this);
                }
            }
        }
        return isCancelled();
    }

    /**
     * Determines if this Request should read input from the connection.
     * Defaults to {@code true}. Set to {@code false} to prevent the response
     * from being read. If set to {@code false}, a {@code RequestContext}
     * <i>may</i> not have to read the response input.
     *
     * @param doInput true if input should be read from the response
     * @return this Request instance
     * @throws IllegalStateException if {@code doInput} is false and any of
     * {@link #asByteSource()}, {@link #asCharSequence()}, or {@link #asClass()}
     * is true
     */
    public Request doInput(boolean doInput) {
        checkDoInput(doInput);
        this.doInput = doInput;
        return this;
    }

    /**
     * Determines if this request should produce a {@link ByteSource}. The
     * {@code ByteSource} is made available through
     * {@link Response#asByteSource()}.
     *
     * @throws IllegalStateException if {@link #doInput()} is false
     */
    public Request asByteSource(boolean asByteSource) {
        checkDoInputTrue(asByteSource);
        this.asByteSource = asByteSource;
        return this;
    }

    /**
     * Determines if this request should produce an {@code CharSequence}. The
     * {@code CharSequence} is made available through
     * {@link Response#asCharSequence()}.
     *
     * @throws IllegalStateException if {@link #doInput()} is false
     */
    public Request asCharSequence(boolean asCharSequence) {
        checkDoInputTrue(asCharSequence);
        this.asCharSequence = asCharSequence;
        return this;
    }

    /**
     * Instructs this {@code Request}to produce an instance of the specified
     * {@code Class}. The process for converting the response input into the
     * given {@code Class} is undefined. The {@link RequestContext} is
     * responsible for this conversion. The result of this conversion is made
     * available through {@link Response#asClass(Class)}.
     *
     * @throws IllegalStateException if {@link #doInput()} is false
     */
    public Request asClass(@Nullable Class<?> cls) {
        checkDoInputTrue(cls != null);
        this.asClass = cls;
        return this;
    }

    /**
     * Returns the {@code Class} previously set through
     * {@link #asClass(Class)}.
     */
    @SuppressWarnings("unchecked")
    public Optional<Class<?>> asClass() {
        return (Optional<Class<?>>) Optional.fromNullable(asClass);
    }

    /**
     * Adds a callback that will receive {@code ByteBuffer} updates. Although
     * the the {@code callback} argument is a {@link ContentListener}, the
     * proper events will still be called if the {@code callback} argument is a
     * {@link ContentCallback} or {@link RequestCallback}.
     * <p>
     * If {@code callback} is not a {@code RequestCallback}, it will <i>only</i>
     * receive the content from the final redirect. This can be very useful for
     * {@code ContentCallback}s such as {@link FileCallback} which should not
     * receive the content from intermediate redirects.
     * <p>
     * This method is thread safe and may be called at any time. <i>However</i>,
     * any callbacks added after this request has started are not guaranteed to
     * receive all events.
     * 
     * @return this Request instance
     */
    public Request callback(ContentListener<? super Response, ? super ByteBuffer> callback) {
        callbacks.add(Requests.wrap(callback));
        return this;
    }

    /**
     * Adds a {@code BasicError.Listener} that will <i>only</i> receive error
     * events (regardless of whether it implements other interfaces).
     */
    public Request errorListener(BasicError.Listener callback) {
        checkArgument(callback != this, "callback can't be this request");
        callbacks.add(Requests.wrap(callback));
        return this;
    }

    /**
     * Returns true if this Request has any registered callbacks.
     */
    public boolean hasCallbacks() {
        return !callbacks.isEmpty();
    }

    @SuppressWarnings("element-type-mismatch")
    public boolean removeCallback(@Nullable Object callback) {
        if (callbacks.remove(callback)) {
            return true;
        } else if (callback != null) {
            for (RequestCallback rc : callbacks) {
                if (rc instanceof Requests.ContentListenerWrapper) {
                    if (((Requests.ContentListenerWrapper) rc).delegate == callback) {
                        callbacks.remove(rc);
                        return true;
                    }
                } else if (rc instanceof Requests.ErrorListenerWrapper) {
                    if (((Requests.ErrorListenerWrapper) rc).delegate == callback) {
                        callbacks.remove(rc);
                        return true;
                    }
                }
            }
        }
        return false;
    }

    /**
     * Sets the {@link CookieHandler}. set to null to the clear the current
     * {@link CookieHandler}.
     */
    public Request cookieHandler(@Nullable CookieHandler cookieHandler) {
        this.cookieHandler = cookieHandler;
        return this;
    }

    /**
     * Cookies will be accepted, but will only be stored locally in this
     * {@code Request}. This ensures cookies are processed, but prevents the
     * cookies from being saved to some global {@link CookieHandler} (if one
     * exists).
     */
    public Request localCookies() {
        this.cookieHandler = new CookieManager();
        return this;
    }

    /**
     * No cookies will be accepted or stored by this {@code Request}.
     */
    public Request noCookies() {
        this.cookieHandler = Cookies.emptyCookieHandler();
        return this;
    }

    /**
     * Determines if a CookieHandler has been set. If false, a
     * {@link RequestContext} is free to use any {@link CookieHandler}.
     *
     * @return true if an explicit {@link CookieHandler} has been set
     */
    public boolean hasCookieHandler() {
        return cookieHandler != null;
    }

    /**
     * The current {@code CookieHandler}. Defaults to absent. If absent, the
     * {@link RequestContext} may set and use any {@code CookieHandler} on the
     * {@code Request}. Many {@link RequestContext}s will set the global
     * {@code CookieHandler} if the handler is absent. Use {@link #noCookies()}
     * to ensure that no cookies are sent or accepted.
     *
     * @see #noCookies()
     * @see #localCookies()
     */
    public Optional<CookieHandler> cookieHandler() {
        return Optional.fromNullable(cookieHandler);
    }

    /**
     * sets the given uri, normalizing and validating it. the query params will
     * be overwritten (existing query parameters will be lost) with the query
     * params of the new uri. The {@code encoding} controls how query parameters
     * in the uri are decoded
     */
    private void setUri(@NonNull URI uri, @NonNull Charset encoding) {
        this.uri = uri.normalize();
        extractParams(encoding); //calls invalidate()
    }

    /**
     * invalidates the query string and actual uri. called when the query
     * params are update
     */
    private void invalidate() {
        actual = null;
        query = null;
    }

    /**
     * Sets a new URI, completely overwriting the previous URI. All previous
     * query parameters will be lost and replaced with any query parameters in
     * the new URI. The query parameters in {@code uri} are decoded using
     * {@code UTF-8}.
     *
     * @param uri the new URI of this {@code Request}
     * @return this instance
     */
    public Request uri(URI uri) {
        setUri(uri, Charsets.UTF_8);
        return this;
    }

    /**
     * Sets a new URI, completely overwriting the previous URI. All previous
     * query parameters will be lost and replaced with any query parameters in
     * the new URI. The query parameters in {@code uri} are decoded using
     * {@code encoding}.
     *
     * @param uri the new URI of this {@code Request}
     * @param encoding the {@code Charset} used to decode the query parameters
     * of {@code uri}
     * @return this instance
     */
    public Request uri(URI uri, Charset encoding) {
        setUri(uri, encoding);
        return this;
    }

    /**
     * Sets a new URI, completely overwriting the previous URI.
     * @see #uri(URI)
     */
    public Request uri(@NonNull String uri) throws URISyntaxException {
        setUri(new URI(uri), Charsets.UTF_8);
        return this;
    }

    /**
     * Sets a new URI, completely overwriting the previous URI.
     * @see #uri(URI, Charset)
     */
    public Request uri(@NonNull String uri, Charset encoding) throws URISyntaxException {
        setUri(new URI(uri), encoding);
        return this;
    }

    /**
     * Returns the actual URI of this request which will include all added query
     * parameters. This {@code URI} may differ from the set {@code URI} if the
     * query parameters have been changed since the {@code URI} was set.
     *
     * @return the effective URI of this {@code Request}
     */
    public URI uri() {
        updateUri();
        return actual;
    }

    /**
     * Returns the query string. If only the query String is required,
     * this method may be more efficient then {@link #uri()} as only the
     * query String must be generated and not the entire URI object.
     * 
     * @return the effective query String
     */
    public String query() {
        updateQuery();
        return query;
    }

    private ListMultimap<String, String> params() {
        if (params == null) {
            params = LinkedListMultimap.create();
        }
        return params;
    }

    /**
     * Determines if this {@code Request} has any query parameters. Query
     * parameters may be accessed through {@link #queryParams()}.
     */
    public boolean hasQueryParams() {
        return params != null && !params.isEmpty();
    }

    /**
     * Returns modifiable {@code ListMultimap} of all query parameters. All
     * modifications made will be immediately visible to any subsequent call to
     * {@link #query()} or {@link #uri()}.
     */
    public ListMultimap<String, String> queryParams() {
        if (paramsView == null) {
            paramsView = Predicated.listMultimap(Listenable.listMultimap(params(), new Listenable.Modification() {
                @Override
                public void onModify(Object src, Listenable.Event type) {
                    invalidate();
                }
                //don't track entries as values can be null
            }), Predicates.notNull(), Predicates.alwaysTrue(), false);
        }
        return paramsView;
    }

    /**
     * Returns all values associated with the specified query parameter name.
     */
    public List<String> queryParam(@Nullable String name) {
        return params != null ? params.get(name) : Collections.<String>emptyList();
    }

    /**
     * Adds a query parameter with the specified name and value. 
     */
    public Request queryParam(@NonNull String name, @Nullable String value) {
        params().put(name, value);
        invalidate();
        return this;
    }

    /**
     * extracts the query parameters from the current uri, the existing
     * query parameters are lost
     */
    private void extractParams(Charset charset) {
        if (params != null) {
            params.clear();
        }
        String raw = uri.getRawQuery();
        if (raw != null && !raw.isEmpty()) {
            UriUtil.parseQuery(uri, charset, params());
        }
        invalidate();
    }

    private StringBuilder resetQueryBuilder() {
        return resetQueryBuilder(32);
    }

    private StringBuilder resetQueryBuilder(int size) {
        if (qb == null) {
            qb = new StringBuilder(Math.max(32, size));
        } else {
            qb.setLength(0);
        }
        return qb;
    }

    private String buildUri() {
        updateQuery();
        resetQueryBuilder();
        final String u = uri.toString();
        final int q = u.indexOf('?');
        final int f = u.indexOf('#', q > 0 ? q : 0);
        if (q != -1) {
            qb.append(u.substring(0, q));
        } else if (f != -1) {
            qb.append(u.substring(0, f));
        } else {
            qb.append(u);
        }
        if (!query.isEmpty()) {
            qb.append('?').append(query);
        }
        if (f != -1) {
            qb.append(u.substring(f));
        }
        return qb.toString();
    }

    private void updateUri() {
        if (actual == null) {
            actual = URI.create(buildUri());
        }
    }

    private void updateQuery() {
        if (query == null) {
            query = buildQueryString();
        }
    }

    /**
     * Determines if a {@code RequestBody} has been set. Set the
     * {@code RequestBody} through {@link #body(RequestBody)} or
     * {@link #method(String, RequestBody)}.
     */
    public boolean hasBody() {
        return body != null;
    }

    /**
     * Sets the {@code RequestBody}.
     */
    public Request body(@Nullable RequestBody body) {
        this.body = body;
        return this;
    }

    /**
     * Returns the {@code RequestBody}, if available. The {@code RequestBody}
     * can be set through {@link #body(RequestBody)} or
     * {@link #method(String, RequestBody)}.
     */
    public Optional<RequestBody> body() {
        return Optional.fromNullable(body);
    }

    /**
     * Sets the http method and {@code RequestBody}.
     */
    public Request method(@NonNull String method, @Nullable RequestBody body) {
        method(method);
        this.body = body;
        return this;
    }

    /**
     * Sets the http method.
     */
    public Request method(@NonNull String method) {
        checkArgument(!method.isEmpty(), "method cannot be empty");
        this.method = method;
        return this;
    }

    /**
     * Returns the encoded query String.
     */
    String buildQueryString() {
        if (hasQueryParams()) {
            StringBuilder sb = resetQueryBuilder(params.size() * 16);
            append(sb, params, queryEscaper());
            return sb.toString();
        } else {
            return "";
        }
    }

    static StringBuilder append(@NonNull StringBuilder sb, @Nullable Multimap<String, String> map,
            @NonNull Escaper escaper) {
        if (map == null || map.isEmpty()) {
            return sb;
        }
        boolean first = true;
        for (Map.Entry<String, String> entry : map.entries()) {
            if (sb.length() > 0 || !first) {
                sb.append('&');
            }
            first = false;
            sb.append(escaper.escape(entry.getKey()));
            if (entry.getValue() != null) {
                sb.append('=');
                sb.append(escaper.escape(entry.getValue()));
            }
        }
        return sb;
    }

    /**
     * Checks the internal state.
     * 
     * @return this instance
     * @throws IllegalStateException if an internal state error is detected
     */
    Request checkRequestState() {
        checkDoInput(doInput);
        return this;
    }

    /**
     * true only if the URI has a path and the path ends in "xml" (case
     * insensitive).
     */
    public boolean isXml() {
        String path = uri.getPath();
        if (path != null) {
            return path.toLowerCase(Locale.US).endsWith("xml");
        } else {
            return false;
        }
    }

    /**
     * true only if the URI has a path and the path ends in one of
     * ["html", "htm", "htmls", "shtml"] (case insensitive).
     */
    public boolean isHtml() {
        String p = uri.getPath();
        if (p != null) {
            p = p.toLowerCase(Locale.US);
            return p.endsWith("html") || p.endsWith("htm") || p.endsWith("htmls") || p.endsWith("shtml");
        } else {
            return false;
        }
    }

    /**
     * Returns a basic string representation of this {@code Request} that
     * includes the {@link #method() method} and {@link #uri() uri}. For more
     * detailed information, use {@link #toDetailString()}.
     */
    @Override
    public String toString() {
        return String.format("[%d] [Request] %s%s %s", id(), Strings.isNullOrEmpty(tag()) ? "" : "[" + tag() + "] ",
                method(), uri());
    }

    /**
     * Returns a String containing <i>all</i> information of this
     * {@link Request}. This String will be very long and span multiple lines.
     */
    public String toDetailString() {
        StringBuilder s = resetQueryBuilder(1024);

        s.append(toString());
        s.append("\ncause: ").append(cause);
        s.append("\nstarted: ").append(isStarted());
        s.append(" requestContext: ").append(requestContext.get());

        s.append("\ndetectCharset: ").append(detectCharset);
        s.append(" doInput: ").append(doInput);

        s.append(" decodeCharset: ").append(decodeCharset);
        s.append("\ndecoderFunction: ").append(decoderFunction);
        s.append("\ncharsetFunction: ").append(charsetFunction);
        s.append("\ncharsetDetectors: ").append(detectors);

        s.append(" asCharSequence: ").append(asCharSequence);
        s.append(" asByteSource: ").append(asByteSource);
        s.append(" asClass: ").append(asClass);
        s.append("\nbody: ").append(body);

        s.append("\ncallbacks: ").append(callbacks.size());
        s.append(" cancel: ").append(isCancelled());

        s.append("\ncookieHandler: ").append(cookieHandler);
        s.append("\nredirectHandler: ").append(redirectHandler);
        s.append("\nproperties: ").append(properties);
        s.append("\nqueryParams: ").append(params);
        s.append("\nqueryEscaper: ").append(queryEscaper);

        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("\nerrors: ");
        if (hasErrors()) {
            for (BasicError t : errors()) {
                s.append("\n  ").append(t);
            }
        } else {
            s.append("none");
        }

        return s.toString();
    }

    /**
     * Returns a new {@code Response} with -1 for the response code and the
     * empty String for the response message.
     *
     * @see #response(int, String)
     * @return the new {@code Response}
     */
    public Response response() {
        return response(-1, null);
    }

    /**
     * Returns a new {@code Response} with the specified response code and
     * message.
     *
     * @param code the response code of the new {@code Response}
     * @param message the message of the new {@code Response}
     * @return the new {@code Response}
     */
    public Response response(int code, @Nullable String message) {
        updateUri();
        return new Response(this, actual, params != null ? params : ImmutableListMultimap.<String, String>of(),
                code, message, cause == null ? null : CollectUtils.concat(cause.redirects(), cause));
    }

    public Response response(@NonNull URI uri, int code, @Nullable String message) {
        return new Response(this, uri, null, code, message,
                cause == null ? null : CollectUtils.concat(cause.redirects(), cause));
    }

    /**
     * If this request is a {@link #isRedirect() redirect}, returns the response
     * that caused the redirect. Will be present if this request was created
     * through {@link Response#redirect()}.
     * 
     * @return the cause Response of this Request if this request is a redirect
     */
    public Optional<Response> cause() {
        return Optional.fromNullable(cause);
    }

    /**
     * Determines if this request is a redirect. Redirects are created through
     * {@link Response#redirect()} and will have a {@link #cause() cause}
     * response.
     *
     * @return true if this request is redirect
     */
    public boolean isRedirect() {
        return cause != null;
    }

    public boolean setSubmissionMillisIfAbsent() {
        if (!properties.containsKey(SUBMISSION_MILLIS)) {
            properties.put(SUBMISSION_MILLIS, System.currentTimeMillis());
            return true;
        }
        return false;
    }

    /**
     * checks equality and constraints for redirect copies
     */
    @SuppressWarnings("StringEquality")
    boolean redirectEquals(Request r) {
        return detectCharset == r.detectCharset && asByteSource == r.asByteSource
                && asCharSequence == r.asCharSequence && doInput == r.doInput

                && tag().equals(r.tag()) && (actual == null || actual != r.actual)
                && (body == null || body != r.body) && cookieHandler == r.cookieHandler
                && decodeCharset == r.decodeCharset && charsetFunction == r.charsetFunction
                && decoderFunction == r.decoderFunction && queryEscaper == r.queryEscaper
                && redirectHandler == r.redirectHandler && (qb == null || qb != r.qb)
                && (query == null || query != r.query) && (paramsView == null || paramsView != r.paramsView)
                && uri == r.uri

                && method == r.method && asClass == r.asClass

                && ((headers == null && r.headers == null) || headers != r.headers)
                && Objects.equal(headers, r.headers)

                && ((params == null && r.params == null) || params != r.params) && Objects.equal(params, r.params)
                && ((errors == null && r.errors == null) || errors != r.errors)

                && properties != null && properties == r.properties && callbacks != null && callbacks == r.callbacks

                && ((detectors == null && r.detectors == null) || detectors != r.detectors)
                && (cause == null || cause != r.cause);
    }

    private static boolean p(String m, Object... args) {
        //        System.out.printf("request: " + m, args);
        //        System.out.println();
        return true;
    }

    @SuppressWarnings("null")
    boolean isEqual(@Nullable Request r) throws IOException {
        return r != null && p("r") && UriUtil.uriEqual(uri(), r.uri()) //tests uri and params
                && p("uri") && asByteSource == r.asByteSource && p("byte") && asCharSequence == r.asCharSequence
                && p("char") && Objects.equal(asClass, r.asClass) && p("class") && Requests.bodyEquals(body, r.body)
                && p("body") && Objects.equal(callbacks, r.callbacks) && callbacksEqual(this, r) && p("callbacks")
                && (cause == null ? r.cause == null : cause.isEqual(r.cause))
                && Objects.equal(charsetFunction, r.charsetFunction) && p("charsetFunction")
                && Objects.equal(cookieHandler, r.cookieHandler) && p("cookieHandler")
                && Objects.equal(decodeCharset, r.decodeCharset) && p("decodeCharset")
                && Objects.equal(decoderFunction, r.decoderFunction) && p("decoderFunction")
                && detectCharset == r.detectCharset && p("detectCharset") && Objects.equal(detectors, r.detectors)
                && elementClassEqual(detectors, r.detectors) && p("detectors") && doInput == r.doInput
                && p("doInput") && p("headers: %s, %s", headers, r.headers) && Objects.equal(headers, r.headers)
                && p("headers") && Objects.equal(method, r.method) && p("method")
                && propertiesEqual(properties, r.properties) && p("properties")
                && Objects.equal(queryEscaper, r.queryEscaper) && p("queryEscaper")
                && Objects.equal(redirectHandler, r.redirectHandler) && p("redirectHandler")
                && Objects.equal(tag, r.tag) && p("tag")
                && (cause == null ? r.cause == null : cause.isEqual(r.cause));
    }

    static boolean propertiesEqual(Map<?, ?> a, Map<?, ?> b) {
        if (a.containsKey(SUBMISSION_MILLIS)) {
            a = Maps.newHashMap(a);
            a.remove(SUBMISSION_MILLIS);
        }
        if (b.containsKey(SUBMISSION_MILLIS)) {
            b = Maps.newHashMap(b);
            b.remove(SUBMISSION_MILLIS);
        }
        return a.equals(b);
    }

    static boolean elementClassEqual(Iterable<?> a, Iterable<?> b) {
        return HashMultiset
                .create(Iterables.transform(CollectUtils.nullToEmptyIterable(a), Base.getClassFunction()))
                .equals(HashMultiset
                        .create(Iterables.transform(CollectUtils.nullToEmptyIterable(b), Base.getClassFunction())));
    }

    static boolean callbacksEqual(Request a, Request b) {
        return HashMultiset.create(Iterables.transform(a.callbacks, Requests.UNWRAP))
                .equals(HashMultiset.create(Iterables.transform(b.callbacks, Requests.UNWRAP)));
    }
}