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