001package org.hl7.fhir.dstu2.utils.client;
002
003/*-
004 * #%L
005 * org.hl7.fhir.dstu2
006 * %%
007 * Copyright (C) 2014 - 2019 Health Level 7
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
023
024
025/*
026  Copyright (c) 2011+, HL7, Inc.
027  All rights reserved.
028  
029  Redistribution and use in source and binary forms, with or without modification, 
030  are permitted provided that the following conditions are met:
031  
032   * Redistributions of source code must retain the above copyright notice, this 
033     list of conditions and the following disclaimer.
034   * Redistributions in binary form must reproduce the above copyright notice, 
035     this list of conditions and the following disclaimer in the documentation 
036     and/or other materials provided with the distribution.
037   * Neither the name of HL7 nor the names of its contributors may be used to 
038     endorse or promote products derived from this software without specific 
039     prior written permission.
040  
041  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
042  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
043  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
044  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
045  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
046  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
047  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
048  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
049  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
050  POSSIBILITY OF SUCH DAMAGE.
051  
052*/
053
054
055import java.io.ByteArrayOutputStream;
056import java.io.IOException;
057import java.io.InputStream;
058import java.io.OutputStreamWriter;
059import java.net.HttpURLConnection;
060import java.net.MalformedURLException;
061import java.net.URI;
062import java.net.URLConnection;
063import java.text.ParseException;
064import java.text.SimpleDateFormat;
065import java.util.Calendar;
066import java.util.Date;
067import java.util.List;
068import java.util.Map;
069
070import org.apache.commons.io.IOUtils;
071import org.apache.commons.lang3.StringUtils;
072import org.apache.http.Header;
073import org.apache.http.HttpEntity;
074import org.apache.http.HttpEntityEnclosingRequest;
075import org.apache.http.HttpHost;
076import org.apache.http.HttpRequest;
077import org.apache.http.HttpResponse;
078import org.apache.http.client.HttpClient;
079import org.apache.http.client.methods.HttpDelete;
080import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
081import org.apache.http.client.methods.HttpGet;
082import org.apache.http.client.methods.HttpOptions;
083import org.apache.http.client.methods.HttpPost;
084import org.apache.http.client.methods.HttpPut;
085import org.apache.http.client.methods.HttpUriRequest;
086import org.apache.http.conn.params.ConnRoutePNames;
087import org.apache.http.entity.ByteArrayEntity;
088import org.apache.http.impl.client.DefaultHttpClient;
089import org.hl7.fhir.dstu2.formats.IParser;
090import org.hl7.fhir.dstu2.formats.IParser.OutputStyle;
091import org.hl7.fhir.dstu2.formats.JsonParser;
092import org.hl7.fhir.dstu2.formats.XmlParser;
093import org.hl7.fhir.dstu2.model.Bundle;
094import org.hl7.fhir.dstu2.model.OperationOutcome;
095import org.hl7.fhir.dstu2.model.OperationOutcome.IssueSeverity;
096import org.hl7.fhir.dstu2.model.OperationOutcome.OperationOutcomeIssueComponent;
097import org.hl7.fhir.dstu2.model.Resource;
098import org.hl7.fhir.dstu2.model.ResourceType;
099import org.hl7.fhir.dstu2.utils.ResourceUtilities;
100
101/**
102 * Helper class handling lower level HTTP transport concerns.
103 * TODO Document methods.
104 * @author Claude Nanjo
105 */
106public class ClientUtils {
107        
108        public static String DEFAULT_CHARSET = "UTF-8";
109        public static final String HEADER_LOCATION = "location";
110        
111        public static <T extends Resource> ResourceRequest<T> issueOptionsRequest(URI optionsUri, String resourceFormat, HttpHost proxy) {
112                HttpOptions options = new HttpOptions(optionsUri);
113                return issueResourceRequest(resourceFormat, options, proxy);
114        }
115        
116        public static <T extends Resource> ResourceRequest<T> issueGetResourceRequest(URI resourceUri, String resourceFormat, HttpHost proxy) {
117                HttpGet httpget = new HttpGet(resourceUri);
118                return issueResourceRequest(resourceFormat, httpget, proxy);
119        }
120        
121        public static <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat, List<Header> headers, HttpHost proxy) {
122                HttpPut httpPut = new HttpPut(resourceUri);
123                return issueResourceRequest(resourceFormat, httpPut, payload, headers, proxy);
124        }
125        
126        public static <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat, HttpHost proxy) {
127                HttpPut httpPut = new HttpPut(resourceUri);
128                return issueResourceRequest(resourceFormat, httpPut, payload, null, proxy);
129        }
130        
131        public static <T extends Resource> ResourceRequest<T> issuePostRequest(URI resourceUri, byte[] payload, String resourceFormat, List<Header> headers, HttpHost proxy) {
132                HttpPost httpPost = new HttpPost(resourceUri);
133                return issueResourceRequest(resourceFormat, httpPost, payload, headers, proxy);
134        }
135        
136        
137        public static <T extends Resource> ResourceRequest<T> issuePostRequest(URI resourceUri, byte[] payload, String resourceFormat, HttpHost proxy) {
138                return issuePostRequest(resourceUri, payload, resourceFormat, null, proxy);
139        }
140        
141        public static Bundle issueGetFeedRequest(URI resourceUri, String resourceFormat, HttpHost proxy) {
142                HttpGet httpget = new HttpGet(resourceUri);
143                configureFhirRequest(httpget, resourceFormat);
144                HttpResponse response = sendRequest(httpget, proxy);
145                return unmarshalReference(response, resourceFormat);
146        }
147        
148        public static Bundle postBatchRequest(URI resourceUri, byte[] payload, String resourceFormat, HttpHost proxy) {
149                HttpPost httpPost = new HttpPost(resourceUri);
150                configureFhirRequest(httpPost, resourceFormat);
151                HttpResponse response = sendPayload(httpPost, payload, proxy);
152        return unmarshalFeed(response, resourceFormat);
153        }
154        
155        public static boolean issueDeleteRequest(URI resourceUri, HttpHost proxy) {
156                HttpDelete deleteRequest = new HttpDelete(resourceUri);
157                HttpResponse response = sendRequest(deleteRequest, proxy);
158                int responseStatusCode = response.getStatusLine().getStatusCode();
159                boolean deletionSuccessful = false;
160                if(responseStatusCode == 204) {
161                        deletionSuccessful = true;
162                }
163                return deletionSuccessful;
164        }
165                
166        /***********************************************************
167         * Request/Response Helper methods
168         ***********************************************************/
169        
170        protected static <T extends Resource> ResourceRequest<T> issueResourceRequest(String resourceFormat, HttpUriRequest request, HttpHost proxy) {
171                return issueResourceRequest(resourceFormat, request, null, proxy);
172        }
173        
174        /**
175         * @param resourceFormat
176         * @param options
177         * @return
178         */
179        protected static <T extends Resource> ResourceRequest<T> issueResourceRequest(String resourceFormat, HttpUriRequest request, byte[] payload, HttpHost proxy) {
180                return issueResourceRequest(resourceFormat, request, payload, null, proxy);
181        }
182        
183        /**
184         * @param resourceFormat
185         * @param options
186         * @return
187         */
188        protected static <T extends Resource> ResourceRequest<T> issueResourceRequest(String resourceFormat, HttpUriRequest request, byte[] payload, List<Header> headers, HttpHost proxy) {
189                configureFhirRequest(request, resourceFormat, headers);
190                HttpResponse response = null;
191                if(request instanceof HttpEntityEnclosingRequest && payload != null) {
192                        response = sendPayload((HttpEntityEnclosingRequestBase)request, payload, proxy);
193                } else if (request instanceof HttpEntityEnclosingRequest && payload == null){
194                        throw new EFhirClientException("PUT and POST requests require a non-null payload");
195                } else {
196                        response = sendRequest(request, proxy);
197                }
198                T resource = unmarshalReference(response, resourceFormat);
199                return new ResourceRequest<T>(resource, response.getStatusLine().getStatusCode(), ClientUtils.getLocationHeader(response));
200        }
201        
202        
203        /**
204         * Method adds required request headers.
205         * TODO handle JSON request as well.
206         * 
207         * @param request
208         */
209        protected static void configureFhirRequest(HttpRequest request, String format) {
210                configureFhirRequest(request, format, null);
211        }
212        
213        /**
214         * Method adds required request headers.
215         * TODO handle JSON request as well.
216         * 
217         * @param request
218         */
219                protected static void configureFhirRequest(HttpRequest request, String format, List<Header> headers) {
220                request.addHeader("User-Agent", "Java FHIR Client for FHIR");
221
222                if (format != null) {           
223                  request.addHeader("Accept",format);
224                  request.addHeader("Content-Type", format + ";charset=" + DEFAULT_CHARSET);
225                }
226    request.addHeader("Accept-Charset", DEFAULT_CHARSET);
227                if(headers != null) {
228                        for(Header header : headers) {
229                                request.addHeader(header);
230                        }
231                }
232        }
233        
234        /**
235         * Method posts request payload
236         * 
237         * @param request
238         * @param payload
239         * @return
240         */
241        protected static HttpResponse sendPayload(HttpEntityEnclosingRequestBase request, byte[] payload, HttpHost proxy) {
242                HttpResponse response = null;
243                try {
244                        HttpClient httpclient = new DefaultHttpClient();
245                        if(proxy != null) {
246                                httpclient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
247                        }
248                        request.setEntity(new ByteArrayEntity(payload));
249                        response = httpclient.execute(request);
250                } catch(IOException ioe) {
251                        throw new EFhirClientException("Error sending HTTP Post/Put Payload", ioe);
252                }
253                return response;
254        }
255        
256        /**
257         * 
258         * @param request
259         * @param payload
260         * @return
261         */
262        protected static HttpResponse sendRequest(HttpUriRequest request, HttpHost proxy) {
263                HttpResponse response = null;
264                try {
265                        HttpClient httpclient = new DefaultHttpClient();
266                        if(proxy != null) {
267                                httpclient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
268                        }
269                        response = httpclient.execute(request);
270                } catch(IOException ioe) {
271                        throw new EFhirClientException("Error sending Http Request", ioe);
272                }
273                return response;
274        }
275        
276        /**
277         * Unmarshals a resource from the response stream.
278         * 
279         * @param response
280         * @return
281         */
282        @SuppressWarnings("unchecked")
283        protected static <T extends Resource> T unmarshalReference(HttpResponse response, String format) {
284                T resource = null;
285                OperationOutcome error = null;
286                InputStream instream = null;
287                HttpEntity entity = response.getEntity();
288                if (entity != null && entity.getContentLength() > 0) {
289                        try {
290                            instream = entity.getContent();
291//                          System.out.println(writeInputStreamAsString(instream));
292                            resource = (T)getParser(format).parse(instream);
293                        if (resource instanceof OperationOutcome && hasError((OperationOutcome)resource)) {
294                                error = (OperationOutcome) resource;
295                        }
296                        } catch(IOException ioe) {
297                                throw new EFhirClientException("Error unmarshalling entity from Http Response", ioe);
298                        } catch(Exception e) {
299                                throw new EFhirClientException("Error parsing response message", e);
300                        } finally {
301                                try{instream.close();}catch(IOException ioe){/* TODO log error */}
302                        }
303                }
304                if(error != null) {
305                        throw new EFhirClientException("Error unmarshalling resource: "+ResourceUtilities.getErrorDescription(error), error);
306                }
307                return resource;
308        }
309        
310        /**
311         * Unmarshals Bundle from response stream.
312         * 
313         * @param response
314         * @return
315         */
316        protected static Bundle unmarshalFeed(HttpResponse response, String format) {
317    Bundle feed = null;
318                InputStream instream = null;
319                HttpEntity entity = response.getEntity();
320                String contentType = response.getHeaders("Content-Type")[0].getValue();
321                OperationOutcome error = null;
322                try {
323                        if (entity != null) {
324                            instream = entity.getContent();
325                            if(contentType.contains(ResourceFormat.RESOURCE_XML.getHeader()) || contentType.contains("text/xml+fhir")) {
326//                              error = (OperationOutcome)getParser(ResourceFormat.RESOURCE_XML.getHeader()).parse(instream);
327//                          } else {
328                                Resource rf = getParser(format).parse(instream);
329                                if (rf instanceof Bundle)
330                                  feed = (Bundle) rf;
331                                else if (rf instanceof OperationOutcome && hasError((OperationOutcome) rf)) {
332                                        error = (OperationOutcome) rf;
333                                        } else {
334                                                throw new EFhirClientException("Error unmarshalling feed from Http Response: a resource was returned instead");
335                                        }
336                            }
337                            instream.close();
338                        }
339                } catch(IOException ioe) {
340                        throw new EFhirClientException("Error unmarshalling feed from Http Response", ioe);
341                } catch(Exception e) {
342                        throw new EFhirClientException("Error parsing response message", e);
343                } finally {
344                        try{instream.close();}catch(IOException ioe){/* TODO log error */}
345                }
346                if(error != null) {
347                        throw new EFhirClientException("Error unmarshalling feed: "+ResourceUtilities.getErrorDescription(error), error);
348                }
349                return feed;
350        }
351        
352        private static boolean hasError(OperationOutcome oo) {
353                for (OperationOutcomeIssueComponent t : oo.getIssue())
354                        if (t.getSeverity() == IssueSeverity.ERROR || t.getSeverity() == IssueSeverity.FATAL)
355                                return true;
356          return false;
357  }
358
359        protected static String getLocationHeader(HttpResponse response) {
360                String location = null;
361                if(response.getHeaders("location").length > 0) {//TODO Distinguish between both cases if necessary
362                location = response.getHeaders("location")[0].getValue();
363        } else if(response.getHeaders("content-location").length > 0) {
364                location = response.getHeaders("content-location")[0].getValue();
365        }
366                return location;
367        }
368        
369        
370        /*****************************************************************
371         * Client connection methods
372         * ***************************************************************/
373        
374        public static HttpURLConnection buildConnection(URI baseServiceUri, String tail) {
375                try {
376                        HttpURLConnection client = (HttpURLConnection) baseServiceUri.resolve(tail).toURL().openConnection();
377                        return client;
378                } catch(MalformedURLException mue) {
379                        throw new EFhirClientException("Invalid Service URL", mue);
380                } catch(IOException ioe) {
381                        throw new EFhirClientException("Unable to establish connection to server: " + baseServiceUri.toString() + tail, ioe);
382                }
383        }
384        
385        public static HttpURLConnection buildConnection(URI baseServiceUri, ResourceType resourceType, String id) {
386                return buildConnection(baseServiceUri, ResourceAddress.buildRelativePathFromResourceType(resourceType, id));
387        }
388        
389        /******************************************************************
390         * Other general helper methods
391         * ****************************************************************/
392         
393        
394        public  static <T extends Resource>  byte[] getResourceAsByteArray(T resource, boolean pretty, boolean isJson) {
395                ByteArrayOutputStream baos = null;
396                byte[] byteArray = null;
397                try {
398                        baos = new ByteArrayOutputStream();
399                        IParser parser = null;
400                        if(isJson) {
401                                parser = new JsonParser();
402                        } else {
403                                parser = new XmlParser();
404                        }
405      parser.setOutputStyle(pretty ? OutputStyle.PRETTY : OutputStyle.NORMAL);
406                        parser.compose(baos, resource);
407                        baos.close();
408                        byteArray =  baos.toByteArray();
409                        baos.close();
410                } catch (Exception e) {
411                        try{
412                                baos.close();
413                        }catch(Exception ex) {
414                                throw new EFhirClientException("Error closing output stream", ex);
415                        }
416                        throw new EFhirClientException("Error converting output stream to byte array", e);
417                }
418                return byteArray;
419        }
420        
421        public  static byte[] getFeedAsByteArray(Bundle feed, boolean pretty, boolean isJson) {
422                ByteArrayOutputStream baos = null;
423                byte[] byteArray = null;
424                try {
425                        baos = new ByteArrayOutputStream();
426                        IParser parser = null;
427                        if(isJson) {
428                                parser = new JsonParser();
429                        } else {
430                                parser = new XmlParser();
431                        }
432      parser.setOutputStyle(pretty ? OutputStyle.PRETTY : OutputStyle.NORMAL);
433                        parser.compose(baos, feed);
434                        baos.close();
435                        byteArray =  baos.toByteArray();
436                        baos.close();
437                } catch (Exception e) {
438                        try{
439                                baos.close();
440                        }catch(Exception ex) {
441                                throw new EFhirClientException("Error closing output stream", ex);
442                        }
443                        throw new EFhirClientException("Error converting output stream to byte array", e);
444                }
445                return byteArray;
446        }
447        
448        public static Calendar getLastModifiedResponseHeaderAsCalendarObject(URLConnection serverConnection) {
449                String dateTime = null;
450                try {
451                        dateTime = serverConnection.getHeaderField("Last-Modified");
452                        SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz");
453                        Date lastModifiedTimestamp = format.parse(dateTime);
454                        Calendar calendar=Calendar.getInstance();
455                        calendar.setTime(lastModifiedTimestamp);
456                        return calendar;
457                } catch(ParseException pe) {
458                        throw new EFhirClientException("Error parsing Last-Modified response header " + dateTime, pe);
459                }
460        }
461        
462        protected static IParser getParser(String format) {
463                if(StringUtils.isBlank(format)) {
464                        format = ResourceFormat.RESOURCE_XML.getHeader();
465                }
466                if(format.equalsIgnoreCase("json") || format.equalsIgnoreCase(ResourceFormat.RESOURCE_JSON.getHeader()) || format.equalsIgnoreCase(ResourceFormat.RESOURCE_JSON.getHeader())) {
467                        return new JsonParser();
468                } else if(format.equalsIgnoreCase("xml") || format.equalsIgnoreCase(ResourceFormat.RESOURCE_XML.getHeader()) || format.equalsIgnoreCase(ResourceFormat.RESOURCE_XML.getHeader())) {
469                        return new XmlParser();
470                } else {
471                        throw new EFhirClientException("Invalid format: " + format);
472                }
473        }
474        
475        /**
476         * Used for debugging
477         * 
478         * @param instream
479         * @return
480         */
481        protected static String writeInputStreamAsString(InputStream instream) {
482                String value = null;
483                try {
484                        value = IOUtils.toString(instream, "UTF-8");
485                        System.out.println(value);
486                        
487                } catch(IOException ioe) {
488                        //Do nothing
489                }
490                return value;
491        }
492        
493  public static Bundle issuePostFeedRequest(URI resourceUri, Map<String, String> parameters, String resourceName, Resource resource, String resourceFormat) throws IOException {
494    HttpPost httppost = new HttpPost(resourceUri);
495    String boundary = "----WebKitFormBoundarykbMUo6H8QaUnYtRy";
496    httppost.addHeader("Content-Type", "multipart/form-data; boundary="+boundary);
497    httppost.addHeader("Accept", resourceFormat);
498    configureFhirRequest(httppost, null);
499    HttpResponse response = sendPayload(httppost, encodeFormSubmission(parameters, resourceName, resource, boundary));
500    return unmarshalFeed(response, resourceFormat);
501  }
502  
503  private static byte[] encodeFormSubmission(Map<String, String> parameters, String resourceName, Resource resource, String boundary) throws IOException {
504    ByteArrayOutputStream b = new ByteArrayOutputStream();
505    OutputStreamWriter w = new OutputStreamWriter(b, "UTF-8");  
506    for (String name : parameters.keySet()) {
507      w.write("--");
508      w.write(boundary);
509      w.write("\r\nContent-Disposition: form-data; name=\""+name+"\"\r\n\r\n");
510      w.write(parameters.get(name)+"\r\n");
511    }
512    w.write("--");
513    w.write(boundary);
514    w.write("\r\nContent-Disposition: form-data; name=\""+resourceName+"\"\r\n\r\n");
515    w.close(); 
516    JsonParser json = new JsonParser();
517    json.setOutputStyle(OutputStyle.NORMAL);
518    json.compose(b, resource);
519    b.close();
520    w = new OutputStreamWriter(b, "UTF-8");  
521    w.write("\r\n--");
522    w.write(boundary);
523    w.write("--");
524    w.close();
525    return b.toByteArray();
526  }
527
528  /**
529   * Method posts request payload
530   * 
531   * @param request
532   * @param payload
533   * @return
534   */
535  protected static HttpResponse sendPayload(HttpEntityEnclosingRequestBase request, byte[] payload) {
536    HttpResponse response = null;
537    try {
538      HttpClient httpclient = new DefaultHttpClient();
539      request.setEntity(new ByteArrayEntity(payload));
540      response = httpclient.execute(request);
541    } catch(IOException ioe) {
542      throw new EFhirClientException("Error sending HTTP Post/Put Payload: "+ioe.getMessage(), ioe);
543    }
544    return response;
545  }
546  
547
548}