39 | * Client operations are thread safe, the HTTP connection may
40 | * be shared between different threads.
41 | *
42 | * If a response entity is obtained that is an instance of {@link java.io.Closeable}
43 | * then the instance MUST be closed after processing the entity to release
44 | * connection-based resources.
45 | *
46 | * If a {@link ClientResponse} is obtained and an entity is not read from the
47 | * response then {@link ClientResponse#close() } MUST be called after processing
48 | * the response to release connection-based resources.
49 | *
50 | * The following methods are currently supported: HEAD, GET, POST, PUT, DELETE, TRACE
51 | * OPTIONS as well as custom methods.
52 | *
53 | *
54 | * @author Jeanfrancois Arcand
55 | */
56 | public final class AhcClientHandler implements ClientHandler {
57 |
58 | private final AsyncHttpClient client;
59 |
60 | private final AhcConfig config;
61 |
62 | private final AhcRequestWriter requestWriter = new AhcRequestWriter();
63 |
64 | private final List cookies = new ArrayList();
65 |
66 | @Context
67 | private MessageBodyWorkers workers;
68 |
69 | /**
70 | * Create a new root handler with an {@link AsyncHttpClient}.
71 | *
72 | * @param client the {@link AsyncHttpClient}.
73 | */
74 | public AhcClientHandler(final AsyncHttpClient client) {
75 | this(client, new DefaultAhcConfig());
76 | }
77 |
78 | /**
79 | * Create a new root handler with an {@link AsyncHttpClient}.
80 | *
81 | * @param client the {@link AsyncHttpClient}.
82 | * @param config the client configuration.
83 | */
84 | public AhcClientHandler(final AsyncHttpClient client, final AhcConfig config) {
85 | this.client = client;
86 | this.config = config;
87 | }
88 |
89 | /**
90 | * Get the client config.
91 | *
92 | * @return the client config.
93 | */
94 | public AhcConfig getConfig() {
95 | return config;
96 | }
97 |
98 | /**
99 | * Get the {@link AsyncHttpClient}.
100 | *
101 | * @return the {@link AsyncHttpClient}.
102 | */
103 | public AsyncHttpClient getHttpClient() {
104 | return client;
105 | }
106 |
107 | /**
108 | * Translate the {@link ClientRequest} into a AsyncHttpClient request, and execute it.
109 | *
110 | * @param cr the HTTP request.
111 | * @return the {@link ClientResponse}
112 | * @throws ClientHandlerException
113 | */
114 | @Override
115 | public ClientResponse handle(final ClientRequest cr)
116 | throws ClientHandlerException {
117 |
118 | try {
119 | final RequestBuilder requestBuilder = getRequestBuilder(cr);
120 | handleCookie(requestBuilder);
121 | requestWriter.configureRequest(requestBuilder, cr, allowBody(cr.getMethod()));
122 |
123 | final Response response = client.executeRequest(requestBuilder.build()).get();
124 |
125 | applyResponseCookies(response.getCookies());
126 |
127 | final ClientResponse r = new ClientResponse(response.getStatusCode(),
128 | getInBoundHeaders(response),
129 | response.getResponseBodyAsStream(),
130 | workers);
131 | if (!r.hasEntity()) {
132 | r.bufferEntity();
133 | r.close();
134 | }
135 | return r;
136 | } catch (final Exception e) {
137 | throw new ClientHandlerException(e);
138 | }
139 | }
140 |
141 | /**
142 | * append request cookies and override existing cookies
143 | *
144 | * @param responseCookies list of cookies from response
145 | */
146 | private void applyResponseCookies(final List responseCookies) {
147 | if (responseCookies != null) {
148 | for (final Cookie rc : responseCookies) {
149 | // remove existing cookie
150 | final Iterator it = cookies.iterator();
151 | while (it.hasNext()) {
152 | final Cookie c = it.next();
153 | if (isSame(rc, c)) {
154 | it.remove();
155 | break;
156 | }
157 | }
158 | // add new cookie
159 | cookies.add(rc);
160 | }
161 | }
162 | }
163 |
164 | private boolean isSame(final Cookie c, final Cookie o) {
165 | return isEquals(c.getDomain(), o.getDomain()) &&
166 | isEquals(c.getPath(), o.getPath()) &&
167 | isEquals(c.getName(), o.getName());
168 | }
169 |
170 | private boolean isEquals(final Object o, final Object o2) {
171 | return (o == null && o2 == null) || o != null && o.equals(o2);
172 | }
173 |
174 | /**
175 | * Check if a body needs to be constructed based on a method's name.
176 | *
177 | * @param method An HTTP method
178 | * @return true if s body can be allowed.
179 | */
180 | private boolean allowBody(final String method) {
181 | if (method.equalsIgnoreCase("GET") || method.equalsIgnoreCase("OPTIONS")
182 | && method.equalsIgnoreCase("TRACE")
183 | && method.equalsIgnoreCase("HEAD")) {
184 | return false;
185 | } else {
186 | return true;
187 | }
188 | }
189 |
190 | /**
191 | * Return the {@link RequestBuilder} based on a method
192 | *
193 | * @param cr the HTTP request.
194 | * @return {@link RequestBuilder}
195 | */
196 | private RequestBuilder getRequestBuilder(final ClientRequest cr) {
197 | final String strMethod = cr.getMethod();
198 | final String uri = cr.getURI().toString();
199 |
200 | if (strMethod.equals("GET")) {
201 | return new RequestBuilder("GET").setUrl(uri);
202 | } else if (strMethod.equals("POST")) {
203 | return new RequestBuilder("POST").setUrl(uri);
204 | } else if (strMethod.equals("PUT")) {
205 | return new RequestBuilder("PUT").setUrl(uri);
206 | } else if (strMethod.equals("DELETE")) {
207 | return new RequestBuilder("DELETE").setUrl(uri);
208 | } else if (strMethod.equals("HEAD")) {
209 | return new RequestBuilder("HEAD").setUrl(uri);
210 | } else if (strMethod.equals("OPTIONS")) {
211 | return new RequestBuilder("OPTIONS").setUrl(uri);
212 | } else {
213 | return new RequestBuilder(strMethod).setUrl(uri);
214 | }
215 | }
216 |
217 | private InBoundHeaders getInBoundHeaders(final Response response) {
218 | final InBoundHeaders headers = new InBoundHeaders();
219 | final FluentCaseInsensitiveStringsMap respHeaders = response.getHeaders();
220 | for (final Map.Entry> header : respHeaders) {
221 | headers.put(header.getKey(), header.getValue());
222 | }
223 | return headers;
224 | }
225 |
226 | /**
227 | * Return the instance of {@link com.sun.jersey.api.client.RequestWriter}. This instance will be injected
228 | * within Jersey so it cannot be null.
229 | *
230 | * @return the instance of {@link com.sun.jersey.api.client.RequestWriter}.
231 | */
232 | public AhcRequestWriter getAhcRequestWriter() {
233 | return requestWriter;
234 | }
235 |
236 | private void handleCookie(final RequestBuilder requestBuilder) {
237 | for (final Cookie c : cookies) {
238 | requestBuilder.addCookie(c);
239 | }
240 | }
241 |
242 | }
243 |
--------------------------------------------------------------------------------
/src/main/java/org/sonatype/spice/jersey/client/ahc/AhcHttpClient.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2010-2011 Sonatype, Inc.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the Eclipse Public License v1.0
5 | * and Apache License v2.0 which accompanies this distribution.
6 | * The Eclipse Public License is available at
7 | * http://www.eclipse.org/legal/epl-v10.html
8 | * The Apache License v2.0 is available at
9 | * http://www.apache.org/licenses/LICENSE-2.0.html
10 | * You may elect to redistribute this code under either of these licenses.
11 | *******************************************************************************/
12 | package org.sonatype.spice.jersey.client.ahc;
13 |
14 | import org.sonatype.spice.jersey.client.ahc.config.AhcConfig;
15 | import org.sonatype.spice.jersey.client.ahc.config.DefaultAhcConfig;
16 |
17 | import com.ning.http.client.AsyncHttpClient;
18 | import com.sun.jersey.api.client.Client;
19 | import com.sun.jersey.api.client.config.ClientConfig;
20 | import com.sun.jersey.core.spi.component.ioc.IoCComponentProviderFactory;
21 |
22 | /**
23 | * A {@link Client} that utilizes the AsyncHttpClient to send and receive
24 | * HTTP request and responses.
25 | *
26 | * If an {@link AhcClientHandler} is not explicitly passed as a
27 | * constructor or method parameter then by default an instance is created with
28 | * an {@link AsyncHttpClient} constructed
29 | *
30 | *
31 | * If a response entity is obtained that is an instance of
32 | * {@link java.io.Closeable}
33 | * then the instance MUST be closed after processing the entity to release
34 | * connection-based resources.
35 | *
36 | * If a {@link com.sun.jersey.api.client.ClientResponse} is obtained and an
37 | * entity is not read from the response then
38 | * {@link com.sun.jersey.api.client.ClientResponse#close() } MUST be called
39 | * after processing the response to release connection-based resources.
40 | *
41 | * @author Jeanfrancois Arcand
42 | */
43 | public class AhcHttpClient extends Client {
44 |
45 | private final AhcClientHandler clientHandler;
46 |
47 | /**
48 | * Create a new client instance.
49 | *
50 | */
51 | public AhcHttpClient() {
52 | this(createDefaultClientHander(new DefaultAhcConfig()));
53 | }
54 |
55 | /**
56 | * Create a new client instance.
57 | *
58 | * @param root the root client handler for dispatching a request and
59 | * returning a response.
60 | */
61 | public AhcHttpClient(final AhcClientHandler root) {
62 | this(root, null);
63 | }
64 |
65 | /**
66 | * Create a new instance with a client configuration and a
67 | * component provider.
68 | *
69 | * @param root the root client handler for dispatching a request and
70 | * returning a response.
71 | * @param config the client configuration.
72 | * @param provider the IoC component provider factory.
73 | * @deprecated the config parameter is no longer utilized and instead
74 | * the config obtained from the {@link AhcClientHandler#getConfig() }
75 | * is utilized instead.
76 | */
77 | @Deprecated
78 | public AhcHttpClient(final AhcClientHandler root, final ClientConfig config,
79 | final IoCComponentProviderFactory provider) {
80 | this(root, provider);
81 | }
82 |
83 | /**
84 | * Create a new instance with a client configuration and a
85 | * component provider.
86 | *
87 | * @param root the root client handler for dispatching a request and
88 | * returning a response.
89 | * @param provider the IoC component provider factory.
90 | */
91 | public AhcHttpClient(final AhcClientHandler root,
92 | final IoCComponentProviderFactory provider) {
93 | super(root, root.getConfig(), provider);
94 |
95 | this.clientHandler = root;
96 | inject(this.clientHandler.getAhcRequestWriter());
97 | }
98 |
99 | /**
100 | * Get the AsyncHttpClient client handler.
101 | *
102 | * @return the AsyncHttpClient client handler.
103 | */
104 | public AhcClientHandler getClientHandler() {
105 | return clientHandler;
106 | }
107 |
108 | /**
109 | * Create a default client.
110 | *
111 | * @return a default client.
112 | */
113 | public static AhcHttpClient create() {
114 | return create(new DefaultAhcConfig());
115 | }
116 |
117 | /**
118 | * Create a default client with client configuration.
119 | *
120 | * @param cc the client configuration.
121 | * @return a default client.
122 | */
123 | public static AhcHttpClient create(final ClientConfig cc) {
124 | return create(cc, null);
125 | }
126 |
127 | /**
128 | * Create a default client with client configuration and component provider.
129 | *
130 | * @param cc the client configuration.
131 | * @param provider the IoC component provider factory.
132 | * @return a default client.
133 | */
134 | public static AhcHttpClient create(final ClientConfig cc, final IoCComponentProviderFactory provider) {
135 | return new AhcHttpClient(createDefaultClientHander(cc), provider);
136 | }
137 |
138 | @Override
139 | public void destroy(){
140 | try{
141 | clientHandler.getHttpClient().close();
142 | } finally {
143 | super.destroy();
144 | }
145 | }
146 |
147 | @Override
148 | protected void finalize(){
149 | try {
150 | // Do not close the AHCClient.
151 | super.destroy();
152 | } finally {
153 | try {
154 | super.finalize();
155 | } catch (final Throwable e) {
156 | // TODO swallow?
157 | }
158 | }
159 | }
160 |
161 | /**
162 | * Create a default AsyncHttpClient client handler.
163 | *
164 | * @return a default AsyncHttpClient client handler.
165 | */
166 | private static AhcClientHandler createDefaultClientHander(final ClientConfig cc) {
167 |
168 | if (AhcConfig.class.isAssignableFrom(cc.getClass()) || DefaultAhcConfig.class.isAssignableFrom(cc.getClass())) {
169 | final AhcConfig c = AhcConfig.class.cast(cc);
170 | return new AhcClientHandler(new AsyncHttpClient(c.getAsyncHttpClientConfigBuilder().build()), c);
171 | } else {
172 | throw new IllegalStateException("Client Config Type not supported");
173 | }
174 | }
175 |
176 | @Override
177 | public void setFollowRedirects(final Boolean redirect) {
178 | clientHandler.getConfig().getAsyncHttpClientConfigBuilder().setFollowRedirects(redirect);
179 | }
180 |
181 | @Override
182 | public void setReadTimeout(final Integer interval) {
183 | clientHandler.getConfig().getAsyncHttpClientConfigBuilder().setRequestTimeoutInMs(interval);
184 | }
185 |
186 | @Override
187 | public void setConnectTimeout(final Integer interval) {
188 | clientHandler.getConfig().getAsyncHttpClientConfigBuilder().setConnectionTimeoutInMs(interval);
189 | }
190 | }
--------------------------------------------------------------------------------
/src/main/java/org/sonatype/spice/jersey/client/ahc/AhcRequestWriter.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2010-2011 Sonatype, Inc.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the Eclipse Public License v1.0
5 | * and Apache License v2.0 which accompanies this distribution.
6 | * The Eclipse Public License is available at
7 | * http://www.eclipse.org/legal/epl-v10.html
8 | * The Apache License v2.0 is available at
9 | * http://www.apache.org/licenses/LICENSE-2.0.html
10 | * You may elect to redistribute this code under either of these licenses.
11 | *******************************************************************************/
12 | package org.sonatype.spice.jersey.client.ahc;
13 |
14 | import static com.sun.jersey.api.client.ClientRequest.getHeaderValue;
15 |
16 | import java.io.ByteArrayOutputStream;
17 | import java.io.IOException;
18 | import java.io.OutputStream;
19 | import java.util.List;
20 | import java.util.Map;
21 |
22 | import javax.ws.rs.core.MultivaluedMap;
23 |
24 | import org.sonatype.spice.jersey.client.ahc.config.AhcConfig;
25 |
26 | import com.ning.http.client.PerRequestConfig;
27 | import com.ning.http.client.Request;
28 | import com.ning.http.client.RequestBuilder;
29 | import com.sun.jersey.api.client.ClientHandlerException;
30 | import com.sun.jersey.api.client.ClientRequest;
31 | import com.sun.jersey.api.client.CommittingOutputStream;
32 | import com.sun.jersey.api.client.RequestWriter;
33 |
34 | /**
35 | * An implementation of {@link RequestWriter} that also configure the AHC {@link RequestBuilder}
36 | *
37 | * @author Jeanfrancois Arcand
38 | */
39 | public class AhcRequestWriter extends RequestWriter {
40 |
41 | public void configureRequest(final RequestBuilder requestBuilder, final ClientRequest cr, final boolean needsBody) {
42 | final Map props = cr.getProperties();
43 |
44 | // Set the read timeout
45 | final Integer readTimeout = (Integer) props.get(AhcConfig.PROPERTY_READ_TIMEOUT);
46 | if (readTimeout != null) {
47 | final PerRequestConfig c = new PerRequestConfig();
48 | c.setRequestTimeoutInMs(readTimeout);
49 | requestBuilder.setPerRequestConfig(c);
50 | }
51 | if (cr.getEntity() != null && needsBody) {
52 | final RequestEntityWriter re = getRequestEntityWriter(cr);
53 |
54 | final ByteArrayOutputStream baos = new ByteArrayOutputStream();
55 | try {
56 | re.writeRequestEntity(new CommittingOutputStream(baos) {
57 | @Override
58 | protected void commit() throws IOException {
59 | configureHeaders(cr.getHeaders(), requestBuilder);
60 | }
61 | });
62 | } catch (final IOException ex) {
63 | throw new ClientHandlerException(ex);
64 | }
65 |
66 | final byte[] content = baos.toByteArray();
67 | requestBuilder.setBody(new Request.EntityWriter() {
68 | @Override
69 | public void writeEntity(final OutputStream out) throws IOException {
70 | out.write(content);
71 | }
72 | });
73 | } else {
74 | configureHeaders(cr.getHeaders(), requestBuilder);
75 | }
76 | }
77 |
78 | private void configureHeaders(final MultivaluedMap metadata, final RequestBuilder requestBuilder) {
79 | for (final Map.Entry> e : metadata.entrySet()) {
80 | final List