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.okhttp; import static com.google.common.base.Preconditions.*; import com.google.common.base.Function; import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.io.Closer; import com.google.common.net.HttpHeaders; import com.google.common.primitives.Ints; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.Monitor; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import com.squareup.okhttp.Call; import com.squareup.okhttp.Callback; import com.squareup.okhttp.MediaType; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.RequestBody; import com.tinspx.util.base.Errors; import com.tinspx.util.collect.CollectUtils; import com.tinspx.util.concurrent.FutureUtils; import com.tinspx.util.io.BAOutputStream; import com.tinspx.util.io.ByteUtils; import com.tinspx.util.io.DecodingChannel; import com.tinspx.util.io.DecodingOutputStream; import com.tinspx.util.io.DetectingOutputStream; import com.tinspx.util.io.StringUtils; import com.tinspx.util.io.callbacks.ByteChannelListener; import com.tinspx.util.io.charset.ByteContentCharDet; import com.tinspx.util.io.charset.CharContentCharDet; import com.tinspx.util.io.charset.CharsetDetector; import com.tinspx.util.io.charset.HtmlCharDet; import com.tinspx.util.io.charset.XmlCharDet; import com.tinspx.util.net.Request; import com.tinspx.util.net.RequestCallback; import com.tinspx.util.net.RequestContext; import com.tinspx.util.net.RequestException; import com.tinspx.util.net.Requests; import com.tinspx.util.net.Response; import java.io.IOException; import java.io.InputStream; import java.net.CookieManager; import java.nio.ByteBuffer; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CancellationException; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.zip.InflaterInputStream; import javax.annotation.Nullable; import javax.annotation.WillClose; import javax.annotation.WillNotClose; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; import lombok.AccessLevel; import lombok.NonNull; import lombok.experimental.FieldDefaults; import lombok.extern.slf4j.Slf4j; import okio.BufferedSink; /** * A {@code RequestContext} that runs requests using a provided * {@link OkHttpClient}. The supplied {@code OkHttpClient} should be fully * configured before {@link #create(OkHttpClient, boolean) creating} an * {@code OkHttpContext}. * <p> * Using this class requires at least version 2.1 of * <a href="http://square.github.io/okhttp/">OkHttp</a> to be added to your * project as a dependency: * <pre>{@code * <dependency> * <groupId>com.squareup.okhttp</groupId> * <artifactId>okhttp</artifactId> * <version>2.1.0</version> * </dependency> * }</pre> * * @author Ian */ @Slf4j @FieldDefaults(level = AccessLevel.PRIVATE) @ThreadSafe class OkHttpContext implements RequestContext { /** * TODO: * add tests, need to add a general RequestContext test framework * make sure submitted requests are not altered * set follow redirects in constructor? */ /** * same as {@link #create(OkHttpClient, boolean) create(client, false)}. */ public static OkHttpContext create(OkHttpClient client) { return new OkHttpContext(client, false); } /** * Creates a new {@code OkHttpContext} using the supplied * {@link OkHttpClient} to execute requests. If {@code debug} is true, * simple debug messages will be printed for all submitted requests. * <p> * A {@link OkHttpClient#clone() copy} of {@code client} is made; therefore, * any subsequent changes made to {@code client} will have no effect on the * returned {@code OkHttpContext}. * * @see #client() */ public static OkHttpContext create(OkHttpClient client, boolean debug) { return new OkHttpContext(client, debug); } final OkHttpClient client; final boolean debug; /** * true when shutdown through {@link #shutdown()} */ @GuardedBy("monitor") volatile boolean shutdown; /** * the current number of active requests */ @GuardedBy("monitor") int requests; final Monitor monitor = new Monitor(); final Monitor.Guard terminated = new Monitor.Guard(monitor) { @Override public boolean isSatisfied() { return shutdown && requests <= 0; } }; final Runnable decrementRequests = new Runnable() { @Override public void run() { monitor.enter(); try { requests--; } finally { monitor.leave(); } } }; private OkHttpContext(@NonNull OkHttpClient client, boolean debug) { //todo: always set no follow redirects, rather should the user be allowed //to set this? this will effect the request creation/submisstion //document info about the cookie handler client = client.clone(); client.setFollowRedirects(false); if (client.getCookieHandler() == null) { client.setCookieHandler(new CookieManager()); } this.client = client; this.debug = debug; } /** * Returns a {@link OkHttpClient#clone() copy} of the underlying * {@code OkHttpClient}. Changes made to the returned {@code OkHttpClient} * will have no effect on this {@code OkHttpContext}. */ public OkHttpClient client() { return client.clone(); } /** * Determines if this {@code OkHttpContext} has been configured to print * simple debug messages for all submitted requests. */ public boolean debug() { return debug; } private void incrementRequests() throws InterruptedException { monitor.enterInterruptibly(); try { requests++; } finally { monitor.leave(); } } /** * Shuts down this {@code OkHttpContext}. Already submitted requests are * allowed to complete, but any additional requests submitted through * {@link #apply(Request) apply} will be rejected, throwing a * {@code RejectedExecutionException}. This has no effect on the underlying * {@link #client() OkHttpClient}, which can still be used externally to * this {@code OkHttpContext}. */ public void shutdown() { monitor.enter(); try { shutdown = true; } finally { monitor.leave(); } } /** * Determines if this {@code OkHttpContext} has been shutdown through * {@link #shutdown()}. * * @return true if shutdown */ public boolean isShutdown() { return shutdown; } /** * Waits until {@link #isShutdown() shutdown} and there are no * {@link #activeRequests() active requests}. * * @see #isTerminated() * @return true if {@link #isTerminated() terminated}, false if the timeout * was reached before termination */ public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { return monitor.waitFor(terminated, timeout, unit); } /** * Determines if this {@code OkHttpContext} has been * {@link #isShutdown() shutdown} and there are no * {@link #activeRequests() active requests}. * * @see #awaitTermination(long, TimeUnit) * @return true if terminated */ public boolean isTerminated() { monitor.enter(); try { return terminated.isSatisfied(); } finally { monitor.leave(); } } /** * Returns the number of active requests that have not yet completed. */ public int activeRequests() { monitor.enter(); try { return requests; } finally { monitor.leave(); } } private void checkNotShutdown() { if (shutdown) { throw new RejectedExecutionException("OkHttpContext shutdown"); } } @Override public ListenableFuture<Response> apply(Request request) throws Exception { RequestRunner runner = new RequestRunner(request); runner.submit(); return runner.future; } static final Function<String, String> notNullTrimUpper = StringUtils.stringFunction().notNull().trim() .uppercaseAscii().get(); static final Function<String, String> notNullTrimLower = StringUtils.stringFunction().notNull().trim() .lowercaseAscii().get(); static final String CONTENT_TYPE = notNullTrimLower.apply(HttpHeaders.CONTENT_TYPE); static final String CONTENT_LENGTH = notNullTrimLower.apply(HttpHeaders.CONTENT_LENGTH); static @Nullable RequestBody createRequestBody(Request request) throws IOException { if (!request.hasBody()) { return null; } final com.tinspx.util.net.RequestBody body = request.body().get(); String contentType = null; long len = -1; for (Map.Entry<String, String> header : body.headers().entries()) { String normalized = notNullTrimLower.apply(header.getKey()); if (CONTENT_TYPE.equals(normalized)) { contentType = header.getValue().trim(); } else if (CONTENT_LENGTH.equals(normalized)) { //invalid length will throw NumberFormatException //TODO: ignore this header and just use the body.size()? //maybe only use the header if hasKnownSize is false len = Long.parseLong(header.getValue().trim()); checkState(len >= 0, "%s has an invalid Content-Length (%s)", body, len); } else { request.addHeader(header.getKey(), header.getValue()); } } //content-type is not required final MediaType mediaType; if (contentType != null) { mediaType = MediaType.parse(contentType); checkState(mediaType != null, "%s has an invalid Content-Type (%s)", body, contentType); } else { mediaType = null; } final long contentLength = len; return new RequestBody() { final Object lock = new Object(); /** * saving the exception is a hack that should work until okhttp * fixes RequestBody. */ @GuardedBy("lock") IOException last; @Override public long contentLength() { if (contentLength >= 0) { return contentLength; } try { if (body.hasKnownSize()) { return body.size(); } } catch (IOException ex) { log.error("polling size of {}", body, ex); synchronized (lock) { last = ex; } } return -1; } @Override public MediaType contentType() { return mediaType; } @Override public void writeTo(BufferedSink sink) throws IOException { synchronized (lock) { if (last != null) { IOException ex = last; last = null; throw ex; } } body.copyTo(sink.outputStream()); } }; } /** * heavily influenced by {@code URLConnectionContext.RequestRunner} */ class RequestRunner implements Callback { /** * The request being executed */ final Request r; /** * future returned the user */ final SettableFuture<Response> future; /** * the internal future used set the result/exception; an internal future * is kept so that {@link #future} can be canceled and not effect this * future */ final SettableFuture<Response> internal; /** * true if this is the first request being executed, false if this is * a redirection of the first request */ final boolean first; /** * set when a redirection is necessary */ @Nullable Request redirect; /** * the response of {@link #r} */ @Nullable Response response; /** * true if the response has a body and the body was not fully read due * to cancellation */ boolean stoppedEarly; /** * saved Call used to cancel the request if cancelled */ Call call; /** * true when either onResponse or onFailure callback has been invoked */ @GuardedBy("this") boolean called; RequestRunner(Request request) { r = checkNotNull(request); future = SettableFuture.create(); internal = SettableFuture.create(); FutureUtils.linkFutures(internal, future); first = true; } RequestRunner(RequestRunner runner) { r = checkNotNull(runner.redirect); future = runner.future; internal = runner.internal; first = false; } private boolean isCancelled() { return future.isCancelled() || r.isCancelled(); } public void submit() throws Exception { boolean enqueued = false; Throwable error = null; try { enqueued = doSubmit(); } catch (Throwable t) { error = t; } if (!enqueued) { if (error != null) { doFailure(error); } else { assert isCancelled(); cancelAndSet(first); } } if (first && error != null) { Throwables.propagateIfInstanceOf(error, Exception.class); throw Throwables.propagate(error); } } /** * submits the request and returns true if the request was enqueued */ private boolean doSubmit() throws Exception { r.start(OkHttpContext.this); r.setSubmissionMillisIfAbsent(); checkNotShutdown(); if (isCancelled() || (!first && r.onRedirect()) || r.onSetup() || future.isCancelled()) { return false; //cancelled } if (debug) { log.info("{}: {}", first ? "connect" : "redirect", r); } //now build the request com.squareup.okhttp.Request.Builder b = new com.squareup.okhttp.Request.Builder(); b.url(r.uri().toURL()); if (!Strings.isNullOrEmpty(r.tag())) { b.tag(r.tag()); } else { b.tag(r.id()); } b.method(notNullTrimUpper.apply(r.method()), createRequestBody(r)); for (Map.Entry<String, String> header : r.headers()) { b.addHeader(header.getKey(), header.getValue()); } @SuppressWarnings("LocalVariableHidesMemberVariable") OkHttpClient client = null; if (r.hasCookieHandler()) { if (client == null) { client = OkHttpContext.this.client.clone(); } client.setCookieHandler(r.cookieHandler().get()); } else { r.cookieHandler(OkHttpContext.this.client.getCookieHandler()); } if (Boolean.TRUE.equals(r.get(Request.HIDE_REDIRECTS))) { if (client == null) { client = OkHttpContext.this.client.clone(); } client.setFollowRedirects(true); } int timeout = asInt(r.get(Request.CONNET_TIMEOUT_MILLIS)); if (timeout >= 0) { if (client == null) { client = OkHttpContext.this.client.clone(); } client.setConnectTimeout(timeout, TimeUnit.MILLISECONDS); } timeout = asInt(r.get(Request.WRITE_TIMEOUT_MILLIS)); if (timeout >= 0) { if (client == null) { client = OkHttpContext.this.client.clone(); } client.setWriteTimeout(timeout, TimeUnit.MILLISECONDS); } timeout = asInt(r.get(Request.READ_TIMEOUT_MILLIS)); if (timeout >= 0) { if (client == null) { client = OkHttpContext.this.client.clone(); } client.setReadTimeout(timeout, TimeUnit.MILLISECONDS); } if (OkHttpContext.this.client.getCache() != null && Boolean.FALSE.equals(r.get(Request.USE_CACHE))) { if (client == null) { client = OkHttpContext.this.client.clone(); } client.setCache(null); } if (first && r.asClass().isPresent()) { //only send this error on the first request r.onError(Errors.message("OkHttpContext does not support asClass(%s)", r.asClass().get().getClass().getName())); } if (client == null) { client = OkHttpContext.this.client; } call = client.newCall(b.build()); if (first) { incrementRequests(); internal.addListener(decrementRequests, MoreExecutors.directExecutor()); } call.enqueue(this); r.addCancelListener(new Runnable() { @Override public void run() { synchronized (RequestRunner.this) { if (called) { return; } } call.cancel(); //schedule cancellation because callbacks may have long //long running code scheduleCancellation(); } }); future.addListener(new Runnable() { @Override public void run() { if (internal.isDone()) { return; } //internal has not completed so the future must be cancelled synchronized (RequestRunner.this) { if (called) { return; } } call.cancel(); scheduleCancellation(); } }, MoreExecutors.directExecutor()); return true; } private void scheduleCancellation() { try { client.getDispatcher().getExecutorService().execute(new Runnable() { @Override public void run() { synchronized (RequestRunner.this) { if (called) { return; } } cancelAndSet(false); } }); } catch (Throwable t) { log.error("cancelling {}", r, t); cancelAndSet(false); } } @Override public void onFailure(com.squareup.okhttp.Request request, IOException e) { synchronized (this) { called = true; } doFailure(e); } @Override public void onResponse(com.squareup.okhttp.Response okr) { synchronized (this) { called = true; } try { processResponse(okr); } catch (Throwable t) { doFailure(t); } } public void processResponse(com.squareup.okhttp.Response okr) throws Exception { final Closer closer = Closer.create(); try { closer.register(okr.body()); buildResponse(okr); if (okr.body() != null) { if (isCancelled()) { stoppedEarly = true; } else { doRead(okr.body().byteStream()); } } if (stoppedEarly) { assert isCancelled(); cancelAndSet(false); } else { if (redirect == null) { buildRedirect(); } if (redirect != null) { if (isCancelled()) { //redirect pending but cancelled cancelAndSet(false, "redirect pending"); } else { new RequestRunner(this).submit(); } } else { doSuccess(); internal.set(response); } } } catch (Throwable t) { throw closer.rethrow(t, Exception.class); } finally { closer.close(); } } /** * converts the okhttp response into tinspx Response, but does not * process the response body. */ private Response buildResponse(com.squareup.okhttp.Response okr) throws IOException { assert response == null : response; response = r.response(okr.request().uri(), okr.code(), okr.message()); final int len = okr.headers().size(); for (int i = 0; i < len; i++) { response.headers().add(okr.headers().name(i), okr.headers().value(i)); } return response; } private void buildRedirect() throws IOException { assert response != null; if (r.redirectHandler().shouldRedirect(response)) { redirect = r.redirectHandler().apply(response.redirect()); } } /** * Reads {@code in} and produces all of the necessary * {@link ContentCallback} events. {@code in} must be the raw input * stream and should not be wrapped in some filtering stream. */ @SuppressWarnings("ThrowFromFinallyBlock") void doRead(final @WillClose InputStream in) throws Exception { boolean complete = false; List<RequestCallback> callbacks = Collections.emptyList(); final Closer closer = Closer.create(); try { closer.register(in); //ensure cleanup even if assertions fail assert in != null; assert response != null; boolean cancelled = false; if (r.hasCallbacks()) { callbacks = Lists.newArrayListWithCapacity(Math.max(4, r.callbacks().size() + 2)); Set<RequestCallback> processed = Sets.newHashSet(); boolean redirectBuilt = false; for (RequestCallback rc : r.callbacks()) { processed.add(rc); boolean start = true; if (rc.ignoreRedirects()) { if (!redirectBuilt) { redirectBuilt = true; buildRedirect(); } start = redirect == null; } if (start && rc.onContentStart(response)) { callbacks.add(rc); } if (cancelled = isCancelled()) { break; } } if (!cancelled) { //allow additional callbacks to be added in onContentStart for (RequestCallback rc : r.callbacks()) { if (processed.contains(rc)) { continue; } boolean start = true; if (rc.ignoreRedirects()) { if (!redirectBuilt) { redirectBuilt = true; buildRedirect(); } start = redirect == null; } if (start && rc.onContentStart(response)) { callbacks.add(rc); } if (cancelled = isCancelled()) { break; } } } } if (cancelled) { stoppedEarly = true; } else { doRead(response.headers().contentLength(), in, callbacks); } complete = true; } catch (Throwable t) { throw closer.rethrow(t, Exception.class); } finally { //if onContentStart has been called, onContentComplete MUST be called Throwable error = null; for (RequestCallback rc : callbacks) { try { rc.onContentComplete(response); } catch (Throwable t) { error = t; log.error("onContentComplete: {}", response, t); } } closer.close(); //may throw IOException if (complete && error != null) { Throwables.propagateIfInstanceOf(error, Exception.class); throw Throwables.propagate(error); } } } /** * Reads {@code in} and sends the * {@link RequestCallback#onContent(Response, ByteBuffer) onContent} * events. Returns true if {@code in} was fully read. The stream may not * be fully read if there are no callbacks and both * {@link Request#asByteSource()} and {@link Request#asCharSequence()} * are false. * * @param length the content length header value, must be nonnegative * @param compressed true if {@code in} is decompressing stream * @param in the stream to read * @param callbacks non-null, possibly empty, list of callbacks. * callbacks that return false from onContent will be removed * @return true if {@code in} was fully read, false if there still may * be some bytes available * @throws IOException */ boolean doRead(long length, @WillNotClose InputStream in, List<RequestCallback> callbacks) throws IOException { assert length >= 0 : length; final boolean canBuffer = length <= Integer.MAX_VALUE; if (canBuffer && in instanceof InflaterInputStream && response.isText()) { length = Ints.saturatedCast(length * 4); //25% compression } BAOutputStream buffer = null; if (r.asByteSource()) { if (canBuffer) { buffer = new BAOutputStream(length > 0 ? (int) length : BUF_SIZE); while (buffer.write(in, WRITE_SIZE) >= WRITE_SIZE) { if (isCancelled()) { //give it one last chance to complete if (buffer.write(in, WRITE_SIZE) >= WRITE_SIZE) { stoppedEarly = true; break; } } } } else { r.onError(Errors.message("content length (%s) is too large to buffer", length)); } } DecodingChannel decoder = null; if (r.asCharSequence()) { if (canBuffer) { decoder = buildDecoder((int) length); } else { r.onError(Errors.message("content length (%s) is too large to decode as CharSequence", length)); } } boolean complete = false; if (buffer != null) { if (decoder != null) { decoder.write(buffer.toByteBuffer()); } if (!callbacks.isEmpty()) { doRead(buffer.toByteBuffer(), callbacks); } complete = !stoppedEarly; } else if (!callbacks.isEmpty()) { if (decoder != null) { callbacks.add(Requests.wrap(new ByteChannelListener(decoder))); } complete = doRead(in, callbacks); } else if (decoder != null) { while (ByteUtils.copy(in, decoder, WRITE_SIZE) >= WRITE_SIZE) { if (isCancelled()) { //give it one last chance to complete if (ByteUtils.copy(in, decoder, WRITE_SIZE) >= WRITE_SIZE) { stoppedEarly = true; break; } } } complete = !stoppedEarly; } if (buffer != null) { response.asByteSource(buffer.asByteSource()); } if (decoder != null) { decoder.close(); response.decodeCharset(decoder.charset()); response.asCharSequence(decoder.content()); } return complete; } /** * Reads {@code in} and sends the onContent events. Will not fully * read the stream if {@code callbacks} is empty or becomes empty. * * @param in the stream to read * @param callbacks non-null, possibly empty, list of callbacks. * callbacks that return false from onContent will be removed * @return true if {@code in} was fully read, false if there still may * be some bytes available * @throws IOException */ boolean doRead(@WillNotClose InputStream in, List<RequestCallback> callbacks) throws IOException { final ByteBuffer buf = BYTE_BUFFER.get(); for (int i = 1; !callbacks.isEmpty(); i++) { //check cancellation every 4th read boolean cancelled = (i & 3) == 0 && isCancelled(); buf.clear(); ByteUtils.copy(in, buf); final boolean eof = buf.hasRemaining(); buf.flip(); if (!buf.hasRemaining()) { return true; } doRead(buf, callbacks); if (eof) { return true; } if (cancelled) { stoppedEarly = true; return false; } } return false; } /** * Sends {@code buf} to all {@code callbacks} in their onContent * method. If any callback returns false from onContent, it is * removed from the list. */ void doRead(ByteBuffer buf, List<RequestCallback> callbacks) { final int lim = buf.limit(), pos = buf.position(); Iterator<RequestCallback> iter = callbacks.iterator(); while (iter.hasNext()) { RequestCallback next = iter.next(); if (!next.onContent(response, buf)) { iter.remove(); } buf.limit(lim).position(pos); } } private boolean shouldDetectCharset() { return r.detectCharset() && !response.hasDecodeCharset(); } private DecodingChannel buildDecoder(int expectedSize) { assert response != null; expectedSize = expectedSize > 0 ? expectedSize : BUF_SIZE; Iterable<CharsetDetector> detectors = Collections.emptySet(); boolean detect = shouldDetectCharset(); if (detect) { if (r.hasCharsetDetectors()) { detectors = Iterables.filter(r.charsetDetectors(), CONTENT_PRED); } Optional<CharContentCharDet> cdet = getContentDetector(response); if (cdet.isPresent() && !Iterables.any(detectors, Predicates.instanceOf(cdet.get().getClass()))) { detectors = CollectUtils.concat(detectors, cdet.get()); } if (Iterables.isEmpty(detectors)) { detect = false; } } if (detect) { return DetectingOutputStream.builder().charsetDetectors(detectors) .decoderFunction(r.decoderFunction()).charsetFunction(r.charsetFunction()) .expectedSize(expectedSize).defaultCharset(r.decodeCharset()).build(); } else { return new DecodingOutputStream( r.decoderFunction().apply(response.decodeCharset().or(r.decodeCharset())), expectedSize); } } private void doSuccess() { assert response != null; if (internal.isDone()) { return; } response.onSuccess(false); if (debug) { log.info("complete: {}", response); } } private void doFailure(Throwable error) { assert error != null; if (response == null) { response = r.response(); } if (internal.isDone()) { return; } response.onFailure(error, false); internal.setException(new RequestException(response, error)); if (debug) { log.info("failed: {}", response, error); } } private void cancelAndSet(boolean propagate) { cancelAndSet(propagate, "user cancelled"); } private void cancelAndSet(boolean propagate, String msg) { try { doCancel(propagate); } finally { internal.setException(new RequestException(response, new CancellationException(msg))); } } private void doCancel(boolean propagate) { if (response == null) { response = r.response(); } if (internal.isDone()) { return; } if (debug) { log.info("cancelled: {}", response); } response.onCancel(propagate); } } /** * If obj is a Number and is non-negative, returns the int value, otherwise * returns -1 */ private static int asInt(@Nullable Object obj) { if (obj instanceof Number) { int value = ((Number) obj).intValue(); return value < 0 ? -1 : value; } else { return -1; } } private static Optional<CharContentCharDet> getContentDetector(Response response) { if (response.isHtml()) { return Optional.<CharContentCharDet>of(new HtmlCharDet()); } else if (response.isXml()) { return Optional.<CharContentCharDet>of(new XmlCharDet()); } else { return Optional.absent(); } } @SuppressWarnings("unchecked") private static final Predicate<CharsetDetector> CONTENT_PRED = Predicates .or(Predicates.instanceOf(ByteContentCharDet.class), Predicates.instanceOf(CharContentCharDet.class)); private static final int BUF_SIZE = 1024 * 16; private static final int WRITE_SIZE = BUF_SIZE * 4; private static final ThreadLocal<ByteBuffer> BYTE_BUFFER = new ThreadLocal<ByteBuffer>() { @Override protected ByteBuffer initialValue() { return ByteBuffer.allocate(BUF_SIZE); } }; }