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