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