001package ca.uhn.fhir.rest.client.impl;
002
003/*
004 * #%L
005 * HAPI FHIR - Client Framework
006 * %%
007 * Copyright (C) 2014 - 2022 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 * http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
025import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
026import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
027import ca.uhn.fhir.context.FhirContext;
028import ca.uhn.fhir.context.RuntimeResourceDefinition;
029import ca.uhn.fhir.interceptor.api.HookParams;
030import ca.uhn.fhir.interceptor.api.IInterceptorService;
031import ca.uhn.fhir.interceptor.api.Pointcut;
032import ca.uhn.fhir.interceptor.executor.InterceptorService;
033import ca.uhn.fhir.parser.DataFormatException;
034import ca.uhn.fhir.parser.IParser;
035import ca.uhn.fhir.rest.api.CacheControlDirective;
036import ca.uhn.fhir.rest.api.Constants;
037import ca.uhn.fhir.rest.api.EncodingEnum;
038import ca.uhn.fhir.rest.api.RequestFormatParamStyleEnum;
039import ca.uhn.fhir.rest.api.SummaryEnum;
040import ca.uhn.fhir.rest.client.api.IHttpClient;
041import ca.uhn.fhir.rest.client.api.IHttpRequest;
042import ca.uhn.fhir.rest.client.api.IHttpResponse;
043import ca.uhn.fhir.rest.client.api.IRestfulClient;
044import ca.uhn.fhir.rest.client.api.IRestfulClientFactory;
045import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
046import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException;
047import ca.uhn.fhir.rest.client.exceptions.InvalidResponseException;
048import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException;
049import ca.uhn.fhir.rest.client.interceptor.AdditionalRequestHeadersInterceptor;
050import ca.uhn.fhir.rest.client.method.HttpGetClientInvocation;
051import ca.uhn.fhir.rest.client.method.IClientResponseHandler;
052import ca.uhn.fhir.rest.client.method.IClientResponseHandlerHandlesBinary;
053import ca.uhn.fhir.rest.client.method.MethodUtil;
054import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
055import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
056import ca.uhn.fhir.util.BinaryUtil;
057import ca.uhn.fhir.util.OperationOutcomeUtil;
058import ca.uhn.fhir.util.XmlDetectionUtil;
059import com.google.common.base.Charsets;
060import org.apache.commons.io.IOUtils;
061import org.apache.commons.lang3.StringUtils;
062import org.apache.commons.lang3.Validate;
063import org.hl7.fhir.instance.model.api.IBase;
064import org.hl7.fhir.instance.model.api.IBaseBinary;
065import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
066import org.hl7.fhir.instance.model.api.IBaseResource;
067import org.hl7.fhir.instance.model.api.IIdType;
068import org.hl7.fhir.instance.model.api.IPrimitiveType;
069
070import javax.annotation.Nonnull;
071import java.io.ByteArrayInputStream;
072import java.io.IOException;
073import java.io.InputStream;
074import java.io.Reader;
075import java.util.ArrayList;
076import java.util.Collections;
077import java.util.HashMap;
078import java.util.LinkedHashMap;
079import java.util.List;
080import java.util.Map;
081import java.util.Set;
082
083import static org.apache.commons.lang3.StringUtils.isBlank;
084import static org.apache.commons.lang3.StringUtils.isNotBlank;
085
086public abstract class BaseClient implements IRestfulClient {
087
088        /**
089         * This property is used by unit tests - do not rely on it in production code
090         * as it may change at any time. If you want to capture responses in a reliable
091         * way in your own code, just use client interceptors
092         */
093        public static final String HAPI_CLIENT_KEEPRESPONSES = "hapi.client.keepresponses";
094
095        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseClient.class);
096
097        private final IHttpClient myClient;
098        private final RestfulClientFactory myFactory;
099        private final String myUrlBase;
100        private boolean myDontValidateConformance;
101        private EncodingEnum myEncoding = null; // default unspecified (will be JSON)
102        private boolean myKeepResponses = false;
103        private IHttpResponse myLastResponse;
104        private String myLastResponseBody;
105        private Boolean myPrettyPrint = false;
106        private SummaryEnum mySummary;
107        private RequestFormatParamStyleEnum myRequestFormatParamStyle = RequestFormatParamStyleEnum.SHORT;
108        private IInterceptorService myInterceptorService;
109
110        BaseClient(IHttpClient theClient, String theUrlBase, RestfulClientFactory theFactory) {
111                super();
112                myClient = theClient;
113                myUrlBase = theUrlBase;
114                myFactory = theFactory;
115
116                /*
117                 * This property is used by unit tests - do not rely on it in production code
118                 * as it may change at any time. If you want to capture responses in a reliable
119                 * way in your own code, just use client interceptors
120                 */
121                if ("true".equals(System.getProperty(HAPI_CLIENT_KEEPRESPONSES))) {
122                        setKeepResponses(true);
123                }
124
125                if (XmlDetectionUtil.isStaxPresent() == false) {
126                        myEncoding = EncodingEnum.JSON;
127                }
128
129                setInterceptorService(new InterceptorService());
130        }
131
132        @Override
133        public IInterceptorService getInterceptorService() {
134                return myInterceptorService;
135        }
136
137        @Override
138        public void setInterceptorService(@Nonnull IInterceptorService theInterceptorService) {
139                Validate.notNull(theInterceptorService, "theInterceptorService must not be null");
140                myInterceptorService = theInterceptorService;
141        }
142
143        protected Map<String, List<String>> createExtraParams(String theCustomAcceptHeader) {
144                HashMap<String, List<String>> retVal = new LinkedHashMap<>();
145
146                if (isBlank(theCustomAcceptHeader)) {
147                        if (myRequestFormatParamStyle == RequestFormatParamStyleEnum.SHORT) {
148                                if (getEncoding() == EncodingEnum.XML) {
149                                        retVal.put(Constants.PARAM_FORMAT, Collections.singletonList("xml"));
150                                } else if (getEncoding() == EncodingEnum.JSON) {
151                                        retVal.put(Constants.PARAM_FORMAT, Collections.singletonList("json"));
152                                }
153                        }
154                }
155
156                if (isPrettyPrint()) {
157                        retVal.put(Constants.PARAM_PRETTY, Collections.singletonList(Constants.PARAM_PRETTY_VALUE_TRUE));
158                }
159
160                return retVal;
161        }
162
163        @Override
164        public <T extends IBaseResource> T fetchResourceFromUrl(Class<T> theResourceType, String theUrl) {
165                BaseHttpClientInvocation clientInvocation = new HttpGetClientInvocation(getFhirContext(), theUrl);
166                ResourceResponseHandler<T> binding = new ResourceResponseHandler<>(theResourceType);
167                return invokeClient(getFhirContext(), binding, clientInvocation, null, false, false, null, null, null, null, null);
168        }
169
170        void forceConformanceCheck() {
171                myFactory.validateServerBase(myUrlBase, myClient, this);
172        }
173
174        @Override
175        public EncodingEnum getEncoding() {
176                return myEncoding;
177        }
178
179        /**
180         * Sets the encoding that will be used on requests. Default is <code>null</code>, which means the client will not
181         * explicitly request an encoding. (This is perfectly acceptable behaviour according to the FHIR specification. In
182         * this case, the server will choose which encoding to return, and the client can handle either XML or JSON)
183         */
184        @Override
185        public void setEncoding(EncodingEnum theEncoding) {
186                myEncoding = theEncoding;
187                // return this;
188        }
189
190        /**
191         * {@inheritDoc}
192         */
193        @Override
194        public IHttpClient getHttpClient() {
195                return myClient;
196        }
197
198        /**
199         * For now, this is a part of the internal API of HAPI - Use with caution as this method may change!
200         */
201        public IHttpResponse getLastResponse() {
202                return myLastResponse;
203        }
204
205        /**
206         * For now, this is a part of the internal API of HAPI - Use with caution as this method may change!
207         */
208        public String getLastResponseBody() {
209                return myLastResponseBody;
210        }
211
212        /**
213         * {@inheritDoc}
214         */
215        @Override
216        public String getServerBase() {
217                return myUrlBase;
218        }
219
220        public SummaryEnum getSummary() {
221                return mySummary;
222        }
223
224        @Override
225        public void setSummary(SummaryEnum theSummary) {
226                mySummary = theSummary;
227        }
228
229        public String getUrlBase() {
230                return myUrlBase;
231        }
232
233        @Override
234        public void setFormatParamStyle(RequestFormatParamStyleEnum theRequestFormatParamStyle) {
235                Validate.notNull(theRequestFormatParamStyle, "theRequestFormatParamStyle must not be null");
236                myRequestFormatParamStyle = theRequestFormatParamStyle;
237        }
238
239        protected <T> T invokeClient(FhirContext theContext, IClientResponseHandler<T> binding, BaseHttpClientInvocation clientInvocation) {
240                return invokeClient(theContext, binding, clientInvocation, false);
241        }
242
243        protected <T> T invokeClient(FhirContext theContext, IClientResponseHandler<T> binding, BaseHttpClientInvocation clientInvocation, boolean theLogRequestAndResponse) {
244                return invokeClient(theContext, binding, clientInvocation, null, null, theLogRequestAndResponse, null, null, null, null, null);
245        }
246
247        protected <T> T invokeClient(FhirContext theContext, IClientResponseHandler<T> binding, BaseHttpClientInvocation clientInvocation, EncodingEnum theEncoding, Boolean thePrettyPrint,
248                                                         boolean theLogRequestAndResponse, SummaryEnum theSummaryMode, Set<String> theSubsetElements, CacheControlDirective theCacheControlDirective, String theCustomAcceptHeader,
249                                                         Map<String, List<String>> theCustomHeaders) {
250
251                if (!myDontValidateConformance) {
252                        myFactory.validateServerBaseIfConfiguredToDoSo(myUrlBase, myClient, this);
253                }
254
255                // TODO: handle non 2xx status codes by throwing the correct exception,
256                // and ensure it's passed upwards
257                IHttpRequest httpRequest = null;
258                IHttpResponse response = null;
259                try {
260                        Map<String, List<String>> params = createExtraParams(theCustomAcceptHeader);
261
262                        if (clientInvocation instanceof HttpGetClientInvocation) {
263                                if (myRequestFormatParamStyle == RequestFormatParamStyleEnum.SHORT && isBlank(theCustomAcceptHeader)) {
264                                        if (theEncoding == EncodingEnum.XML) {
265                                                params.put(Constants.PARAM_FORMAT, Collections.singletonList("xml"));
266                                        } else if (theEncoding == EncodingEnum.JSON) {
267                                                params.put(Constants.PARAM_FORMAT, Collections.singletonList("json"));
268                                        }
269                                }
270                        }
271
272                        if (theSummaryMode != null) {
273                                params.put(Constants.PARAM_SUMMARY, Collections.singletonList(theSummaryMode.getCode()));
274                        } else if (mySummary != null) {
275                                params.put(Constants.PARAM_SUMMARY, Collections.singletonList(mySummary.getCode()));
276                        }
277
278                        if (thePrettyPrint == Boolean.TRUE) {
279                                params.put(Constants.PARAM_PRETTY, Collections.singletonList(Constants.PARAM_PRETTY_VALUE_TRUE));
280                        }
281
282                        if (theSubsetElements != null && theSubsetElements.isEmpty() == false) {
283                                params.put(Constants.PARAM_ELEMENTS, Collections.singletonList(StringUtils.join(theSubsetElements, ',')));
284                        }
285
286                        EncodingEnum encoding = getEncoding();
287                        if (theEncoding != null) {
288                                encoding = theEncoding;
289                        }
290
291                        httpRequest = clientInvocation.asHttpRequest(myUrlBase, params, encoding, thePrettyPrint);
292
293                        if (isNotBlank(theCustomAcceptHeader)) {
294                                httpRequest.removeHeaders(Constants.HEADER_ACCEPT);
295                                httpRequest.addHeader(Constants.HEADER_ACCEPT, theCustomAcceptHeader);
296                        }
297
298                        if (theCacheControlDirective != null) {
299                                StringBuilder b = new StringBuilder();
300                                addToCacheControlHeader(b, Constants.CACHE_CONTROL_NO_CACHE, theCacheControlDirective.isNoCache());
301                                addToCacheControlHeader(b, Constants.CACHE_CONTROL_NO_STORE, theCacheControlDirective.isNoStore());
302                                if (theCacheControlDirective.getMaxResults() != null) {
303                                        addToCacheControlHeader(b, Constants.CACHE_CONTROL_MAX_RESULTS + "=" + theCacheControlDirective.getMaxResults().intValue(), true);
304                                }
305                                if (b.length() > 0) {
306                                        httpRequest.addHeader(Constants.HEADER_CACHE_CONTROL, b.toString());
307                                }
308                        }
309
310                        if (theLogRequestAndResponse) {
311                                ourLog.info("Client invoking: {}", httpRequest);
312                                String body = httpRequest.getRequestBodyFromStream();
313                                if (body != null) {
314                                        ourLog.info("Client request body: {}", body);
315                                }
316                        }
317
318                        if (theCustomHeaders != null) {
319                                AdditionalRequestHeadersInterceptor interceptor = new AdditionalRequestHeadersInterceptor(theCustomHeaders);
320                                interceptor.interceptRequest(httpRequest);
321                        }
322
323                        HookParams requestParams = new HookParams();
324                        requestParams.add(IHttpRequest.class, httpRequest);
325                        requestParams.add(IRestfulClient.class, this);
326                        getInterceptorService().callHooks(Pointcut.CLIENT_REQUEST, requestParams);
327
328                        response = httpRequest.execute();
329
330                        HookParams responseParams = new HookParams();
331                        responseParams.add(IHttpRequest.class, httpRequest);
332                        responseParams.add(IHttpResponse.class, response);
333                        responseParams.add(IRestfulClient.class, this);
334                        getInterceptorService().callHooks(Pointcut.CLIENT_RESPONSE, responseParams);
335
336                        String mimeType;
337                        if (Constants.STATUS_HTTP_204_NO_CONTENT == response.getStatus()) {
338                                mimeType = null;
339                        } else {
340                                mimeType = response.getMimeType();
341                        }
342
343                        Map<String, List<String>> headers = response.getAllHeaders();
344
345                        if (response.getStatus() < 200 || response.getStatus() > 299) {
346                                String body = null;
347                                try (Reader reader = response.createReader()) {
348                                        body = IOUtils.toString(reader);
349                                } catch (Exception e) {
350                                        ourLog.debug("Failed to read input stream", e);
351                                }
352
353                                String message = "HTTP " + response.getStatus() + " " + response.getStatusInfo();
354                                IBaseOperationOutcome oo = null;
355                                if (Constants.CT_TEXT.equals(mimeType)) {
356                                        message = message + ": " + body;
357                                } else {
358                                        EncodingEnum enc = EncodingEnum.forContentType(mimeType);
359                                        if (enc != null) {
360                                                IParser p = enc.newParser(theContext);
361                                                try {
362                                                        // TODO: handle if something other than OO comes back
363                                                        oo = (IBaseOperationOutcome) p.parseResource(body);
364                                                        String details = OperationOutcomeUtil.getFirstIssueDetails(getFhirContext(), oo);
365                                                        if (isNotBlank(details)) {
366                                                                message = message + ": " + details;
367                                                        }
368                                                } catch (Exception e) {
369                                                        ourLog.debug("Failed to process OperationOutcome response");
370                                                }
371                                        }
372                                }
373
374                                keepResponseAndLogIt(theLogRequestAndResponse, response, body);
375
376                                BaseServerResponseException exception = BaseServerResponseException.newInstance(response.getStatus(), message);
377                                exception.setOperationOutcome(oo);
378
379                                if (body != null) {
380                                        exception.setResponseBody(body);
381                                }
382
383                                throw exception;
384                        }
385                        if (binding instanceof IClientResponseHandlerHandlesBinary) {
386                                IClientResponseHandlerHandlesBinary<T> handlesBinary = (IClientResponseHandlerHandlesBinary<T>) binding;
387                                if (handlesBinary.isBinary()) {
388                                        try (InputStream reader = response.readEntity()) {
389                                                return handlesBinary.invokeClientForBinary(mimeType, reader, response.getStatus(), headers);
390                                        }
391                                }
392                        }
393
394                        try (InputStream inputStream = response.readEntity()) {
395                                InputStream inputStreamToReturn = inputStream;
396
397                                if (ourLog.isTraceEnabled() || myKeepResponses || theLogRequestAndResponse) {
398                                        if (inputStream != null) {
399                                                String responseString = IOUtils.toString(inputStream, Charsets.UTF_8);
400                                                keepResponseAndLogIt(theLogRequestAndResponse, response, responseString);
401                                                inputStreamToReturn = new ByteArrayInputStream(responseString.getBytes(Charsets.UTF_8));
402                                        }
403                                }
404
405                                if (inputStreamToReturn == null) {
406                                        inputStreamToReturn = new ByteArrayInputStream(new byte[]{});
407                                }
408
409                                return binding.invokeClient(mimeType, inputStreamToReturn, response.getStatus(), headers);
410                        }
411
412                } catch (DataFormatException e) {
413                        String msg;
414                        if (httpRequest != null) {
415                                msg = getFhirContext().getLocalizer().getMessage(BaseClient.class, "failedToParseResponse", httpRequest.getHttpVerbName(), httpRequest.getUri(), e.toString());
416                        } else {
417                                msg = getFhirContext().getLocalizer().getMessage(BaseClient.class, "failedToParseResponse", "UNKNOWN", "UNKNOWN", e.toString());
418                        }
419                        throw new FhirClientConnectionException(Msg.code(1359) + msg, e);
420                } catch (IllegalStateException e) {
421                        throw new FhirClientConnectionException(Msg.code(1360) + e);
422                } catch (IOException e) {
423                        String msg;
424                        msg = getFhirContext().getLocalizer().getMessage(BaseClient.class, "failedToParseResponse", httpRequest.getHttpVerbName(), httpRequest.getUri(), e.toString());
425                        throw new FhirClientConnectionException(Msg.code(1361) + msg, e);
426                } catch (RuntimeException e) {
427                        throw e;
428                } catch (Exception e) {
429                        throw new FhirClientConnectionException(Msg.code(1362) + e);
430                } finally {
431                        if (response != null) {
432                                response.close();
433                        }
434                }
435        }
436
437        private void addToCacheControlHeader(StringBuilder theBuilder, String theDirective, boolean theActive) {
438                if (theActive) {
439                        if (theBuilder.length() > 0) {
440                                theBuilder.append(", ");
441                        }
442                        theBuilder.append(theDirective);
443                }
444        }
445
446        /**
447         * For now, this is a part of the internal API of HAPI - Use with caution as this method may change!
448         */
449        public boolean isKeepResponses() {
450                return myKeepResponses;
451        }
452
453        /**
454         * For now, this is a part of the internal API of HAPI - Use with caution as this method may change!
455         */
456        public void setKeepResponses(boolean theKeepResponses) {
457                myKeepResponses = theKeepResponses;
458        }
459
460        /**
461         * Returns the pretty print flag, which is a request to the server for it to return "pretty printed" responses. Note
462         * that this is currently a non-standard flag (_pretty) which is supported only by HAPI based servers (and any other
463         * servers which might implement it).
464         */
465        public boolean isPrettyPrint() {
466                return Boolean.TRUE.equals(myPrettyPrint);
467        }
468
469        /**
470         * Sets the pretty print flag, which is a request to the server for it to return "pretty printed" responses. Note
471         * that this is currently a non-standard flag (_pretty) which is supported only by HAPI based servers (and any other
472         * servers which might implement it).
473         */
474        @Override
475        public void setPrettyPrint(Boolean thePrettyPrint) {
476                myPrettyPrint = thePrettyPrint;
477                // return this;
478        }
479
480        private void keepResponseAndLogIt(boolean theLogRequestAndResponse, IHttpResponse response, String responseString) {
481                if (myKeepResponses) {
482                        myLastResponse = response;
483                        myLastResponseBody = responseString;
484                }
485                if (theLogRequestAndResponse) {
486                        String message = "HTTP " + response.getStatus() + " " + response.getStatusInfo();
487                        if (StringUtils.isNotBlank(responseString)) {
488                                ourLog.info("Client response: {}\n{}", message, responseString);
489                        } else {
490                                ourLog.info("Client response: {}", message);
491                        }
492                } else {
493                        ourLog.trace("FHIR response:\n{}\n{}", response, responseString);
494                }
495        }
496
497        @Override
498        public void registerInterceptor(Object theInterceptor) {
499                Validate.notNull(theInterceptor, "Interceptor can not be null");
500                getInterceptorService().registerInterceptor(theInterceptor);
501        }
502
503        /**
504         * This method is an internal part of the HAPI API and may change, use with caution. If you want to disable the
505         * loading of conformance statements, use
506         * {@link IRestfulClientFactory#setServerValidationMode(ServerValidationModeEnum)}
507         */
508        public void setDontValidateConformance(boolean theDontValidateConformance) {
509                myDontValidateConformance = theDontValidateConformance;
510        }
511
512        @Override
513        public void unregisterInterceptor(Object theInterceptor) {
514                Validate.notNull(theInterceptor, "Interceptor can not be null");
515                getInterceptorService().unregisterInterceptor(theInterceptor);
516        }
517
518        protected final class ResourceOrBinaryResponseHandler extends ResourceResponseHandler<IBaseResource> {
519
520
521                @Override
522                public IBaseResource invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException {
523
524                        /*
525                         * For operation responses, if the response content type is a FHIR content-type
526                         * (which is will probably almost always be) we just handle it normally. However,
527                         * if we get back a successful (2xx) response from an operation, and the content
528                         * type is something other than FHIR, we'll return it as a Binary wrapped in
529                         * a Parameters resource.
530                         */
531                        EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType);
532                        if (respType != null || theResponseStatusCode < 200 || theResponseStatusCode >= 300) {
533                                return super.invokeClient(theResponseMimeType, theResponseInputStream, theResponseStatusCode, theHeaders);
534                        }
535
536                        // Create a Binary resource to return
537                        IBaseBinary responseBinary = BinaryUtil.newBinary(getFhirContext());
538
539                        // Fetch the content type
540                        String contentType = null;
541                        List<String> contentTypeHeaders = theHeaders.get(Constants.HEADER_CONTENT_TYPE_LC);
542                        if (contentTypeHeaders != null && contentTypeHeaders.size() > 0) {
543                                contentType = contentTypeHeaders.get(0);
544                        }
545                        responseBinary.setContentType(contentType);
546
547                        // Fetch the content itself
548                        try {
549                                responseBinary.setContent(IOUtils.toByteArray(theResponseInputStream));
550                        } catch (IOException e) {
551                                throw new InternalErrorException(Msg.code(1363) + "IO failure parsing response", e);
552                        }
553
554                        return responseBinary;
555                }
556
557        }
558
559        protected class ResourceResponseHandler<T extends IBaseResource> implements IClientResponseHandler<T> {
560
561                private boolean myAllowHtmlResponse;
562                private IIdType myId;
563                private List<Class<? extends IBaseResource>> myPreferResponseTypes;
564                private Class<T> myReturnType;
565
566                public ResourceResponseHandler() {
567                        this(null);
568                }
569
570                public ResourceResponseHandler(Class<T> theReturnType) {
571                        this(theReturnType, null, null);
572                }
573
574                public ResourceResponseHandler(Class<T> theReturnType, Class<? extends IBaseResource> thePreferResponseType, IIdType theId) {
575                        this(theReturnType, thePreferResponseType, theId, false);
576                }
577
578                public ResourceResponseHandler(Class<T> theReturnType, Class<? extends IBaseResource> thePreferResponseType, IIdType theId, boolean theAllowHtmlResponse) {
579                        this(theReturnType, toTypeList(thePreferResponseType), theId, theAllowHtmlResponse);
580                }
581
582                public ResourceResponseHandler(Class<T> theClass, List<Class<? extends IBaseResource>> thePreferResponseTypes) {
583                        this(theClass, thePreferResponseTypes, null, false);
584                }
585
586                public ResourceResponseHandler(Class<T> theReturnType, List<Class<? extends IBaseResource>> thePreferResponseTypes, IIdType theId, boolean theAllowHtmlResponse) {
587                        myReturnType = theReturnType;
588                        myId = theId;
589                        myPreferResponseTypes = thePreferResponseTypes;
590                        myAllowHtmlResponse = theAllowHtmlResponse;
591                }
592
593                @Override
594                public T invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException {
595                        if (theResponseStatusCode == Constants.STATUS_HTTP_204_NO_CONTENT) {
596                                return null;
597                        }
598
599                        EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType);
600                        if (respType == null) {
601                                if (myAllowHtmlResponse && theResponseMimeType.toLowerCase().contains(Constants.CT_HTML) && myReturnType != null) {
602                                        return readHtmlResponse(theResponseInputStream);
603                                }
604                                throw NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseInputStream);
605                        }
606                        IParser parser = respType.newParser(getFhirContext());
607                        parser.setServerBaseUrl(getUrlBase());
608                        if (myPreferResponseTypes != null) {
609                                parser.setPreferTypes(myPreferResponseTypes);
610                        }
611                        T retVal = parser.parseResource(myReturnType, theResponseInputStream);
612
613                        MethodUtil.parseClientRequestResourceHeaders(myId, theHeaders, retVal);
614
615                        return retVal;
616                }
617
618                @SuppressWarnings("unchecked")
619                private T readHtmlResponse(InputStream theResponseInputStream) {
620                        RuntimeResourceDefinition resDef = getFhirContext().getResourceDefinition(myReturnType);
621                        IBaseResource instance = resDef.newInstance();
622                        BaseRuntimeChildDefinition textChild = resDef.getChildByName("text");
623                        BaseRuntimeElementCompositeDefinition<?> textElement = (BaseRuntimeElementCompositeDefinition<?>) textChild.getChildByName("text");
624                        IBase textInstance = textElement.newInstance();
625                        textChild.getMutator().addValue(instance, textInstance);
626
627                        BaseRuntimeChildDefinition divChild = textElement.getChildByName("div");
628                        BaseRuntimeElementDefinition<?> divElement = divChild.getChildByName("div");
629                        IPrimitiveType<?> divInstance = (IPrimitiveType<?>) divElement.newInstance();
630                        try {
631                                divInstance.setValueAsString(IOUtils.toString(theResponseInputStream, Charsets.UTF_8));
632                        } catch (Exception e) {
633                                throw new InvalidResponseException(Msg.code(1364) + "Failed to process HTML response from server: " + e.getMessage(), 400, e);
634                        }
635                        divChild.getMutator().addValue(textInstance, divInstance);
636                        return (T) instance;
637                }
638
639                public ResourceResponseHandler<T> setPreferResponseTypes(List<Class<? extends IBaseResource>> thePreferResponseTypes) {
640                        myPreferResponseTypes = thePreferResponseTypes;
641                        return this;
642                }
643        }
644
645        static ArrayList<Class<? extends IBaseResource>> toTypeList(Class<? extends IBaseResource> thePreferResponseType) {
646                ArrayList<Class<? extends IBaseResource>> preferResponseTypes = null;
647                if (thePreferResponseType != null) {
648                        preferResponseTypes = new ArrayList<>(1);
649                        preferResponseTypes.add(thePreferResponseType);
650                }
651                return preferResponseTypes;
652        }
653
654}