View Javadoc
1   package ca.uhn.fhir.rest.client;
2   
3   /*
4    * #%L
5    * HAPI FHIR - Core Library
6    * %%
7    * Copyright (C) 2014 University Health Network
8    * %%
9    * Licensed under the Apache License, Version 2.0 (the "License");
10   * you may not use this file except in compliance with the License.
11   * You may obtain a copy of the License at
12   * 
13   *      http://www.apache.org/licenses/LICENSE-2.0
14   * 
15   * Unless required by applicable law or agreed to in writing, software
16   * distributed under the License is distributed on an "AS IS" BASIS,
17   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18   * See the License for the specific language governing permissions and
19   * limitations under the License.
20   * #L%
21   */
22  
23  import java.io.ByteArrayInputStream;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.InputStreamReader;
27  import java.io.Reader;
28  import java.io.StringReader;
29  import java.nio.charset.Charset;
30  import java.util.ArrayList;
31  import java.util.Collections;
32  import java.util.HashMap;
33  import java.util.LinkedHashMap;
34  import java.util.List;
35  import java.util.Map;
36  
37  import org.apache.commons.io.IOUtils;
38  import org.apache.commons.lang3.StringUtils;
39  import org.apache.commons.lang3.Validate;
40  import org.apache.http.Header;
41  import org.apache.http.HttpEntity;
42  import org.apache.http.HttpEntityEnclosingRequest;
43  import org.apache.http.HttpResponse;
44  import org.apache.http.client.HttpClient;
45  import org.apache.http.client.methods.CloseableHttpResponse;
46  import org.apache.http.client.methods.HttpRequestBase;
47  import org.apache.http.entity.ContentType;
48  
49  import ca.uhn.fhir.context.FhirContext;
50  import ca.uhn.fhir.model.dstu.resource.OperationOutcome;
51  import ca.uhn.fhir.parser.DataFormatException;
52  import ca.uhn.fhir.parser.IParser;
53  import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException;
54  import ca.uhn.fhir.rest.method.IClientResponseHandler;
55  import ca.uhn.fhir.rest.method.IClientResponseHandlerHandlesBinary;
56  import ca.uhn.fhir.rest.server.Constants;
57  import ca.uhn.fhir.rest.server.EncodingEnum;
58  import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
59  
60  public abstract class BaseClient {
61  	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseClient.class);
62  
63  	private final HttpClient myClient;
64  	private EncodingEnum myEncoding = null; // default unspecified (will be XML)
65  	private List<IClientInterceptor> myInterceptors = new ArrayList<IClientInterceptor>();
66  	private boolean myKeepResponses = false;
67  	private HttpResponse myLastResponse;
68  	private String myLastResponseBody;
69  	private Boolean myPrettyPrint = false;
70  
71  	private final String myUrlBase;
72  
73  	BaseClient(HttpClient theClient, String theUrlBase) {
74  		super();
75  		myClient = theClient;
76  		myUrlBase = theUrlBase;
77  	}
78  
79  	protected Map<String, List<String>> createExtraParams() {
80  		HashMap<String, List<String>> retVal = new LinkedHashMap<String, List<String>>();
81  
82  		if (getEncoding() == EncodingEnum.XML) {
83  			retVal.put(Constants.PARAM_FORMAT, Collections.singletonList("xml"));
84  		} else if (getEncoding() == EncodingEnum.JSON) {
85  			retVal.put(Constants.PARAM_FORMAT, Collections.singletonList("json"));
86  		}
87  
88  		if (isPrettyPrint()) {
89  			retVal.put(Constants.PARAM_PRETTY, Collections.singletonList(Constants.PARAM_PRETTY_VALUE_TRUE));
90  		}
91  
92  		return retVal;
93  	}
94  
95  	/**
96  	 * Returns the encoding that will be used on requests. Default is <code>null</code>, which means the client will not explicitly request an encoding. (This is standard behaviour according to the
97  	 * FHIR specification)
98  	 */
99  	public EncodingEnum getEncoding() {
100 		return myEncoding;
101 	}
102 
103 	/**
104 	 * For now, this is a part of the internal API of HAPI - Use with caution as this method may change!
105 	 */
106 	public HttpResponse getLastResponse() {
107 		return myLastResponse;
108 	}
109 
110 	/**
111 	 * For now, this is a part of the internal API of HAPI - Use with caution as this method may change!
112 	 */
113 	public String getLastResponseBody() {
114 		return myLastResponseBody;
115 	}
116 
117 	String getServerBase() {
118 		return myUrlBase;
119 	}
120 
121 	public String getUrlBase() {
122 		return myUrlBase;
123 	}
124 
125 	<T> T invokeClient(FhirContext theContext, IClientResponseHandler<T> binding, BaseHttpClientInvocation clientInvocation) {
126 		return invokeClient(theContext, binding, clientInvocation, false);
127 	}
128 
129 	<T> T invokeClient(FhirContext theContext, IClientResponseHandler<T> binding, BaseHttpClientInvocation clientInvocation, boolean theLogRequestAndResponse) {
130 		return invokeClient(theContext, binding, clientInvocation, null, null, theLogRequestAndResponse);
131 	}
132 	
133 	<T> T invokeClient(FhirContext theContext, IClientResponseHandler<T> binding, BaseHttpClientInvocation clientInvocation, EncodingEnum theEncoding, Boolean thePrettyPrint, boolean theLogRequestAndResponse) {
134 		// TODO: handle non 2xx status codes by throwing the correct exception,
135 		// and ensure it's passed upwards
136 		HttpRequestBase httpRequest;
137 		HttpResponse response;
138 		try {
139 			Map<String, List<String>> params = createExtraParams();
140 			
141 			if (theEncoding == EncodingEnum.XML) {
142 				params.put(Constants.PARAM_FORMAT, Collections.singletonList("xml"));
143 			} else if (theEncoding == EncodingEnum.JSON) {
144 				params.put(Constants.PARAM_FORMAT, Collections.singletonList("json"));
145 			}
146 			if (thePrettyPrint == Boolean.TRUE) {
147 				params.put(Constants.PARAM_PRETTY, Collections.singletonList(Constants.PARAM_PRETTY_VALUE_TRUE));
148 			}
149 
150 			EncodingEnum encoding = getEncoding();
151 			if (theEncoding != null) {
152 				encoding=theEncoding;
153 			}
154 			
155 			httpRequest = clientInvocation.asHttpRequest(myUrlBase, params, encoding);
156 
157 			if (theLogRequestAndResponse) {
158 				ourLog.info("Client invoking: {}", httpRequest);
159 				if (httpRequest instanceof HttpEntityEnclosingRequest) {
160 					HttpEntity entity = ((HttpEntityEnclosingRequest) httpRequest).getEntity();
161 					if (entity.isRepeatable()) {
162 						String content = IOUtils.toString(entity.getContent());
163 						ourLog.info("Client request body: {}", content);
164 					}
165 				}
166 			}
167 
168 			for (IClientInterceptor nextInterceptor : myInterceptors) {
169 				nextInterceptor.interceptRequest(httpRequest);
170 			}
171 
172 			response = myClient.execute(httpRequest);
173 
174 			for (IClientInterceptor nextInterceptor : myInterceptors) {
175 				nextInterceptor.interceptResponse(response);
176 			}
177 
178 		} catch (DataFormatException e) {
179 			throw new FhirClientConnectionException(e);
180 		} catch (IOException e) {
181 			throw new FhirClientConnectionException(e);
182 		}
183 
184 		try {
185 			ContentType ct = ContentType.get(response.getEntity());
186 			String mimeType = ct != null ? ct.getMimeType() : null;
187 
188 			Map<String, List<String>> headers = new HashMap<String, List<String>>();
189 			if (response.getAllHeaders() != null) {
190 				for (Header next : response.getAllHeaders()) {
191 					String name = next.getName().toLowerCase();
192 					List<String> list = headers.get(name);
193 					if (list == null) {
194 						list = new ArrayList<String>();
195 						headers.put(name, list);
196 					}
197 					list.add(next.getValue());
198 				}
199 			}
200 
201 			if (response.getStatusLine().getStatusCode() < 200 || response.getStatusLine().getStatusCode() > 299) {
202 				String body = null;
203 				Reader reader = null;
204 				try {
205 					reader = createReaderFromResponse(response);
206 					body = IOUtils.toString(reader);
207 				} catch (Exception e) {
208 					ourLog.debug("Failed to read input stream", e);
209 				} finally {
210 					IOUtils.closeQuietly(reader);
211 				}
212 
213 				String message = "HTTP " + response.getStatusLine().getStatusCode() + " " + response.getStatusLine().getReasonPhrase();
214 				OperationOutcome oo=null;
215 				if (Constants.CT_TEXT.equals(mimeType)) {
216 					message = message + ": " + body;
217 				} else {
218 					EncodingEnum enc = EncodingEnum.forContentType(mimeType);
219 					if (enc != null) {
220 						IParser p = enc.newParser(theContext);
221 						try {
222 							oo = p.parseResource(OperationOutcome.class, body);
223 							if (oo.getIssueFirstRep().getDetails().isEmpty()==false) {
224 								message = message + ": " + oo.getIssueFirstRep().getDetails().getValue();
225 							}
226 						} catch (Exception e) {
227 							ourLog.debug("Failed to process OperationOutcome response");
228 						}
229 					}
230 				}
231 
232 				keepResponseAndLogIt(theLogRequestAndResponse, response, body);
233 
234 				BaseServerResponseException exception = BaseServerResponseException.newInstance(response.getStatusLine().getStatusCode(), message);
235 				exception.setOperationOutcome(oo);
236 				
237 				if (body != null) {
238 					exception.setResponseBody(body);
239 				}
240 
241 				throw exception;
242 			}
243 			if (binding instanceof IClientResponseHandlerHandlesBinary) {
244 				IClientResponseHandlerHandlesBinary<T> handlesBinary = (IClientResponseHandlerHandlesBinary<T>) binding;
245 				if (handlesBinary.isBinary()) {
246 					InputStream reader = response.getEntity().getContent();
247 					try {
248 
249 						if (ourLog.isTraceEnabled() || myKeepResponses || theLogRequestAndResponse) {
250 							byte[] responseBytes = IOUtils.toByteArray(reader);
251 							if (myKeepResponses) {
252 								myLastResponse = response;
253 								myLastResponseBody = null;
254 							}
255 							String message = "HTTP " + response.getStatusLine().getStatusCode() + " " + response.getStatusLine().getReasonPhrase();
256 							if (theLogRequestAndResponse) {
257 								ourLog.info("Client response: {} - {} bytes", message, responseBytes.length);
258 							} else {
259 								ourLog.trace("Client response: {} - {} bytes", message, responseBytes.length);
260 							}
261 							reader = new ByteArrayInputStream(responseBytes);
262 						}
263 
264 						return handlesBinary.invokeClient(mimeType, reader, response.getStatusLine().getStatusCode(), headers);
265 					} finally {
266 						IOUtils.closeQuietly(reader);
267 					}
268 				}
269 			}
270 
271 			Reader reader = createReaderFromResponse(response);
272 
273 			if (ourLog.isTraceEnabled() || myKeepResponses || theLogRequestAndResponse) {
274 				String responseString = IOUtils.toString(reader);
275 				keepResponseAndLogIt(theLogRequestAndResponse, response, responseString);
276 				reader = new StringReader(responseString);
277 			}
278 
279 			try {
280 				return binding.invokeClient(mimeType, reader, response.getStatusLine().getStatusCode(), headers);
281 			} finally {
282 				IOUtils.closeQuietly(reader);
283 			}
284 
285 		} catch (IllegalStateException e) {
286 			throw new FhirClientConnectionException(e);
287 		} catch (IOException e) {
288 			throw new FhirClientConnectionException(e);
289 		} finally {
290 			if (response instanceof CloseableHttpResponse) {
291 				try {
292 					((CloseableHttpResponse) response).close();
293 				} catch (IOException e) {
294 					ourLog.debug("Failed to close response", e);
295 				}
296 			}
297 		}
298 	}
299 
300 	/**
301 	 * For now, this is a part of the internal API of HAPI - Use with caution as this method may change!
302 	 */
303 	public boolean isKeepResponses() {
304 		return myKeepResponses;
305 	}
306 
307 	/**
308 	 * Returns the pretty print flag, which is a request to the server for it to return "pretty printed" responses. Note that this is currently a non-standard flag (_pretty) which is supported only by
309 	 * HAPI based servers (and any other servers which might implement it).
310 	 */
311 	public boolean isPrettyPrint() {
312 		return Boolean.TRUE.equals(myPrettyPrint);
313 	}
314 
315 	/**
316 	 * Returns the pretty print flag, which is a request to the server for it to return "pretty printed" responses. Note that this is currently a non-standard flag (_pretty) which is supported only by
317 	 * HAPI based servers (and any other servers which might implement it).
318 	 */
319 	public Boolean getPrettyPrint() {
320 		return myPrettyPrint;
321 	}
322 	
323 	private void keepResponseAndLogIt(boolean theLogRequestAndResponse, HttpResponse response, String responseString) {
324 		if (myKeepResponses) {
325 			myLastResponse = response;
326 			myLastResponseBody = responseString;
327 		}
328 		if (theLogRequestAndResponse) {
329 			String message = "HTTP " + response.getStatusLine().getStatusCode() + " " + response.getStatusLine().getReasonPhrase();
330 			if (StringUtils.isNotBlank(responseString)) {
331 				ourLog.info("Client response: {}\n{}", message, responseString);
332 			} else {
333 				ourLog.info("Client response: {}", message, responseString);
334 			}
335 		} else {
336 			ourLog.trace("FHIR response:\n{}\n{}", response, responseString);
337 		}
338 	}
339 
340 	public void registerInterceptor(IClientInterceptor theInterceptor) {
341 		Validate.notNull(theInterceptor, "Interceptor can not be null");
342 		myInterceptors.add(theInterceptor);
343 	}
344 
345 	/**
346 	 * Sets the encoding that will be used on requests. Default is <code>null</code>, which means the client will not explicitly request an encoding. (This is standard behaviour according to the FHIR
347 	 * specification)
348 	 */
349 	public BaseClient setEncoding(EncodingEnum theEncoding) {
350 		myEncoding = theEncoding;
351 		return this;
352 	}
353 
354 	/**
355 	 * For now, this is a part of the internal API of HAPI - Use with caution as this method may change!
356 	 */
357 	public void setKeepResponses(boolean theKeepResponses) {
358 		myKeepResponses = theKeepResponses;
359 	}
360 
361 	/**
362 	 * For now, this is a part of the internal API of HAPI - Use with caution as this method may change!
363 	 */
364 	public void setLastResponse(HttpResponse theLastResponse) {
365 		myLastResponse = theLastResponse;
366 	}
367 
368 	/**
369 	 * For now, this is a part of the internal API of HAPI - Use with caution as this method may change!
370 	 */
371 	public void setLastResponseBody(String theLastResponseBody) {
372 		myLastResponseBody = theLastResponseBody;
373 	}
374 
375 	/**
376 	 * Sets the pretty print flag, which is a request to the server for it to return "pretty printed" responses. Note that this is currently a non-standard flag (_pretty) which is supported only by
377 	 * HAPI based servers (and any other servers which might implement it).
378 	 */
379 	public BaseClient setPrettyPrint(Boolean thePrettyPrint) {
380 		myPrettyPrint = thePrettyPrint;
381 		return this;
382 	}
383 
384 	public void unregisterInterceptor(IClientInterceptor theInterceptor) {
385 		Validate.notNull(theInterceptor, "Interceptor can not be null");
386 		myInterceptors.remove(theInterceptor);
387 	}
388 
389 	public static Reader createReaderFromResponse(HttpResponse theResponse) throws IllegalStateException, IOException {
390 		HttpEntity entity = theResponse.getEntity();
391 		if (entity == null) {
392 			return new StringReader("");
393 		}
394 		Charset charset = null;
395 		if (entity.getContentType() != null && entity.getContentType().getElements() != null && entity.getContentType().getElements().length > 0) {
396 			ContentType ct = ContentType.get(entity);
397 			charset = ct.getCharset();
398 		}
399 		if (charset == null) {
400 			ourLog.warn("Response did not specify a charset.");
401 			charset = Charset.forName("UTF-8");
402 		}
403 
404 		Reader reader = new InputStreamReader(theResponse.getEntity().getContent(), charset);
405 		return reader;
406 	}
407 
408 }