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