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