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