View Javadoc

1   /*
2    * Copyright (C) 2012 Atlassian
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  package com.atlassian.jira.rest.client.internal.async;
17  
18  import com.atlassian.httpclient.api.DefaultResponseTransformation;
19  import com.atlassian.httpclient.api.EntityBuilder;
20  import com.atlassian.httpclient.api.HttpClient;
21  import com.atlassian.httpclient.api.Response;
22  import com.atlassian.httpclient.api.ResponsePromise;
23  import com.atlassian.httpclient.api.ResponseTransformation;
24  import com.atlassian.jira.rest.client.api.RestClientException;
25  import com.atlassian.jira.rest.client.api.domain.util.ErrorCollection;
26  import com.atlassian.jira.rest.client.internal.json.JsonArrayParser;
27  import com.atlassian.jira.rest.client.internal.json.JsonObjectParser;
28  import com.atlassian.jira.rest.client.internal.json.JsonParseUtil;
29  import com.atlassian.jira.rest.client.internal.json.JsonParser;
30  import com.atlassian.jira.rest.client.internal.json.gen.JsonGenerator;
31  import com.atlassian.util.concurrent.Promise;
32  import com.google.common.base.Function;
33  import com.google.common.collect.ImmutableList;
34  import org.apache.commons.lang3.StringUtils;
35  import org.codehaus.jettison.json.JSONArray;
36  import org.codehaus.jettison.json.JSONException;
37  import org.codehaus.jettison.json.JSONObject;
38  
39  import javax.annotation.Nullable;
40  import java.io.ByteArrayInputStream;
41  import java.io.IOException;
42  import java.io.InputStream;
43  import java.net.URI;
44  import java.nio.charset.Charset;
45  import java.util.Collection;
46  import java.util.Collections;
47  import java.util.Map;
48  
49  /**
50   * This is a base class for asynchronous REST clients.
51   *
52   * @since v2.0
53   */
54  public abstract class AbstractAsynchronousRestClient {
55  
56      private static final String JSON_CONTENT_TYPE = "application/json";
57  
58      private final HttpClient client;
59  
60      protected AbstractAsynchronousRestClient(HttpClient client) {
61          this.client = client;
62      }
63  
64      protected interface ResponseHandler<T> {
65          T handle(Response request) throws JSONException, IOException;
66      }
67  
68      protected final <T> Promise<T> getAndParse(final URI uri, final JsonParser<?, T> parser) {
69          return callAndParse(client.newRequest(uri).setAccept("application/json").get(), parser);
70      }
71  
72      protected final <I, T> Promise<T> postAndParse(final URI uri, I entity, final JsonGenerator<I> jsonGenerator,
73                                                     final JsonObjectParser<T> parser) {
74          final ResponsePromise responsePromise = client.newRequest(uri)
75                  .setEntity(toEntity(jsonGenerator, entity))
76                  .post();
77          return callAndParse(responsePromise, parser);
78      }
79  
80      protected final <T> Promise<T> postAndParse(final URI uri, final JSONObject entity, final JsonObjectParser<T> parser) {
81          final ResponsePromise responsePromise = client.newRequest(uri)
82                  .setEntity(entity.toString())
83                  .setContentType(JSON_CONTENT_TYPE)
84                  .post();
85          return callAndParse(responsePromise, parser);
86      }
87  
88      protected final Promise<Void> post(final URI uri, final String entity) {
89          final ResponsePromise responsePromise = client.newRequest(uri)
90                  .setEntity(entity)
91                  .setContentType(JSON_CONTENT_TYPE)
92                  .post();
93          return call(responsePromise);
94      }
95  
96      protected final Promise<Void> post(final URI uri, final JSONObject entity) {
97          return post(uri, entity.toString());
98      }
99  
100     protected final <T> Promise<Void> post(final URI uri, final T entity, final JsonGenerator<T> jsonGenerator) {
101         final ResponsePromise responsePromise = client.newRequest(uri)
102                 .setEntity(toEntity(jsonGenerator, entity))
103                 .post();
104         return call(responsePromise);
105     }
106 
107     protected final Promise<Void> post(final URI uri) {
108         return post(uri, StringUtils.EMPTY);
109     }
110 
111     protected final <I, T> Promise<T> putAndParse(final URI uri, I entity, final JsonGenerator<I> jsonGenerator,
112                                                   final JsonObjectParser<T> parser) {
113         final ResponsePromise responsePromise = client.newRequest(uri)
114                 .setEntity(toEntity(jsonGenerator, entity))
115                 .put();
116         return callAndParse(responsePromise, parser);
117     }
118 
119     protected final <T> Promise<Void> put(final URI uri, final T entity, final JsonGenerator<T> jsonGenerator) {
120         final ResponsePromise responsePromise = client.newRequest(uri)
121                 .setEntity(toEntity(jsonGenerator, entity))
122                 .put();
123         return call(responsePromise);
124     }
125 
126     protected final Promise<Void> delete(final URI uri) {
127         final ResponsePromise responsePromise = client.newRequest(uri).delete();
128         return call(responsePromise);
129     }
130 
131     protected final <T> Promise<T> callAndParse(final ResponsePromise responsePromise, final ResponseHandler<T> responseHandler) {
132         final Function<Response, T> transformFunction = toFunction(responseHandler);
133         final ResponseTransformation<Object> responseTransformation = DefaultResponseTransformation.builder()
134                 .ok(transformFunction)
135                 .created(transformFunction)
136                 .others(AbstractAsynchronousRestClient.errorFunction())
137                 .build();
138         return new DelegatingPromise(responsePromise.transform(responseTransformation));
139     }
140 
141     @SuppressWarnings("unchecked")
142     protected final <T> Promise<T> callAndParse(final ResponsePromise responsePromise, final JsonParser<?, T> parser) {
143         final ResponseHandler<T> responseHandler = new ResponseHandler<T>() {
144             @Override
145             public T handle(Response response) throws JSONException, IOException {
146                 final String body = response.getEntity();
147                 return (T) (parser instanceof JsonObjectParser ?
148                         ((JsonObjectParser) parser).parse(new JSONObject(body)) :
149                         ((JsonArrayParser) parser).parse(new JSONArray(body)));
150             }
151         };
152         return callAndParse(responsePromise, responseHandler);
153     }
154 
155     protected final Promise<Void> call(final ResponsePromise responsePromise) {
156         final ResponseTransformation<Object> responseTransformation = DefaultResponseTransformation.builder()
157                 .ok(constant((Void) null))
158                 .created(constant((Void) null))
159                 .noContent(constant((Void) null))
160                 .others(AbstractAsynchronousRestClient.errorFunction())
161                 .build();
162         return new DelegatingPromise(responsePromise.transform(responseTransformation));
163     }
164 
165     protected HttpClient client() {
166         return client;
167     }
168 
169     private static <T> Function<Response, T> errorFunction() {
170         return new Function<Response, T>() {
171             @Override
172             public T apply(Response response) {
173                 try {
174                     final String body = response.getEntity();
175                     final Collection<ErrorCollection> errorMessages = extractErrors(response.getStatusCode(), body);
176                     throw new RestClientException(errorMessages, response.getStatusCode());
177                 } catch (JSONException e) {
178                     throw new RestClientException(e, response.getStatusCode());
179                 }
180             }
181         };
182     }
183 
184     private static <T> Function<Response, T> toFunction(final ResponseHandler<T> responseHandler) {
185         return new Function<Response, T>() {
186             @Override
187             public T apply(@Nullable Response input) {
188                 try {
189                     return responseHandler.handle(input);
190                 } catch (JSONException e) {
191                     throw new RestClientException(e);
192                 } catch (IOException e) {
193                     throw new RestClientException(e);
194                 }
195             }
196         };
197     }
198 
199     private static <T> Function<Response, T> constant(final T value) {
200         return new Function<Response, T>() {
201             @Override
202             public T apply(Response input) {
203                 return value;
204             }
205         };
206     }
207 
208     static Collection<ErrorCollection> extractErrors(final int status, final String body) throws JSONException {
209         if (body == null) {
210             return Collections.emptyList();
211         }
212         final JSONObject jsonObject = new JSONObject(body);
213         final JSONArray issues = jsonObject.optJSONArray("issues");
214         final ImmutableList.Builder<ErrorCollection> results = ImmutableList.builder();
215         if (issues != null && issues.length() == 0) {
216             final JSONArray errors = jsonObject.optJSONArray("errors");
217             for (int i = 0; i < errors.length(); i++) {
218                 final JSONObject currentJsonObject = errors.getJSONObject(i);
219                 results.add(getErrorsFromJson(currentJsonObject.getInt("status"), currentJsonObject
220                         .optJSONObject("elementErrors")));
221             }
222         } else {
223             results.add(getErrorsFromJson(status, jsonObject));
224         }
225         return results.build();
226     }
227 
228     private static ErrorCollection getErrorsFromJson(final int status, final JSONObject jsonObject) throws JSONException {
229         final JSONObject jsonErrors = jsonObject.optJSONObject("errors");
230         final JSONArray jsonErrorMessages = jsonObject.optJSONArray("errorMessages");
231 
232         final Collection<String> errorMessages;
233         if (jsonErrorMessages != null) {
234             errorMessages = JsonParseUtil.toStringCollection(jsonErrorMessages);
235         } else {
236             errorMessages = Collections.emptyList();
237         }
238 
239         final Map<String, String> errors;
240         if (jsonErrors != null && jsonErrors.length() > 0) {
241             errors = JsonParseUtil.toStringMap(jsonErrors.names(), jsonErrors);
242         } else {
243             errors = Collections.emptyMap();
244         }
245         return new ErrorCollection(status, errorMessages, errors);
246     }
247 
248     private <T> EntityBuilder toEntity(final JsonGenerator<T> generator, final T bean) {
249         return new EntityBuilder() {
250 
251             @Override
252             public Entity build() {
253                 return new Entity() {
254                     @Override
255                     public Map<String, String> getHeaders() {
256                         return Collections.singletonMap("Content-Type", JSON_CONTENT_TYPE);
257                     }
258 
259                     @Override
260                     public InputStream getInputStream() {
261                         try {
262                             return new ByteArrayInputStream(generator.generate(bean).toString().getBytes(Charset
263                                     .forName("UTF-8")));
264                         } catch (JSONException e) {
265                             throw new RestClientException(e);
266                         }
267                     }
268                 };
269             }
270         };
271     }
272 }