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