1/*
2 * Copyright 2009 Guenther Niess
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *   http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.kenai.jbosh;
18
19import java.io.IOException;
20import java.util.concurrent.locks.Lock;
21import java.util.concurrent.locks.ReentrantLock;
22
23import org.apache.http.HttpEntity;
24import org.apache.http.HttpResponse;
25import org.apache.http.client.HttpClient;
26import org.apache.http.client.methods.HttpPost;
27import org.apache.http.entity.ByteArrayEntity;
28
29import org.apache.http.protocol.BasicHttpContext;
30import org.apache.http.protocol.HttpContext;
31import org.apache.http.util.EntityUtils;
32
33final class ApacheHTTPResponse implements HTTPResponse {
34
35    ///////////////////////////////////////////////////////////////////////////
36    // Constants:
37
38    /**
39     * Name of the accept encoding header.
40     */
41    private static final String ACCEPT_ENCODING = "Accept-Encoding";
42
43    /**
44     * Value to use for the ACCEPT_ENCODING header.
45     */
46    private static final String ACCEPT_ENCODING_VAL =
47            ZLIBCodec.getID() + ", " + GZIPCodec.getID();
48
49    /**
50     * Name of the character set to encode the body to/from.
51     */
52    private static final String CHARSET = "UTF-8";
53
54    /**
55     * Content type to use when transmitting the body data.
56     */
57    private static final String CONTENT_TYPE = "text/xml; charset=utf-8";
58
59    ///////////////////////////////////////////////////////////////////////////
60    // Class variables:
61
62    /**
63     * Lock used for internal synchronization.
64     */
65    private final Lock lock = new ReentrantLock();
66
67    /**
68     * The execution state of an HTTP process.
69     */
70    private final HttpContext context;
71
72    /**
73     * HttpClient instance to use to communicate.
74     */
75    private final HttpClient client;
76
77    /**
78     * The HTTP POST request is sent to the server.
79     */
80    private final HttpPost post;
81
82    /**
83     * A flag which indicates if the transmission was already done.
84     */
85    private boolean sent;
86
87    /**
88     * Exception to throw when the response data is attempted to be accessed,
89     * or {@code null} if no exception should be thrown.
90     */
91    private BOSHException toThrow;
92
93    /**
94     * The response body which was received from the server or {@code null}
95     * if that has not yet happened.
96     */
97    private AbstractBody body;
98
99    /**
100     * The HTTP response status code.
101     */
102    private int statusCode;
103
104    ///////////////////////////////////////////////////////////////////////////
105    // Constructors:
106
107    /**
108     * Create and send a new request to the upstream connection manager,
109     * providing deferred access to the results to be returned.
110     *
111     * @param client client instance to use when sending the request
112     * @param cfg client configuration
113     * @param params connection manager parameters from the session creation
114     *  response, or {@code null} if the session has not yet been established
115     * @param request body of the client request
116     */
117    ApacheHTTPResponse(
118            final HttpClient client,
119            final BOSHClientConfig cfg,
120            final CMSessionParams params,
121            final AbstractBody request) {
122        super();
123        this.client = client;
124        this.context = new BasicHttpContext();
125        this.post = new HttpPost(cfg.getURI().toString());
126        this.sent = false;
127
128        try {
129            String xml = request.toXML();
130            byte[] data = xml.getBytes(CHARSET);
131
132            String encoding = null;
133            if (cfg.isCompressionEnabled() && params != null) {
134                AttrAccept accept = params.getAccept();
135                if (accept != null) {
136                    if (accept.isAccepted(ZLIBCodec.getID())) {
137                        encoding = ZLIBCodec.getID();
138                        data = ZLIBCodec.encode(data);
139                    } else if (accept.isAccepted(GZIPCodec.getID())) {
140                        encoding = GZIPCodec.getID();
141                        data = GZIPCodec.encode(data);
142                    }
143                }
144            }
145
146            ByteArrayEntity entity = new ByteArrayEntity(data);
147            entity.setContentType(CONTENT_TYPE);
148            if (encoding != null) {
149                entity.setContentEncoding(encoding);
150            }
151            post.setEntity(entity);
152            if (cfg.isCompressionEnabled()) {
153                post.setHeader(ACCEPT_ENCODING, ACCEPT_ENCODING_VAL);
154            }
155        } catch (Exception e) {
156            toThrow = new BOSHException("Could not generate request", e);
157        }
158    }
159
160    ///////////////////////////////////////////////////////////////////////////
161    // HTTPResponse interface methods:
162
163    /**
164     * Abort the client transmission and response processing.
165     */
166    public void abort() {
167        if (post != null) {
168            post.abort();
169            toThrow = new BOSHException("HTTP request aborted");
170        }
171    }
172
173    /**
174     * Wait for and then return the response body.
175     *
176     * @return body of the response
177     * @throws InterruptedException if interrupted while awaiting the response
178     * @throws BOSHException on communication failure
179     */
180    public AbstractBody getBody() throws InterruptedException, BOSHException {
181        if (toThrow != null) {
182            throw(toThrow);
183        }
184        lock.lock();
185        try {
186            if (!sent) {
187                awaitResponse();
188            }
189        } finally {
190            lock.unlock();
191        }
192        return body;
193    }
194
195    /**
196     * Wait for and then return the response HTTP status code.
197     *
198     * @return HTTP status code of the response
199     * @throws InterruptedException if interrupted while awaiting the response
200     * @throws BOSHException on communication failure
201     */
202    public int getHTTPStatus() throws InterruptedException, BOSHException {
203        if (toThrow != null) {
204            throw(toThrow);
205        }
206        lock.lock();
207        try {
208            if (!sent) {
209                awaitResponse();
210            }
211        } finally {
212            lock.unlock();
213        }
214        return statusCode;
215    }
216
217    ///////////////////////////////////////////////////////////////////////////
218    // Package-private methods:
219
220    /**
221     * Await the response, storing the result in the instance variables of
222     * this class when they arrive.
223     *
224     * @throws InterruptedException if interrupted while awaiting the response
225     * @throws BOSHException on communication failure
226     */
227    private synchronized void awaitResponse() throws BOSHException {
228        HttpEntity entity = null;
229        try {
230            HttpResponse httpResp = client.execute(post, context);
231            entity = httpResp.getEntity();
232            byte[] data = EntityUtils.toByteArray(entity);
233            String encoding = entity.getContentEncoding() != null ?
234                    entity.getContentEncoding().getValue() :
235                    null;
236            if (ZLIBCodec.getID().equalsIgnoreCase(encoding)) {
237                data = ZLIBCodec.decode(data);
238            } else if (GZIPCodec.getID().equalsIgnoreCase(encoding)) {
239                data = GZIPCodec.decode(data);
240            }
241            body = StaticBody.fromString(new String(data, CHARSET));
242            statusCode = httpResp.getStatusLine().getStatusCode();
243            sent = true;
244        } catch (IOException iox) {
245            abort();
246            toThrow = new BOSHException("Could not obtain response", iox);
247            throw(toThrow);
248        } catch (RuntimeException ex) {
249            abort();
250            throw(ex);
251        }
252    }
253}
254