
001package org.hl7.fhir.r5.utils.client.network; 002 003import java.io.IOException; 004import java.util.ArrayList; 005import java.util.List; 006import java.util.concurrent.TimeUnit; 007 008 009import org.apache.commons.lang3.StringUtils; 010import org.hl7.fhir.r5.formats.IParser; 011import org.hl7.fhir.r5.formats.JsonParser; 012import org.hl7.fhir.r5.formats.XmlParser; 013import org.hl7.fhir.r5.model.Bundle; 014import org.hl7.fhir.r5.model.OperationOutcome; 015import org.hl7.fhir.r5.model.Resource; 016import org.hl7.fhir.r5.utils.OperationOutcomeUtilities; 017import org.hl7.fhir.r5.utils.ResourceUtilities; 018import org.hl7.fhir.r5.utils.client.EFhirClientException; 019import org.hl7.fhir.r5.utils.client.ResourceFormat; 020import org.hl7.fhir.utilities.MimeType; 021import org.hl7.fhir.utilities.ToolingClientLogger; 022import org.hl7.fhir.utilities.Utilities; 023import org.hl7.fhir.utilities.http.*; 024import org.hl7.fhir.utilities.xhtml.XhtmlUtils; 025 026public class FhirRequestBuilder { 027 028 protected static final String DEFAULT_CHARSET = "UTF-8"; 029 030 protected static final String LOCATION_HEADER = "location"; 031 protected static final String CONTENT_LOCATION_HEADER = "content-location"; 032 033 private final HTTPRequest httpRequest; 034 private String resourceFormat = null; 035 private Iterable<HTTPHeader> headers = null; 036 private String message = null; 037 private int retryCount = 1; 038 /** 039 * The timeout quantity. Used in combination with {@link FhirRequestBuilder#timeoutUnit}. 040 */ 041 private long timeout = 5000; 042 /** 043 * Time unit for {@link FhirRequestBuilder#timeout}. 044 */ 045 private TimeUnit timeoutUnit = TimeUnit.MILLISECONDS; 046 047 /** 048 * {@link ToolingClientLogger} for log output. 049 */ 050 private ToolingClientLogger logger = null; 051 052 private String source; 053 054 public FhirRequestBuilder(HTTPRequest httpRequest, String source) { 055 this.source = source; 056 this.httpRequest = httpRequest; 057 } 058 059 /** 060 * Adds necessary default headers, formatting headers, and any passed in {@link HTTPHeader}s to the passed in 061 * {@link HTTPRequest} 062 * 063 * @param request {@link HTTPRequest} to add headers to. 064 * @param format Expected {@link Resource} format. 065 * @param headers Any additional {@link HTTPHeader}s to add to the request. 066 */ 067 protected static HTTPRequest formatHeaders(HTTPRequest request, String format, Iterable<HTTPHeader> headers) { 068 List<HTTPHeader> allHeaders = new ArrayList<>(); 069 request.getHeaders().forEach(allHeaders::add); 070 071 if (format != null) getResourceFormatHeaders(request, format).forEach(allHeaders::add); 072 if (headers != null) headers.forEach(allHeaders::add); 073 return request.withHeaders(allHeaders); 074 } 075 076 /** 077 * Adds necessary headers for the given resource format provided. 078 * 079 * @param httpRequest {@link HTTPRequest} to add default headers to. 080 * @param format Expected {@link Resource} format. 081 */ 082 protected static Iterable<HTTPHeader> getResourceFormatHeaders(HTTPRequest httpRequest, String format) { 083 List<HTTPHeader> headers = new ArrayList<>(); 084 headers.add(new HTTPHeader("Accept", format)); 085 if (httpRequest.getMethod() == HTTPRequest.HttpMethod.PUT 086 || httpRequest.getMethod() == HTTPRequest.HttpMethod.POST 087 || httpRequest.getMethod() == HTTPRequest.HttpMethod.PATCH 088 ) { 089 headers.add( new HTTPHeader("Content-Type", format + ";charset=" + DEFAULT_CHARSET)); 090 } 091 return headers; 092 } 093 094 /** 095 * Returns true if any of the {@link OperationOutcome.OperationOutcomeIssueComponent} within the 096 * provided {@link OperationOutcome} have an {@link OperationOutcome.IssueSeverity} of 097 * {@link OperationOutcome.IssueSeverity#ERROR} or 098 * {@link OperationOutcome.IssueSeverity#FATAL} 099 * 100 * @param oo {@link OperationOutcome} to evaluate. 101 * @return {@link Boolean#TRUE} if an error exists. 102 */ 103 protected static boolean hasError(OperationOutcome oo) { 104 return (oo.getIssue().stream() 105 .anyMatch(issue -> issue.getSeverity() == OperationOutcome.IssueSeverity.ERROR 106 || issue.getSeverity() == OperationOutcome.IssueSeverity.FATAL)); 107 } 108 109 protected ManagedFhirWebAccessor getManagedWebAccessor() { 110 return ManagedWebAccess.fhirAccessor().withRetries(retryCount).withTimeout(timeout, timeoutUnit).withLogger(logger); 111 } 112 113 public FhirRequestBuilder withResourceFormat(String resourceFormat) { 114 this.resourceFormat = resourceFormat; 115 return this; 116 } 117 118 public FhirRequestBuilder withHeaders(Iterable<HTTPHeader> headers) { 119 this.headers = headers; 120 return this; 121 } 122 123 public FhirRequestBuilder withMessage(String message) { 124 this.message = message; 125 return this; 126 } 127 128 public FhirRequestBuilder withRetryCount(int retryCount) { 129 this.retryCount = retryCount; 130 return this; 131 } 132 133 public FhirRequestBuilder withLogger(ToolingClientLogger logger) { 134 this.logger = logger; 135 return this; 136 } 137 138 public FhirRequestBuilder withTimeout(long timeout, TimeUnit unit) { 139 this.timeout = timeout; 140 this.timeoutUnit = unit; 141 return this; 142 } 143 144 public <T extends Resource> ResourceRequest<T> execute() throws IOException { 145 HTTPRequest requestWithHeaders = formatHeaders(httpRequest, resourceFormat, headers); 146 HTTPResult response = getManagedWebAccessor().httpCall(requestWithHeaders);//getHttpClient().newCall(httpRequest.build()).execute(); 147 T resource = unmarshalReference(response, resourceFormat, null); 148 return new ResourceRequest<T>(resource, response.getCode(), getLocationHeader(response.getHeaders())); 149 } 150 151 public Bundle executeAsBatch() throws IOException { 152 HTTPRequest requestWithHeaders = formatHeaders(httpRequest, resourceFormat, null); 153 HTTPResult response = getManagedWebAccessor().httpCall(requestWithHeaders); 154 return unmarshalFeed(response, resourceFormat); 155 } 156 157 /** 158 * Unmarshalls a resource from the response stream. 159 */ 160 @SuppressWarnings("unchecked") 161 protected <T extends Resource> T unmarshalReference(HTTPResult response, String format, String resourceType) { 162 int code = response.getCode(); 163 boolean ok = code >= 200 && code < 300; 164 if (response.getContent() == null) { 165 if (!ok) { 166 if (Utilities.noString(response.getMessage())) { 167 throw new EFhirClientException(code, response.getMessagefromCode()); 168 } else { 169 throw new EFhirClientException(code, response.getMessage()); 170 } 171 } else { 172 return null; 173 } 174 } 175 String body; 176 177 Resource resource = null; 178 try { 179 body = response.getContentAsString(); 180 String contentType = HTTPHeaderUtil.getSingleHeader(response.getHeaders(), "Content-Type"); 181 if (contentType == null) { 182 if (ok) { 183 resource = getParser(format).parse(body); 184 } else { 185 System.out.println("Got error response with no Content-Type from "+source+" with status "+code); 186 System.out.println(body); 187 resource = OperationOutcomeUtilities.outcomeFromTextError(body); 188 } 189 } else { 190 if (contentType.contains(";")) { 191 contentType = contentType.substring(0, contentType.indexOf(";")); 192 } 193 switch (contentType) { 194 case "application/json": 195 case "application/fhir+json": 196 if (!format.contains("json")) { 197 System.out.println("Got json response expecting "+format+" from "+source+" with status "+code); 198 } 199 resource = getParser(ResourceFormat.RESOURCE_JSON.getHeader()).parse(body); 200 break; 201 case "application/xml": 202 case "application/fhir+xml": 203 case "text/xml": 204 if (!format.contains("xml")) { 205 System.out.println("Got xml response expecting "+format+" from "+source+" with status "+code); 206 } 207 resource = getParser(ResourceFormat.RESOURCE_XML.getHeader()).parse(body); 208 break; 209 case "text/plain": 210 resource = OperationOutcomeUtilities.outcomeFromTextError(body); 211 break; 212 case "text/html" : 213 resource = OperationOutcomeUtilities.outcomeFromTextError(XhtmlUtils.convertHtmlToText(response.getContentAsString(), source)); 214 break; 215 default: // not sure what else to do? 216 System.out.println("Got content-type '"+contentType+"' from "+source); 217 System.out.println(body); 218 resource = OperationOutcomeUtilities.outcomeFromTextError(body); 219 } 220 } 221 } catch (IOException ioe) { 222 throw new EFhirClientException(0, "Error reading Http Response from "+source+":"+ioe.getMessage(), ioe); 223 } catch (Exception e) { 224 throw new EFhirClientException(0, "Error parsing response message from "+source+": "+e.getMessage(), e); 225 } 226 if (resource instanceof OperationOutcome && (!"OperationOutcome".equals(resourceType) || !ok)) { 227 OperationOutcome error = (OperationOutcome) resource; 228 if (hasError((OperationOutcome) resource)) { 229 throw new EFhirClientException(0, "Error from "+source+": " + ResourceUtilities.getErrorDescription(error), error); 230 } else { 231 // umm, weird... 232 System.out.println("Got OperationOutcome with no error from "+source+" with status "+code); 233 System.out.println(body); 234 return null; 235 } 236 } 237 if (resource == null) { 238 System.out.println("No resource from "+source+" with status "+code); 239 System.out.println(body); 240 return null; // shouldn't get here? 241 } 242 if (resourceType != null && !resource.fhirType().equals(resourceType)) { 243 throw new EFhirClientException(0, "Error parsing response message from "+source+": Found an "+resource.fhirType()+" looking for a "+resourceType); 244 } 245 return (T) resource; 246 } 247 248 /** 249 * Unmarshalls Bundle from response stream. 250 */ 251 protected Bundle unmarshalFeed(HTTPResult response, String format) { 252 return unmarshalReference(response, format, "Bundle"); 253 } 254 255 /** 256 * Returns the appropriate parser based on the format type passed in. Defaults to XML parser if a blank format is 257 * provided...because reasons. 258 * <p/> 259 * Currently supports only "json" and "xml" formats. 260 * 261 * @param format One of "json" or "xml". 262 * @return {@link JsonParser} or {@link XmlParser} 263 */ 264 protected IParser getParser(String format) { 265 if (StringUtils.isBlank(format)) { 266 format = ResourceFormat.RESOURCE_XML.getHeader(); 267 } 268 MimeType mt = new MimeType(format); 269 if (mt.getBase().equalsIgnoreCase(ResourceFormat.RESOURCE_JSON.getHeader())) { 270 return new JsonParser(); 271 } else if (mt.getBase().equalsIgnoreCase(ResourceFormat.RESOURCE_XML.getHeader())) { 272 return new XmlParser(); 273 } else { 274 throw new EFhirClientException(0, "Invalid format: " + format); 275 } 276 } 277 278 /** 279 * Extracts the 'location' header from the passed headers. If no value for 'location' exists, the 280 * value for 'content-location' is returned. If neither header exists, we return null. 281 * 282 * @param headers Headers to search for 'location' or 'content-location'. 283 */ 284 protected static String getLocationHeader(Iterable<HTTPHeader> headers) { 285 String locationHeader = HTTPHeaderUtil.getSingleHeader(headers, LOCATION_HEADER); 286 287 if (locationHeader != null) { 288 return locationHeader; 289 } 290 return HTTPHeaderUtil.getSingleHeader(headers, CONTENT_LOCATION_HEADER); 291 } 292}