001package ca.uhn.fhir.rest.client;
002
003import static org.apache.commons.lang3.StringUtils.isNotBlank;
004
005/*
006 * #%L
007 * HAPI FHIR - Core Library
008 * %%
009 * Copyright (C) 2014 - 2016 University Health Network
010 * %%
011 * Licensed under the Apache License, Version 2.0 (the "License");
012 * you may not use this file except in compliance with the License.
013 * You may obtain a copy of the License at
014 * 
015 *      http://www.apache.org/licenses/LICENSE-2.0
016 * 
017 * Unless required by applicable law or agreed to in writing, software
018 * distributed under the License is distributed on an "AS IS" BASIS,
019 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
020 * See the License for the specific language governing permissions and
021 * limitations under the License.
022 * #L%
023 */
024
025import java.io.ByteArrayInputStream;
026import java.io.IOException;
027import java.io.InputStream;
028import java.io.InputStreamReader;
029import java.io.Reader;
030import java.io.StringReader;
031import java.nio.charset.Charset;
032import java.util.ArrayList;
033import java.util.Collections;
034import java.util.HashMap;
035import java.util.LinkedHashMap;
036import java.util.List;
037import java.util.Map;
038import java.util.Set;
039
040import org.apache.commons.io.IOUtils;
041import org.apache.commons.lang3.StringUtils;
042import org.apache.commons.lang3.Validate;
043import org.apache.http.Header;
044import org.apache.http.HttpEntity;
045import org.apache.http.HttpEntityEnclosingRequest;
046import org.apache.http.HttpResponse;
047import org.apache.http.client.HttpClient;
048import org.apache.http.client.methods.CloseableHttpResponse;
049import org.apache.http.client.methods.HttpRequestBase;
050import org.apache.http.entity.ContentType;
051import org.hl7.fhir.instance.model.api.IBase;
052import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
053import org.hl7.fhir.instance.model.api.IBaseResource;
054import org.hl7.fhir.instance.model.api.IIdType;
055import org.hl7.fhir.instance.model.api.IPrimitiveType;
056
057import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
058import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
059import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
060import ca.uhn.fhir.context.FhirContext;
061import ca.uhn.fhir.context.RuntimeResourceDefinition;
062import ca.uhn.fhir.parser.DataFormatException;
063import ca.uhn.fhir.parser.IParser;
064import ca.uhn.fhir.rest.api.SummaryEnum;
065import ca.uhn.fhir.rest.client.api.IRestfulClient;
066import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException;
067import ca.uhn.fhir.rest.client.exceptions.InvalidResponseException;
068import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException;
069import ca.uhn.fhir.rest.method.HttpGetClientInvocation;
070import ca.uhn.fhir.rest.method.IClientResponseHandler;
071import ca.uhn.fhir.rest.method.IClientResponseHandlerHandlesBinary;
072import ca.uhn.fhir.rest.method.MethodUtil;
073import ca.uhn.fhir.rest.server.Constants;
074import ca.uhn.fhir.rest.server.EncodingEnum;
075import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
076import ca.uhn.fhir.util.OperationOutcomeUtil;
077
078public abstract class BaseClient implements IRestfulClient {
079
080        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseClient.class);
081
082        private final HttpClient myClient;
083        private boolean myDontValidateConformance;
084        private EncodingEnum myEncoding = null; // default unspecified (will be XML)
085        private final RestfulClientFactory myFactory;
086        private List<IClientInterceptor> myInterceptors = new ArrayList<IClientInterceptor>();
087        private boolean myKeepResponses = false;
088        private HttpResponse myLastResponse;
089        private String myLastResponseBody;
090        private Boolean myPrettyPrint = false;
091        private SummaryEnum mySummary;
092        private final String myUrlBase;
093
094        BaseClient(HttpClient theClient, String theUrlBase, RestfulClientFactory theFactory) {
095                super();
096                myClient = theClient;
097                myUrlBase = theUrlBase;
098                myFactory = theFactory;
099        }
100
101        protected Map<String, List<String>> createExtraParams() {
102                HashMap<String, List<String>> retVal = new LinkedHashMap<String, List<String>>();
103
104                if (getEncoding() == EncodingEnum.XML) {
105                        retVal.put(Constants.PARAM_FORMAT, Collections.singletonList("xml"));
106                } else if (getEncoding() == EncodingEnum.JSON) {
107                        retVal.put(Constants.PARAM_FORMAT, Collections.singletonList("json"));
108                }
109
110                if (isPrettyPrint()) {
111                        retVal.put(Constants.PARAM_PRETTY, Collections.singletonList(Constants.PARAM_PRETTY_VALUE_TRUE));
112                }
113
114                return retVal;
115        }
116
117        void forceConformanceCheck() {
118                myFactory.validateServerBase(myUrlBase, myClient, this);
119        }
120
121        /**
122         * Returns the encoding that will be used on requests. Default is <code>null</code>, which means the client will not
123         * explicitly request an encoding. (This is standard behaviour according to the FHIR specification)
124         */
125        public EncodingEnum getEncoding() {
126                return myEncoding;
127        }
128
129        /**
130         * {@inheritDoc}
131         */
132        @Override
133        public HttpClient getHttpClient() {
134                return myClient;
135        }
136
137        public List<IClientInterceptor> getInterceptors() {
138                return Collections.unmodifiableList(myInterceptors);
139        }
140
141        /**
142         * For now, this is a part of the internal API of HAPI - Use with caution as this method may change!
143         */
144        public HttpResponse getLastResponse() {
145                return myLastResponse;
146        }
147
148        /**
149         * For now, this is a part of the internal API of HAPI - Use with caution as this method may change!
150         */
151        public String getLastResponseBody() {
152                return myLastResponseBody;
153        }
154
155        /**
156         * Returns the pretty print flag, which is a request to the server for it to return "pretty printed" responses. Note
157         * that this is currently a non-standard flag (_pretty) which is supported only by HAPI based servers (and any other
158         * servers which might implement it).
159         */
160        public Boolean getPrettyPrint() {
161                return myPrettyPrint;
162        }
163
164        /**
165         * {@inheritDoc}
166         */
167        @Override
168        public String getServerBase() {
169                return myUrlBase;
170        }
171
172        public SummaryEnum getSummary() {
173                return mySummary;
174        }
175
176        public String getUrlBase() {
177                return myUrlBase;
178        }
179
180        <T> T invokeClient(FhirContext theContext, IClientResponseHandler<T> binding, BaseHttpClientInvocation clientInvocation) {
181                return invokeClient(theContext, binding, clientInvocation, false);
182        }
183
184        <T> T invokeClient(FhirContext theContext, IClientResponseHandler<T> binding, BaseHttpClientInvocation clientInvocation, boolean theLogRequestAndResponse) {
185                return invokeClient(theContext, binding, clientInvocation, null, null, theLogRequestAndResponse, null, null);
186        }
187
188        <T> T invokeClient(FhirContext theContext, IClientResponseHandler<T> binding, BaseHttpClientInvocation clientInvocation, EncodingEnum theEncoding, Boolean thePrettyPrint, boolean theLogRequestAndResponse, SummaryEnum theSummaryMode, Set<String> theSubsetElements) {
189
190                if (!myDontValidateConformance) {
191                        myFactory.validateServerBaseIfConfiguredToDoSo(myUrlBase, myClient, this);
192                }
193
194                // TODO: handle non 2xx status codes by throwing the correct exception,
195                // and ensure it's passed upwards
196                HttpRequestBase httpRequest;
197                HttpResponse response;
198                try {
199                        Map<String, List<String>> params = createExtraParams();
200
201                        if (theEncoding == EncodingEnum.XML) {
202                                params.put(Constants.PARAM_FORMAT, Collections.singletonList("xml"));
203                        } else if (theEncoding == EncodingEnum.JSON) {
204                                params.put(Constants.PARAM_FORMAT, Collections.singletonList("json"));
205                        }
206
207                        if (theSummaryMode != null) {
208                                params.put(Constants.PARAM_SUMMARY, Collections.singletonList(theSummaryMode.getCode()));
209                        } else if (mySummary != null) {
210                                params.put(Constants.PARAM_SUMMARY, Collections.singletonList(mySummary.getCode()));
211                        }
212
213                        if (thePrettyPrint == Boolean.TRUE) {
214                                params.put(Constants.PARAM_PRETTY, Collections.singletonList(Constants.PARAM_PRETTY_VALUE_TRUE));
215                        }
216
217                        if (theSubsetElements != null && theSubsetElements.isEmpty() == false) {
218                                params.put(Constants.PARAM_ELEMENTS, Collections.singletonList(StringUtils.join(theSubsetElements, ',')));
219                        }
220
221                        EncodingEnum encoding = getEncoding();
222                        if (theEncoding != null) {
223                                encoding = theEncoding;
224                        }
225
226                        httpRequest = clientInvocation.asHttpRequest(myUrlBase, params, encoding, thePrettyPrint);
227
228                        if (theLogRequestAndResponse) {
229                                ourLog.info("Client invoking: {}", httpRequest);
230                                if (httpRequest instanceof HttpEntityEnclosingRequest) {
231                                        HttpEntity entity = ((HttpEntityEnclosingRequest) httpRequest).getEntity();
232                                        if (entity.isRepeatable()) {
233                                                String content = IOUtils.toString(entity.getContent());
234                                                ourLog.info("Client request body: {}", content);
235                                        }
236                                }
237                        }
238
239                        for (IClientInterceptor nextInterceptor : myInterceptors) {
240                                nextInterceptor.interceptRequest(httpRequest);
241                        }
242
243                        response = myClient.execute(httpRequest);
244
245                        for (IClientInterceptor nextInterceptor : myInterceptors) {
246                                nextInterceptor.interceptResponse(response);
247                        }
248
249                } catch (DataFormatException e) {
250                        throw new FhirClientConnectionException(e);
251                } catch (IOException e) {
252                        throw new FhirClientConnectionException(e);
253                }
254
255                try {
256                        String mimeType;
257                        if (Constants.STATUS_HTTP_204_NO_CONTENT == response.getStatusLine().getStatusCode()) {
258                                mimeType = null;
259                        } else {
260                                ContentType ct = ContentType.get(response.getEntity());
261                                mimeType = ct != null ? ct.getMimeType() : null;
262                        }
263
264                        Map<String, List<String>> headers = new HashMap<String, List<String>>();
265                        if (response.getAllHeaders() != null) {
266                                for (Header next : response.getAllHeaders()) {
267                                        String name = next.getName().toLowerCase();
268                                        List<String> list = headers.get(name);
269                                        if (list == null) {
270                                                list = new ArrayList<String>();
271                                                headers.put(name, list);
272                                        }
273                                        list.add(next.getValue());
274                                }
275                        }
276
277                        if (response.getStatusLine().getStatusCode() < 200 || response.getStatusLine().getStatusCode() > 299) {
278                                String body = null;
279                                Reader reader = null;
280                                try {
281                                        reader = createReaderFromResponse(response);
282                                        body = IOUtils.toString(reader);
283                                } catch (Exception e) {
284                                        ourLog.debug("Failed to read input stream", e);
285                                } finally {
286                                        IOUtils.closeQuietly(reader);
287                                }
288
289                                String message = "HTTP " + response.getStatusLine().getStatusCode() + " " + response.getStatusLine().getReasonPhrase();
290                                IBaseOperationOutcome oo = null;
291                                if (Constants.CT_TEXT.equals(mimeType)) {
292                                        message = message + ": " + body;
293                                } else {
294                                        EncodingEnum enc = EncodingEnum.forContentType(mimeType);
295                                        if (enc != null) {
296                                                IParser p = enc.newParser(theContext);
297                                                try {
298                                                        // TODO: handle if something other than OO comes back
299                                                        oo = (IBaseOperationOutcome) p.parseResource(body);
300                                                        String details = OperationOutcomeUtil.getFirstIssueDetails(getFhirContext(), oo);
301                                                        if (isNotBlank(details)) {
302                                                                message = message + ": " + details;
303                                                        }
304                                                } catch (Exception e) {
305                                                        ourLog.debug("Failed to process OperationOutcome response");
306                                                }
307                                        }
308                                }
309
310                                keepResponseAndLogIt(theLogRequestAndResponse, response, body);
311
312                                BaseServerResponseException exception = BaseServerResponseException.newInstance(response.getStatusLine().getStatusCode(), message);
313                                exception.setOperationOutcome(oo);
314
315                                if (body != null) {
316                                        exception.setResponseBody(body);
317                                }
318
319                                throw exception;
320                        }
321                        if (binding instanceof IClientResponseHandlerHandlesBinary) {
322                                IClientResponseHandlerHandlesBinary<T> handlesBinary = (IClientResponseHandlerHandlesBinary<T>) binding;
323                                if (handlesBinary.isBinary()) {
324                                        InputStream reader = response.getEntity().getContent();
325                                        try {
326
327                                                if (ourLog.isTraceEnabled() || myKeepResponses || theLogRequestAndResponse) {
328                                                        byte[] responseBytes = IOUtils.toByteArray(reader);
329                                                        if (myKeepResponses) {
330                                                                myLastResponse = response;
331                                                                myLastResponseBody = null;
332                                                        }
333                                                        String message = "HTTP " + response.getStatusLine().getStatusCode() + " " + response.getStatusLine().getReasonPhrase();
334                                                        if (theLogRequestAndResponse) {
335                                                                ourLog.info("Client response: {} - {} bytes", message, responseBytes.length);
336                                                        } else {
337                                                                ourLog.trace("Client response: {} - {} bytes", message, responseBytes.length);
338                                                        }
339                                                        reader = new ByteArrayInputStream(responseBytes);
340                                                }
341
342                                                return handlesBinary.invokeClient(mimeType, reader, response.getStatusLine().getStatusCode(), headers);
343                                        } finally {
344                                                IOUtils.closeQuietly(reader);
345                                        }
346                                }
347                        }
348
349                        Reader reader = createReaderFromResponse(response);
350
351                        if (ourLog.isTraceEnabled() || myKeepResponses || theLogRequestAndResponse) {
352                                String responseString = IOUtils.toString(reader);
353                                keepResponseAndLogIt(theLogRequestAndResponse, response, responseString);
354                                reader = new StringReader(responseString);
355                        }
356
357                        try {
358                                return binding.invokeClient(mimeType, reader, response.getStatusLine().getStatusCode(), headers);
359                        } finally {
360                                IOUtils.closeQuietly(reader);
361                        }
362
363                } catch (IllegalStateException e) {
364                        throw new FhirClientConnectionException(e);
365                } catch (IOException e) {
366                        throw new FhirClientConnectionException(e);
367                } finally {
368                        if (response instanceof CloseableHttpResponse) {
369                                try {
370                                        ((CloseableHttpResponse) response).close();
371                                } catch (IOException e) {
372                                        ourLog.debug("Failed to close response", e);
373                                }
374                        }
375                }
376        }
377
378        /**
379         * For now, this is a part of the internal API of HAPI - Use with caution as this method may change!
380         */
381        public boolean isKeepResponses() {
382                return myKeepResponses;
383        }
384
385        /**
386         * Returns the pretty print flag, which is a request to the server for it to return "pretty printed" responses. Note
387         * that this is currently a non-standard flag (_pretty) which is supported only by HAPI based servers (and any other
388         * servers which might implement it).
389         */
390        public boolean isPrettyPrint() {
391                return Boolean.TRUE.equals(myPrettyPrint);
392        }
393
394        private void keepResponseAndLogIt(boolean theLogRequestAndResponse, HttpResponse response, String responseString) {
395                if (myKeepResponses) {
396                        myLastResponse = response;
397                        myLastResponseBody = responseString;
398                }
399                if (theLogRequestAndResponse) {
400                        String message = "HTTP " + response.getStatusLine().getStatusCode() + " " + response.getStatusLine().getReasonPhrase();
401                        if (StringUtils.isNotBlank(responseString)) {
402                                ourLog.info("Client response: {}\n{}", message, responseString);
403                        } else {
404                                ourLog.info("Client response: {}", message, responseString);
405                        }
406                } else {
407                        ourLog.trace("FHIR response:\n{}\n{}", response, responseString);
408                }
409        }
410
411        @Override
412        public void registerInterceptor(IClientInterceptor theInterceptor) {
413                Validate.notNull(theInterceptor, "Interceptor can not be null");
414                myInterceptors.add(theInterceptor);
415        }
416
417        /**
418         * This method is an internal part of the HAPI API and may change, use with caution. If you want to disable the
419         * loading of conformance statements, use
420         * {@link IRestfulClientFactory#setServerValidationModeEnum(ServerValidationModeEnum)}
421         */
422        public void setDontValidateConformance(boolean theDontValidateConformance) {
423                myDontValidateConformance = theDontValidateConformance;
424        }
425
426        /**
427         * Sets the encoding that will be used on requests. Default is <code>null</code>, which means the client will not
428         * explicitly request an encoding. (This is perfectly acceptable behaviour according to the FHIR specification. In
429         * this case, the server will choose which encoding to return, and the client can handle either XML or JSON)
430         */
431        @Override
432        public void setEncoding(EncodingEnum theEncoding) {
433                myEncoding = theEncoding;
434                // return this;
435        }
436
437        /**
438         * For now, this is a part of the internal API of HAPI - Use with caution as this method may change!
439         */
440        public void setKeepResponses(boolean theKeepResponses) {
441                myKeepResponses = theKeepResponses;
442        }
443
444        /**
445         * For now, this is a part of the internal API of HAPI - Use with caution as this method may change!
446         */
447        public void setLastResponse(HttpResponse theLastResponse) {
448                myLastResponse = theLastResponse;
449        }
450
451        /**
452         * For now, this is a part of the internal API of HAPI - Use with caution as this method may change!
453         */
454        public void setLastResponseBody(String theLastResponseBody) {
455                myLastResponseBody = theLastResponseBody;
456        }
457
458        /**
459         * Sets the pretty print flag, which is a request to the server for it to return "pretty printed" responses. Note
460         * that this is currently a non-standard flag (_pretty) which is supported only by HAPI based servers (and any other
461         * servers which might implement it).
462         */
463        @Override
464        public void setPrettyPrint(Boolean thePrettyPrint) {
465                myPrettyPrint = thePrettyPrint;
466                // return this;
467        }
468
469        @Override
470        public void setSummary(SummaryEnum theSummary) {
471                mySummary = theSummary;
472        }
473
474        @Override
475        public void unregisterInterceptor(IClientInterceptor theInterceptor) {
476                Validate.notNull(theInterceptor, "Interceptor can not be null");
477                myInterceptors.remove(theInterceptor);
478        }
479
480        public static Reader createReaderFromResponse(HttpResponse theResponse) throws IllegalStateException, IOException {
481                HttpEntity entity = theResponse.getEntity();
482                if (entity == null) {
483                        return new StringReader("");
484                }
485                Charset charset = null;
486                if (entity.getContentType() != null && entity.getContentType().getElements() != null && entity.getContentType().getElements().length > 0) {
487                        ContentType ct = ContentType.get(entity);
488                        charset = ct.getCharset();
489                }
490                if (charset == null) {
491                        if (Constants.STATUS_HTTP_204_NO_CONTENT != theResponse.getStatusLine().getStatusCode()) {
492                                ourLog.warn("Response did not specify a charset.");
493                        }
494                        charset = Charset.forName("UTF-8");
495                }
496
497                Reader reader = new InputStreamReader(theResponse.getEntity().getContent(), charset);
498                return reader;
499        }
500
501        @Override
502        public <T extends IBaseResource> T fetchResourceFromUrl(Class<T> theResourceType, String theUrl) {
503                BaseHttpClientInvocation clientInvocation = new HttpGetClientInvocation(theUrl);
504                ResourceResponseHandler<T> binding = new ResourceResponseHandler<T>(theResourceType, null, false);
505                return invokeClient(getFhirContext(), binding, clientInvocation, null, false, false, null, null);
506        }
507
508        protected final class ResourceResponseHandler<T extends IBaseResource> implements IClientResponseHandler<T> {
509
510                private boolean myAllowHtmlResponse;
511                private IIdType myId;
512                private Class<T> myType;
513
514                public ResourceResponseHandler(Class<T> theType, IIdType theId) {
515                        myType = theType;
516                        myId = theId;
517                }
518
519                public ResourceResponseHandler(Class<T> theType, IIdType theId, boolean theAllowHtmlResponse) {
520                        myType = theType;
521                        myId = theId;
522                        myAllowHtmlResponse = theAllowHtmlResponse;
523                }
524
525                @Override
526                public T invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException {
527                        EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType);
528                        if (respType == null) {
529                                if (myAllowHtmlResponse && theResponseMimeType.toLowerCase().contains(Constants.CT_HTML) && myType != null) {
530                                        return readHtmlResponse(theResponseReader);
531                                }
532                                throw NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseReader);
533                        }
534                        IParser parser = respType.newParser(getFhirContext());
535                        T retVal = parser.parseResource(myType, theResponseReader);
536
537                        MethodUtil.parseClientRequestResourceHeaders(myId, theHeaders, retVal);
538
539                        return retVal;
540                }
541
542                @SuppressWarnings("unchecked")
543                private T readHtmlResponse(Reader theResponseReader) {
544                        RuntimeResourceDefinition resDef = getFhirContext().getResourceDefinition(myType);
545                        IBaseResource instance = resDef.newInstance();
546                        BaseRuntimeChildDefinition textChild = resDef.getChildByName("text");
547                        BaseRuntimeElementCompositeDefinition<?> textElement = (BaseRuntimeElementCompositeDefinition<?>) textChild.getChildByName("text");
548                        IBase textInstance = textElement.newInstance();
549                        textChild.getMutator().addValue(instance, textInstance);
550
551                        BaseRuntimeChildDefinition divChild = textElement.getChildByName("div");
552                        BaseRuntimeElementDefinition<?> divElement = divChild.getChildByName("div");
553                        IPrimitiveType<?> divInstance = (IPrimitiveType<?>) divElement.newInstance();
554                        try {
555                                divInstance.setValueAsString(IOUtils.toString(theResponseReader));
556                        } catch (Exception e) {
557                                throw new InvalidResponseException(400, "Failed to process HTML response from server: " + e.getMessage(), e);
558                        }
559                        divChild.getMutator().addValue(textInstance, divInstance);
560                        return (T) instance;
561                }
562        }
563
564}