1d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru/*
2d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru * Copyright (C) 2011 The Android Open Source Project
3d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru *
4d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru * Licensed under the Apache License, Version 2.0 (the "License");
5d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru * you may not use this file except in compliance with the License.
6d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru * You may obtain a copy of the License at
7d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru *
8d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru *      http://www.apache.org/licenses/LICENSE-2.0
9d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru *
10d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru * Unless required by applicable law or agreed to in writing, software
11d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru * distributed under the License is distributed on an "AS IS" BASIS,
12d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru * See the License for the specific language governing permissions and
14d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru * limitations under the License.
15d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru */
16d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru
17d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Querupackage com.android.volley.toolbox;
18d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru
19d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queruimport com.android.volley.AuthFailureError;
20d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queruimport com.android.volley.Request;
21e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queruimport com.android.volley.Request.Method;
22d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru
23d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queruimport org.apache.http.HttpEntity;
24d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queruimport org.apache.http.HttpResponse;
25d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queruimport org.apache.http.NameValuePair;
26d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queruimport org.apache.http.client.HttpClient;
27e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queruimport org.apache.http.client.methods.HttpDelete;
28e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queruimport org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
29d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queruimport org.apache.http.client.methods.HttpGet;
30364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chuimport org.apache.http.client.methods.HttpHead;
31364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chuimport org.apache.http.client.methods.HttpOptions;
32d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queruimport org.apache.http.client.methods.HttpPost;
33e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queruimport org.apache.http.client.methods.HttpPut;
34364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chuimport org.apache.http.client.methods.HttpTrace;
35d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queruimport org.apache.http.client.methods.HttpUriRequest;
36d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queruimport org.apache.http.entity.ByteArrayEntity;
37d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queruimport org.apache.http.message.BasicNameValuePair;
38d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queruimport org.apache.http.params.HttpConnectionParams;
39d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queruimport org.apache.http.params.HttpParams;
40d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru
41d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queruimport java.io.IOException;
42364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chuimport java.net.URI;
43d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queruimport java.util.ArrayList;
44d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queruimport java.util.List;
45d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queruimport java.util.Map;
46d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru
47d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru/**
48d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru * An HttpStack that performs request over an {@link HttpClient}.
49d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru */
50d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Querupublic class HttpClientStack implements HttpStack {
51d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru    protected final HttpClient mClient;
52d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru
53e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru    private final static String HEADER_CONTENT_TYPE = "Content-Type";
54e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru
55d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru    public HttpClientStack(HttpClient client) {
56d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru        mClient = client;
57d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru    }
58d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru
59d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru    private static void addHeaders(HttpUriRequest httpRequest, Map<String, String> headers) {
60d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru        for (String key : headers.keySet()) {
61d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru            httpRequest.setHeader(key, headers.get(key));
62d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru        }
63d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru    }
64d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru
65d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru    @SuppressWarnings("unused")
66d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru    private static List<NameValuePair> getPostParameterPairs(Map<String, String> postParams) {
67d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru        List<NameValuePair> result = new ArrayList<NameValuePair>(postParams.size());
68d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru        for (String key : postParams.keySet()) {
69d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru            result.add(new BasicNameValuePair(key, postParams.get(key)));
70d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru        }
71d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru        return result;
72d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru    }
73d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru
74d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru    @Override
75d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru    public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
76d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru            throws IOException, AuthFailureError {
77e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru        HttpUriRequest httpRequest = createHttpRequest(request, additionalHeaders);
78d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru        addHeaders(httpRequest, additionalHeaders);
79d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru        addHeaders(httpRequest, request.getHeaders());
80d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru        onPrepareRequest(httpRequest);
81d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru        HttpParams httpParams = httpRequest.getParams();
82d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru        int timeoutMs = request.getTimeoutMs();
83d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru        // TODO: Reevaluate this connection timeout based on more wide-scale
84d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru        // data collection and possibly different for wifi vs. 3G.
85d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru        HttpConnectionParams.setConnectionTimeout(httpParams, 5000);
86d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru        HttpConnectionParams.setSoTimeout(httpParams, timeoutMs);
87d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru        return mClient.execute(httpRequest);
88d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru    }
89d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru
90d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru    /**
91e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru     * Creates the appropriate subclass of HttpUriRequest for passed in request.
92e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru     */
93e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru    @SuppressWarnings("deprecation")
94e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru    /* protected */ static HttpUriRequest createHttpRequest(Request<?> request,
95e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru            Map<String, String> additionalHeaders) throws AuthFailureError {
96e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru        switch (request.getMethod()) {
97e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru            case Method.DEPRECATED_GET_OR_POST: {
98e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru                // This is the deprecated way that needs to be handled for backwards compatibility.
99e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru                // If the request's post body is null, then the assumption is that the request is
100e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru                // GET.  Otherwise, it is assumed that the request is a POST.
101e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru                byte[] postBody = request.getPostBody();
102e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru                if (postBody != null) {
103e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru                    HttpPost postRequest = new HttpPost(request.getUrl());
104e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru                    postRequest.addHeader(HEADER_CONTENT_TYPE, request.getPostBodyContentType());
105e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru                    HttpEntity entity;
106e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru                    entity = new ByteArrayEntity(postBody);
107e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru                    postRequest.setEntity(entity);
108e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru                    return postRequest;
109e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru                } else {
110e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru                    return new HttpGet(request.getUrl());
111e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru                }
112e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru            }
113e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru            case Method.GET:
114e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru                return new HttpGet(request.getUrl());
115e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru            case Method.DELETE:
116e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru                return new HttpDelete(request.getUrl());
117e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru            case Method.POST: {
118e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru                HttpPost postRequest = new HttpPost(request.getUrl());
119e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru                postRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType());
120e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru                setEntityIfNonEmptyBody(postRequest, request);
121e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru                return postRequest;
122e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru            }
123e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru            case Method.PUT: {
124e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru                HttpPut putRequest = new HttpPut(request.getUrl());
125e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru                putRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType());
126e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru                setEntityIfNonEmptyBody(putRequest, request);
127e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru                return putRequest;
128e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru            }
129364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu            case Method.HEAD:
130364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu                return new HttpHead(request.getUrl());
131364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu            case Method.OPTIONS:
132364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu                return new HttpOptions(request.getUrl());
133364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu            case Method.TRACE:
134364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu                return new HttpTrace(request.getUrl());
135364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu            case Method.PATCH: {
136364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu                HttpPatch patchRequest = new HttpPatch(request.getUrl());
137364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu                patchRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType());
138364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu                setEntityIfNonEmptyBody(patchRequest, request);
139364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu                return patchRequest;
140364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu            }
141e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru            default:
142e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru                throw new IllegalStateException("Unknown request method.");
143e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru        }
144e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru    }
145e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru
146e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru    private static void setEntityIfNonEmptyBody(HttpEntityEnclosingRequestBase httpRequest,
147e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru            Request<?> request) throws AuthFailureError {
148e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru        byte[] body = request.getBody();
149e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru        if (body != null) {
150e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru            HttpEntity entity = new ByteArrayEntity(body);
151e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru            httpRequest.setEntity(entity);
152e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru        }
153e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru    }
154e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru
155e48f4430bfd3030350aa5ba827b449c37e2fadc9Jean-Baptiste Queru    /**
156d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru     * Called before the request is executed using the underlying HttpClient.
157d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru     *
158d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru     * <p>Overwrite in subclasses to augment the request.</p>
159d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru     */
160d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru    protected void onPrepareRequest(HttpUriRequest request) throws IOException {
161d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru        // Nothing.
162d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru    }
163364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu
164364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu    /**
165364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu     * The HttpPatch class does not exist in the Android framework, so this has been defined here.
166364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu     */
167364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu    public static final class HttpPatch extends HttpEntityEnclosingRequestBase {
168364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu
169364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu        public final static String METHOD_NAME = "PATCH";
170364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu
171364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu        public HttpPatch() {
172364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu            super();
173364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu        }
174364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu
175364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu        public HttpPatch(final URI uri) {
176364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu            super();
177364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu            setURI(uri);
178364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu        }
179364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu
180364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu        /**
181364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu         * @throws IllegalArgumentException if the uri is invalid.
182364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu         */
183364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu        public HttpPatch(final String uri) {
184364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu            super();
185364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu            setURI(URI.create(uri));
186364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu        }
187364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu
188364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu        @Override
189364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu        public String getMethod() {
190364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu            return METHOD_NAME;
191364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu        }
192364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu
193364614be46d20fba35c3b6bc1c2d43fe8e861e9cMaurice Chu    }
194d56b88ae161057e848e7410d1b9ce5b0b8c427fcJean-Baptiste Queru}
195