// // ======================================================================== // Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // // You may elect to redistribute this code under either of these licenses. // ======================================================================== // package org.eclipse.jetty.client; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.jetty.client.security.SecurityListener; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeaders; import org.eclipse.jetty.http.HttpMethods; import org.eclipse.jetty.http.HttpSchemes; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.HttpVersions; import org.eclipse.jetty.io.Buffer; import org.eclipse.jetty.io.BufferCache.CachedBuffer; import org.eclipse.jetty.io.ByteArrayBuffer; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.thread.Timeout; /** *

* An HTTP client API that encapsulates an exchange (a request and its response) with a HTTP server. *

* * This object encapsulates: * * *

* The HttpExchange class is intended to be used by a developer wishing to have close asynchronous interaction with the the exchange.
* Typically a developer will extend the HttpExchange class with a derived class that overrides some or all of the onXxx callbacks.
* There are also some predefined HttpExchange subtypes that can be used as a basis, see {@link org.eclipse.jetty.client.ContentExchange} and * {@link org.eclipse.jetty.client.CachedExchange}. *

* *

* Typically the HttpExchange is passed to the {@link HttpClient#send(HttpExchange)} method, which in turn selects a {@link HttpDestination} and calls its * {@link HttpDestination#send(HttpExchange)}, which then creates or selects a {@link AbstractHttpConnection} and calls its {@link AbstractHttpConnection#send(HttpExchange)}. A * developer may wish to directly call send on the destination or connection if they wish to bypass some handling provided (eg Cookie handling in the * HttpDestination). *

* *

* In some circumstances, the HttpClient or HttpDestination may wish to retry a HttpExchange (eg. failed pipeline request, authentication retry or redirection). * In such cases, the HttpClient and/or HttpDestination may insert their own HttpExchangeListener to intercept and filter the call backs intended for the * HttpExchange. *

*/ public class HttpExchange { static final Logger LOG = Log.getLogger(HttpExchange.class); public static final int STATUS_START = 0; public static final int STATUS_WAITING_FOR_CONNECTION = 1; public static final int STATUS_WAITING_FOR_COMMIT = 2; public static final int STATUS_SENDING_REQUEST = 3; public static final int STATUS_WAITING_FOR_RESPONSE = 4; public static final int STATUS_PARSING_HEADERS = 5; public static final int STATUS_PARSING_CONTENT = 6; public static final int STATUS_COMPLETED = 7; public static final int STATUS_EXPIRED = 8; public static final int STATUS_EXCEPTED = 9; public static final int STATUS_CANCELLING = 10; public static final int STATUS_CANCELLED = 11; // HTTP protocol fields private String _method = HttpMethods.GET; private Buffer _scheme = HttpSchemes.HTTP_BUFFER; private String _uri; private int _version = HttpVersions.HTTP_1_1_ORDINAL; private Address _address; private final HttpFields _requestFields = new HttpFields(); private Buffer _requestContent; private InputStream _requestContentSource; private AtomicInteger _status = new AtomicInteger(STATUS_START); private boolean _retryStatus = false; // controls if the exchange will have listeners autoconfigured by the destination private boolean _configureListeners = true; private HttpEventListener _listener = new Listener(); private volatile AbstractHttpConnection _connection; private Address _localAddress = null; // a timeout for this exchange private long _timeout = -1; private volatile Timeout.Task _timeoutTask; private long _lastStateChange=System.currentTimeMillis(); private long _sent=-1; private int _lastState=-1; private int _lastStatePeriod=-1; boolean _onRequestCompleteDone; boolean _onResponseCompleteDone; boolean _onDone; // == onConnectionFail || onException || onExpired || onCancelled || onResponseCompleted && onRequestCompleted protected void expire(HttpDestination destination) { AbstractHttpConnection connection = _connection; if (getStatus() < HttpExchange.STATUS_COMPLETED) setStatus(HttpExchange.STATUS_EXPIRED); destination.exchangeExpired(this); if (connection != null) connection.exchangeExpired(this); } public int getStatus() { return _status.get(); } /** * @param status * the status to wait for * @throws InterruptedException * if the waiting thread is interrupted * @deprecated Use {@link #waitForDone()} instead */ @Deprecated public void waitForStatus(int status) throws InterruptedException { throw new UnsupportedOperationException(); } /** * Wait until the exchange is "done". Done is defined as when a final state has been passed to the HttpExchange via the associated onXxx call. Note that an * exchange can transit a final state when being used as part of a dialog (eg {@link SecurityListener}. Done status is thus defined as: * *
     * done == onConnectionFailed || onException || onExpire || onRequestComplete && onResponseComplete
     * 
* * @return the done status * @throws InterruptedException */ public int waitForDone() throws InterruptedException { synchronized (this) { while (!isDone()) this.wait(); return _status.get(); } } public void reset() { // TODO - this should do a cancel and wakeup everybody that was waiting. // might need a version number concept synchronized (this) { _timeoutTask = null; _onRequestCompleteDone = false; _onResponseCompleteDone = false; _onDone = false; setStatus(STATUS_START); } } /* ------------------------------------------------------------ */ /** * @param newStatus * @return True if the status was actually set. */ boolean setStatus(int newStatus) { boolean set = false; try { int oldStatus = _status.get(); boolean ignored = false; if (oldStatus != newStatus) { long now = System.currentTimeMillis(); _lastStatePeriod=(int)(now-_lastStateChange); _lastState=oldStatus; _lastStateChange=now; if (newStatus==STATUS_SENDING_REQUEST) _sent=_lastStateChange; } // State machine: from which old status you can go into which new status switch (oldStatus) { case STATUS_START: switch (newStatus) { case STATUS_START: case STATUS_WAITING_FOR_CONNECTION: case STATUS_WAITING_FOR_COMMIT: case STATUS_CANCELLING: case STATUS_EXCEPTED: set = _status.compareAndSet(oldStatus,newStatus); break; case STATUS_EXPIRED: set = setStatusExpired(newStatus,oldStatus); break; } break; case STATUS_WAITING_FOR_CONNECTION: switch (newStatus) { case STATUS_WAITING_FOR_COMMIT: case STATUS_CANCELLING: case STATUS_EXCEPTED: set = _status.compareAndSet(oldStatus,newStatus); break; case STATUS_EXPIRED: set = setStatusExpired(newStatus,oldStatus); break; } break; case STATUS_WAITING_FOR_COMMIT: switch (newStatus) { case STATUS_SENDING_REQUEST: case STATUS_CANCELLING: case STATUS_EXCEPTED: set = _status.compareAndSet(oldStatus,newStatus); break; case STATUS_EXPIRED: set = setStatusExpired(newStatus,oldStatus); break; } break; case STATUS_SENDING_REQUEST: switch (newStatus) { case STATUS_WAITING_FOR_RESPONSE: if (set = _status.compareAndSet(oldStatus,newStatus)) getEventListener().onRequestCommitted(); break; case STATUS_CANCELLING: case STATUS_EXCEPTED: set = _status.compareAndSet(oldStatus,newStatus); break; case STATUS_EXPIRED: set = setStatusExpired(newStatus,oldStatus); break; } break; case STATUS_WAITING_FOR_RESPONSE: switch (newStatus) { case STATUS_PARSING_HEADERS: case STATUS_CANCELLING: case STATUS_EXCEPTED: set = _status.compareAndSet(oldStatus,newStatus); break; case STATUS_EXPIRED: set = setStatusExpired(newStatus,oldStatus); break; } break; case STATUS_PARSING_HEADERS: switch (newStatus) { case STATUS_PARSING_CONTENT: if (set = _status.compareAndSet(oldStatus,newStatus)) getEventListener().onResponseHeaderComplete(); break; case STATUS_CANCELLING: case STATUS_EXCEPTED: set = _status.compareAndSet(oldStatus,newStatus); break; case STATUS_EXPIRED: set = setStatusExpired(newStatus,oldStatus); break; } break; case STATUS_PARSING_CONTENT: switch (newStatus) { case STATUS_COMPLETED: if (set = _status.compareAndSet(oldStatus,newStatus)) getEventListener().onResponseComplete(); break; case STATUS_CANCELLING: case STATUS_EXCEPTED: set = _status.compareAndSet(oldStatus,newStatus); break; case STATUS_EXPIRED: set = setStatusExpired(newStatus,oldStatus); break; } break; case STATUS_COMPLETED: switch (newStatus) { case STATUS_START: case STATUS_EXCEPTED: case STATUS_WAITING_FOR_RESPONSE: set = _status.compareAndSet(oldStatus,newStatus); break; case STATUS_CANCELLING: case STATUS_EXPIRED: // Don't change the status, it's too late ignored = true; break; } break; case STATUS_CANCELLING: switch (newStatus) { case STATUS_EXCEPTED: case STATUS_CANCELLED: if (set = _status.compareAndSet(oldStatus,newStatus)) done(); break; default: // Ignore other statuses, we're cancelling ignored = true; break; } break; case STATUS_EXCEPTED: case STATUS_EXPIRED: case STATUS_CANCELLED: switch (newStatus) { case STATUS_START: set = _status.compareAndSet(oldStatus,newStatus); break; case STATUS_COMPLETED: ignored = true; done(); break; default: ignored = true; break; } break; default: // Here means I allowed to set a state that I don't recognize throw new AssertionError(oldStatus + " => " + newStatus); } if (!set && !ignored) throw new IllegalStateException(toState(oldStatus) + " => " + toState(newStatus)); LOG.debug("setStatus {} {}",newStatus,this); } catch (IOException x) { LOG.warn(x); } return set; } private boolean setStatusExpired(int newStatus, int oldStatus) { boolean set; if (set = _status.compareAndSet(oldStatus,newStatus)) getEventListener().onExpire(); return set; } public boolean isDone() { synchronized (this) { return _onDone; } } /** * @deprecated */ @Deprecated public boolean isDone(int status) { return isDone(); } public HttpEventListener getEventListener() { return _listener; } public void setEventListener(HttpEventListener listener) { _listener = listener; } public void setTimeout(long timeout) { _timeout = timeout; } public long getTimeout() { return _timeout; } /** * @param url * an absolute URL (for example 'http://localhost/foo/bar?a=1') */ public void setURL(String url) { setURI(URI.create(url)); } /** * @param address * the address of the server */ public void setAddress(Address address) { _address = address; } /** * @return the address of the server */ public Address getAddress() { return _address; } /** * the local address used by the connection * * Note: this method will not be populated unless the exchange has been executed by the HttpClient * * @return the local address used for the running of the exchange if available, null otherwise. */ public Address getLocalAddress() { return _localAddress; } /** * @param scheme * the scheme of the URL (for example 'http') */ public void setScheme(Buffer scheme) { _scheme = scheme; } /** * @param scheme * the scheme of the URL (for example 'http') */ public void setScheme(String scheme) { if (scheme != null) { if (HttpSchemes.HTTP.equalsIgnoreCase(scheme)) setScheme(HttpSchemes.HTTP_BUFFER); else if (HttpSchemes.HTTPS.equalsIgnoreCase(scheme)) setScheme(HttpSchemes.HTTPS_BUFFER); else setScheme(new ByteArrayBuffer(scheme)); } } /** * @return the scheme of the URL */ public Buffer getScheme() { return _scheme; } /** * @param version * the HTTP protocol version as integer, 9, 10 or 11 for 0.9, 1.0 or 1.1 */ public void setVersion(int version) { _version = version; } /** * @param version * the HTTP protocol version as string */ public void setVersion(String version) { CachedBuffer v = HttpVersions.CACHE.get(version); if (v == null) _version = 10; else _version = v.getOrdinal(); } /** * @return the HTTP protocol version as integer * @see #setVersion(int) */ public int getVersion() { return _version; } /** * @param method * the HTTP method (for example 'GET') */ public void setMethod(String method) { _method = method; } /** * @return the HTTP method */ public String getMethod() { return _method; } /** * @return request URI * @see #getRequestURI() * @deprecated */ @Deprecated public String getURI() { return getRequestURI(); } /** * @return request URI */ public String getRequestURI() { return _uri; } /** * Set the request URI * * @param uri * new request URI * @see #setRequestURI(String) * @deprecated */ @Deprecated public void setURI(String uri) { setRequestURI(uri); } /** * Set the request URI * * Per RFC 2616 sec5, Request-URI = "*" | absoluteURI | abs_path | authority
* where:
*
* "*" - request applies to server itself
* absoluteURI - required for proxy requests, e.g. http://localhost:8080/context
* (this form is generated automatically by HttpClient)
* abs_path - used for most methods, e.g. /context
* authority - used for CONNECT method only, e.g. localhost:8080
*
* For complete definition of URI components, see RFC 2396 sec3.
* * @param uri * new request URI */ public void setRequestURI(String uri) { _uri = uri; } /* ------------------------------------------------------------ */ /** * @param uri * an absolute URI (for example 'http://localhost/foo/bar?a=1') */ public void setURI(URI uri) { if (!uri.isAbsolute()) throw new IllegalArgumentException("!Absolute URI: " + uri); if (uri.isOpaque()) throw new IllegalArgumentException("Opaque URI: " + uri); if (LOG.isDebugEnabled()) LOG.debug("URI = {}",uri.toASCIIString()); String scheme = uri.getScheme(); int port = uri.getPort(); if (port <= 0) port = "https".equalsIgnoreCase(scheme)?443:80; setScheme(scheme); setAddress(new Address(uri.getHost(),port)); HttpURI httpUri = new HttpURI(uri); String completePath = httpUri.getCompletePath(); setRequestURI(completePath == null?"/":completePath); } /** * Adds the specified request header * * @param name * the header name * @param value * the header value */ public void addRequestHeader(String name, String value) { getRequestFields().add(name,value); } /** * Adds the specified request header * * @param name * the header name * @param value * the header value */ public void addRequestHeader(Buffer name, Buffer value) { getRequestFields().add(name,value); } /** * Sets the specified request header * * @param name * the header name * @param value * the header value */ public void setRequestHeader(String name, String value) { getRequestFields().put(name,value); } /** * Sets the specified request header * * @param name * the header name * @param value * the header value */ public void setRequestHeader(Buffer name, Buffer value) { getRequestFields().put(name,value); } /** * @param value * the content type of the request */ public void setRequestContentType(String value) { getRequestFields().put(HttpHeaders.CONTENT_TYPE_BUFFER,value); } /** * @return the request headers */ public HttpFields getRequestFields() { return _requestFields; } /** * @param requestContent * the request content */ public void setRequestContent(Buffer requestContent) { _requestContent = requestContent; } /** * @param stream * the request content as a stream */ public void setRequestContentSource(InputStream stream) { _requestContentSource = stream; if (_requestContentSource != null && _requestContentSource.markSupported()) _requestContentSource.mark(Integer.MAX_VALUE); } /** * @return the request content as a stream */ public InputStream getRequestContentSource() { return _requestContentSource; } public Buffer getRequestContentChunk(Buffer buffer) throws IOException { synchronized (this) { if (_requestContentSource!=null) { if (buffer == null) buffer = new ByteArrayBuffer(8192); // TODO configure int space = buffer.space(); int length = _requestContentSource.read(buffer.array(),buffer.putIndex(),space); if (length >= 0) { buffer.setPutIndex(buffer.putIndex()+length); return buffer; } } return null; } } /** * @return the request content */ public Buffer getRequestContent() { return _requestContent; } /** * @return whether a retry will be attempted or not */ public boolean getRetryStatus() { return _retryStatus; } /** * @param retryStatus * whether a retry will be attempted or not */ public void setRetryStatus(boolean retryStatus) { _retryStatus = retryStatus; } /** * Initiates the cancelling of this exchange. The status of the exchange is set to {@link #STATUS_CANCELLING}. Cancelling the exchange is an asynchronous * operation with respect to the request/response, and as such checking the request/response status of a cancelled exchange may return undefined results * (for example it may have only some of the response headers being sent by the server). The cancelling of the exchange is completed when the exchange * status (see {@link #getStatus()}) is {@link #STATUS_CANCELLED}, and this can be waited using {@link #waitForDone()}. */ public void cancel() { setStatus(STATUS_CANCELLING); abort(); } private void done() { synchronized (this) { disassociate(); _onDone = true; notifyAll(); } } private void abort() { AbstractHttpConnection httpConnection = _connection; if (httpConnection != null) { try { // Closing the connection here will cause the connection // to be returned in HttpConnection.handle() httpConnection.close(); } catch (IOException x) { LOG.debug(x); } finally { disassociate(); } } } void associate(AbstractHttpConnection connection) { if (connection.getEndPoint().getLocalAddr() != null) _localAddress = new Address(connection.getEndPoint().getLocalAddr(),connection.getEndPoint().getLocalPort()); _connection = connection; if (getStatus() == STATUS_CANCELLING) abort(); } boolean isAssociated() { return this._connection != null; } AbstractHttpConnection disassociate() { AbstractHttpConnection result = _connection; this._connection = null; if (getStatus() == STATUS_CANCELLING) setStatus(STATUS_CANCELLED); return result; } public static String toState(int s) { String state; switch (s) { case STATUS_START: state = "START"; break; case STATUS_WAITING_FOR_CONNECTION: state = "CONNECTING"; break; case STATUS_WAITING_FOR_COMMIT: state = "CONNECTED"; break; case STATUS_SENDING_REQUEST: state = "SENDING"; break; case STATUS_WAITING_FOR_RESPONSE: state = "WAITING"; break; case STATUS_PARSING_HEADERS: state = "HEADERS"; break; case STATUS_PARSING_CONTENT: state = "CONTENT"; break; case STATUS_COMPLETED: state = "COMPLETED"; break; case STATUS_EXPIRED: state = "EXPIRED"; break; case STATUS_EXCEPTED: state = "EXCEPTED"; break; case STATUS_CANCELLING: state = "CANCELLING"; break; case STATUS_CANCELLED: state = "CANCELLED"; break; default: state = "UNKNOWN"; } return state; } @Override public String toString() { String state=toState(getStatus()); long now=System.currentTimeMillis(); long forMs = now -_lastStateChange; String s= _lastState>=0 ?String.format("%s@%x=%s//%s%s#%s(%dms)->%s(%dms)",getClass().getSimpleName(),hashCode(),_method,_address,_uri,toState(_lastState),_lastStatePeriod,state,forMs) :String.format("%s@%x=%s//%s%s#%s(%dms)",getClass().getSimpleName(),hashCode(),_method,_address,_uri,state,forMs); if (getStatus()>=STATUS_SENDING_REQUEST && _sent>0) s+="sent="+(now-_sent)+"ms"; return s; } /** */ protected Connection onSwitchProtocol(EndPoint endp) throws IOException { return null; } /** * Callback called when the request headers have been sent to the server. This implementation does nothing. * * @throws IOException * allowed to be thrown by overriding code */ protected void onRequestCommitted() throws IOException { } /** * Callback called when the request and its body have been sent to the server. This implementation does nothing. * * @throws IOException * allowed to be thrown by overriding code */ protected void onRequestComplete() throws IOException { } /** * Callback called when a response status line has been received from the server. This implementation does nothing. * * @param version * the HTTP version * @param status * the HTTP status code * @param reason * the HTTP status reason string * @throws IOException * allowed to be thrown by overriding code */ protected void onResponseStatus(Buffer version, int status, Buffer reason) throws IOException { } /** * Callback called for each response header received from the server. This implementation does nothing. * * @param name * the header name * @param value * the header value * @throws IOException * allowed to be thrown by overriding code */ protected void onResponseHeader(Buffer name, Buffer value) throws IOException { } /** * Callback called when the response headers have been completely received from the server. This implementation does nothing. * * @throws IOException * allowed to be thrown by overriding code */ protected void onResponseHeaderComplete() throws IOException { } /** * Callback called for each chunk of the response content received from the server. This implementation does nothing. * * @param content * the buffer holding the content chunk * @throws IOException * allowed to be thrown by overriding code */ protected void onResponseContent(Buffer content) throws IOException { } /** * Callback called when the entire response has been received from the server This implementation does nothing. * * @throws IOException * allowed to be thrown by overriding code */ protected void onResponseComplete() throws IOException { } /** * Callback called when an exception was thrown during an attempt to establish the connection with the server (for example the server is not listening). * This implementation logs a warning. * * @param x * the exception thrown attempting to establish the connection with the server */ protected void onConnectionFailed(Throwable x) { LOG.warn("CONNECTION FAILED " + this,x); } /** * Callback called when any other exception occurs during the handling of this exchange. This implementation logs a warning. * * @param x * the exception thrown during the handling of this exchange */ protected void onException(Throwable x) { LOG.warn("EXCEPTION " + this,x); } /** * Callback called when no response has been received within the timeout. This implementation logs a warning. */ protected void onExpire() { LOG.warn("EXPIRED " + this); } /** * Callback called when the request is retried (due to failures or authentication). Implementations must reset any consumable content that needs to be sent. * * @throws IOException * allowed to be thrown by overriding code */ protected void onRetry() throws IOException { if (_requestContentSource != null) { if (_requestContentSource.markSupported()) { _requestContent = null; _requestContentSource.reset(); } else { throw new IOException("Unsupported retry attempt"); } } } /** * @return true if the exchange should have listeners configured for it by the destination, false if this is being managed elsewhere * @see #setConfigureListeners(boolean) */ public boolean configureListeners() { return _configureListeners; } /** * @param autoConfigure * whether the listeners are configured by the destination or elsewhere */ public void setConfigureListeners(boolean autoConfigure) { this._configureListeners = autoConfigure; } protected void scheduleTimeout(final HttpDestination destination) { assert _timeoutTask == null; _timeoutTask = new Timeout.Task() { @Override public void expired() { HttpExchange.this.expire(destination); } }; HttpClient httpClient = destination.getHttpClient(); long timeout = getTimeout(); if (timeout > 0) httpClient.schedule(_timeoutTask,timeout); else httpClient.schedule(_timeoutTask); } protected void cancelTimeout(HttpClient httpClient) { Timeout.Task task = _timeoutTask; if (task != null) httpClient.cancel(task); _timeoutTask = null; } private class Listener implements HttpEventListener { public void onConnectionFailed(Throwable ex) { try { HttpExchange.this.onConnectionFailed(ex); } finally { done(); } } public void onException(Throwable ex) { try { HttpExchange.this.onException(ex); } finally { done(); } } public void onExpire() { try { HttpExchange.this.onExpire(); } finally { done(); } } public void onRequestCommitted() throws IOException { HttpExchange.this.onRequestCommitted(); } public void onRequestComplete() throws IOException { try { HttpExchange.this.onRequestComplete(); } finally { synchronized (HttpExchange.this) { _onRequestCompleteDone = true; // Member _onDone may already be true, for example // because the exchange expired or has been canceled _onDone |= _onResponseCompleteDone; if (_onDone) disassociate(); HttpExchange.this.notifyAll(); } } } public void onResponseComplete() throws IOException { try { HttpExchange.this.onResponseComplete(); } finally { synchronized (HttpExchange.this) { _onResponseCompleteDone = true; // Member _onDone may already be true, for example // because the exchange expired or has been canceled _onDone |= _onRequestCompleteDone; if (_onDone) disassociate(); HttpExchange.this.notifyAll(); } } } public void onResponseContent(Buffer content) throws IOException { HttpExchange.this.onResponseContent(content); } public void onResponseHeader(Buffer name, Buffer value) throws IOException { HttpExchange.this.onResponseHeader(name,value); } public void onResponseHeaderComplete() throws IOException { HttpExchange.this.onResponseHeaderComplete(); } public void onResponseStatus(Buffer version, int status, Buffer reason) throws IOException { HttpExchange.this.onResponseStatus(version,status,reason); } public void onRetry() { HttpExchange.this.setRetryStatus(true); try { HttpExchange.this.onRetry(); } catch (IOException e) { LOG.debug(e); } } } /** * @deprecated use {@link org.eclipse.jetty.client.CachedExchange} instead */ @Deprecated public static class CachedExchange extends org.eclipse.jetty.client.CachedExchange { public CachedExchange(boolean cacheFields) { super(cacheFields); } } /** * @deprecated use {@link org.eclipse.jetty.client.ContentExchange} instead */ @Deprecated public static class ContentExchange extends org.eclipse.jetty.client.ContentExchange { } }