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.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))); } }