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