001package org.hl7.fhir.dstu3.utils.client; 002 003/*- 004 * #%L 005 * org.hl7.fhir.dstu3 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.io.UnsupportedEncodingException; 060import java.net.HttpURLConnection; 061import java.net.MalformedURLException; 062import java.net.URI; 063import java.net.URLConnection; 064import java.nio.charset.StandardCharsets; 065import java.text.ParseException; 066import java.text.SimpleDateFormat; 067import java.util.ArrayList; 068import java.util.Calendar; 069import java.util.Date; 070import java.util.List; 071import java.util.Map; 072 073import org.apache.commons.codec.binary.Base64; 074import org.apache.commons.io.IOUtils; 075import org.apache.commons.lang3.StringUtils; 076import org.apache.http.Header; 077import org.apache.http.HttpEntityEnclosingRequest; 078import org.apache.http.HttpHost; 079import org.apache.http.HttpRequest; 080import org.apache.http.HttpResponse; 081import org.apache.http.client.HttpClient; 082import org.apache.http.client.methods.HttpDelete; 083import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; 084import org.apache.http.client.methods.HttpGet; 085import org.apache.http.client.methods.HttpOptions; 086import org.apache.http.client.methods.HttpPost; 087import org.apache.http.client.methods.HttpPut; 088import org.apache.http.client.methods.HttpUriRequest; 089import org.apache.http.conn.params.ConnRoutePNames; 090import org.apache.http.entity.ByteArrayEntity; 091import org.apache.http.impl.client.DefaultHttpClient; 092import org.apache.http.params.HttpConnectionParams; 093import org.apache.http.params.HttpParams; 094import org.hl7.fhir.dstu3.formats.IParser; 095import org.hl7.fhir.dstu3.formats.IParser.OutputStyle; 096import org.hl7.fhir.dstu3.formats.JsonParser; 097import org.hl7.fhir.dstu3.formats.XmlParser; 098import org.hl7.fhir.dstu3.model.Bundle; 099import org.hl7.fhir.dstu3.model.OperationOutcome; 100import org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity; 101import org.hl7.fhir.dstu3.model.OperationOutcome.OperationOutcomeIssueComponent; 102import org.hl7.fhir.dstu3.model.Resource; 103import org.hl7.fhir.dstu3.model.ResourceType; 104import org.hl7.fhir.dstu3.utils.ResourceUtilities; 105 106/** 107 * Helper class handling lower level HTTP transport concerns. 108 * TODO Document methods. 109 * @author Claude Nanjo 110 */ 111public class ClientUtils { 112 113 public static final String DEFAULT_CHARSET = "UTF-8"; 114 public static final String HEADER_LOCATION = "location"; 115 116 private HttpHost proxy; 117 private int timeout = 5000; 118 private String username; 119 private String password; 120 private ToolingClientLogger logger; 121 122 public HttpHost getProxy() { 123 return proxy; 124 } 125 126 public void setProxy(HttpHost proxy) { 127 this.proxy = proxy; 128 } 129 130 public int getTimeout() { 131 return timeout; 132 } 133 134 public void setTimeout(int timeout) { 135 this.timeout = timeout; 136 } 137 138 public String getUsername() { 139 return username; 140 } 141 142 public void setUsername(String username) { 143 this.username = username; 144 } 145 146 public String getPassword() { 147 return password; 148 } 149 150 public void setPassword(String password) { 151 this.password = password; 152 } 153 154 public <T extends Resource> ResourceRequest<T> issueOptionsRequest(URI optionsUri, String resourceFormat) { 155 HttpOptions options = new HttpOptions(optionsUri); 156 return issueResourceRequest(resourceFormat, options); 157 } 158 159 public <T extends Resource> ResourceRequest<T> issueGetResourceRequest(URI resourceUri, String resourceFormat) { 160 HttpGet httpget = new HttpGet(resourceUri); 161 return issueResourceRequest(resourceFormat, httpget); 162 } 163 164 public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat, List<Header> headers) { 165 HttpPut httpPut = new HttpPut(resourceUri); 166 return issueResourceRequest(resourceFormat, httpPut, payload, headers); 167 } 168 169 public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat) { 170 HttpPut httpPut = new HttpPut(resourceUri); 171 return issueResourceRequest(resourceFormat, httpPut, payload, null); 172 } 173 174 public <T extends Resource> ResourceRequest<T> issuePostRequest(URI resourceUri, byte[] payload, String resourceFormat, List<Header> headers) { 175 HttpPost httpPost = new HttpPost(resourceUri); 176 return issueResourceRequest(resourceFormat, httpPost, payload, headers); 177 } 178 179 180 public <T extends Resource> ResourceRequest<T> issuePostRequest(URI resourceUri, byte[] payload, String resourceFormat) { 181 return issuePostRequest(resourceUri, payload, resourceFormat, null); 182 } 183 184 public Bundle issueGetFeedRequest(URI resourceUri, String resourceFormat) { 185 HttpGet httpget = new HttpGet(resourceUri); 186 configureFhirRequest(httpget, resourceFormat); 187 HttpResponse response = sendRequest(httpget); 188 return unmarshalReference(response, resourceFormat); 189 } 190 191 private void setAuth(HttpRequest httpget) { 192 if (password != null) { 193 try { 194 byte[] b = Base64.encodeBase64((username+":"+password).getBytes("ASCII")); 195 String b64 = new String(b, StandardCharsets.US_ASCII); 196 httpget.setHeader("Authorization", "Basic " + b64); 197 } catch (UnsupportedEncodingException e) { 198 } 199 } 200 } 201 202 public Bundle postBatchRequest(URI resourceUri, byte[] payload, String resourceFormat) { 203 HttpPost httpPost = new HttpPost(resourceUri); 204 configureFhirRequest(httpPost, resourceFormat); 205 HttpResponse response = sendPayload(httpPost, payload, proxy); 206 return unmarshalFeed(response, resourceFormat); 207 } 208 209 public boolean issueDeleteRequest(URI resourceUri) { 210 HttpDelete deleteRequest = new HttpDelete(resourceUri); 211 HttpResponse response = sendRequest(deleteRequest); 212 int responseStatusCode = response.getStatusLine().getStatusCode(); 213 boolean deletionSuccessful = false; 214 if(responseStatusCode == 204) { 215 deletionSuccessful = true; 216 } 217 return deletionSuccessful; 218 } 219 220 /*********************************************************** 221 * Request/Response Helper methods 222 ***********************************************************/ 223 224 protected <T extends Resource> ResourceRequest<T> issueResourceRequest(String resourceFormat, HttpUriRequest request) { 225 return issueResourceRequest(resourceFormat, request, null); 226 } 227 228 /** 229 * @param resourceFormat 230 * @param options 231 * @return 232 */ 233 protected <T extends Resource> ResourceRequest<T> issueResourceRequest(String resourceFormat, HttpUriRequest request, byte[] payload) { 234 return issueResourceRequest(resourceFormat, request, payload, null); 235 } 236 237 /** 238 * @param resourceFormat 239 * @param options 240 * @return 241 */ 242 protected <T extends Resource> ResourceRequest<T> issueResourceRequest(String resourceFormat, HttpUriRequest request, byte[] payload, List<Header> headers) { 243 configureFhirRequest(request, resourceFormat, headers); 244 HttpResponse response = null; 245 if(request instanceof HttpEntityEnclosingRequest && payload != null) { 246 response = sendPayload((HttpEntityEnclosingRequestBase)request, payload, proxy); 247 } else if (request instanceof HttpEntityEnclosingRequest && payload == null){ 248 throw new EFhirClientException("PUT and POST requests require a non-null payload"); 249 } else { 250 response = sendRequest(request); 251 } 252 T resource = unmarshalReference(response, resourceFormat); 253 return new ResourceRequest<T>(resource, response.getStatusLine().getStatusCode(), getLocationHeader(response)); 254 } 255 256 257 /** 258 * Method adds required request headers. 259 * TODO handle JSON request as well. 260 * 261 * @param request 262 */ 263 protected void configureFhirRequest(HttpRequest request, String format) { 264 configureFhirRequest(request, format, null); 265 } 266 267 /** 268 * Method adds required request headers. 269 * TODO handle JSON request as well. 270 * 271 * @param request 272 */ 273 protected void configureFhirRequest(HttpRequest request, String format, List<Header> headers) { 274 request.addHeader("User-Agent", "Java FHIR Client for FHIR"); 275 276 if (format != null) { 277 request.addHeader("Accept",format); 278 request.addHeader("Content-Type", format + ";charset=" + DEFAULT_CHARSET); 279 } 280 request.addHeader("Accept-Charset", DEFAULT_CHARSET); 281 if(headers != null) { 282 for(Header header : headers) { 283 request.addHeader(header); 284 } 285 } 286 setAuth(request); 287 } 288 289 /** 290 * Method posts request payload 291 * 292 * @param request 293 * @param payload 294 * @return 295 */ 296 protected HttpResponse sendPayload(HttpEntityEnclosingRequestBase request, byte[] payload, HttpHost proxy) { 297 HttpResponse response = null; 298 try { 299 HttpClient httpclient = new DefaultHttpClient(); 300 if(proxy != null) { 301 httpclient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy); 302 } 303 request.setEntity(new ByteArrayEntity(payload)); 304 log(request); 305 response = httpclient.execute(request); 306 } catch(IOException ioe) { 307 throw new EFhirClientException("Error sending HTTP Post/Put Payload", ioe); 308 } 309 return response; 310 } 311 312 /** 313 * 314 * @param request 315 * @param payload 316 * @return 317 */ 318 protected HttpResponse sendRequest(HttpUriRequest request) { 319 HttpResponse response = null; 320 try { 321 HttpClient httpclient = new DefaultHttpClient(); 322 log(request); 323 HttpParams params = httpclient.getParams(); 324 HttpConnectionParams.setConnectionTimeout(params, timeout); 325 HttpConnectionParams.setSoTimeout(params, timeout); 326 if(proxy != null) { 327 httpclient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy); 328 } 329 response = httpclient.execute(request); 330 } catch(IOException ioe) { 331 throw new EFhirClientException("Error sending Http Request: "+ioe.getMessage(), ioe); 332 } 333 return response; 334 } 335 336 /** 337 * Unmarshals a resource from the response stream. 338 * 339 * @param response 340 * @return 341 */ 342 @SuppressWarnings("unchecked") 343 protected <T extends Resource> T unmarshalReference(HttpResponse response, String format) { 344 T resource = null; 345 OperationOutcome error = null; 346 byte[] cnt = log(response); 347 if (cnt != null) { 348 try { 349 resource = (T)getParser(format).parse(cnt); 350 if (resource instanceof OperationOutcome && hasError((OperationOutcome)resource)) { 351 error = (OperationOutcome) resource; 352 } 353 } catch(IOException ioe) { 354 throw new EFhirClientException("Error reading Http Response: "+ioe.getMessage(), ioe); 355 } catch(Exception e) { 356 throw new EFhirClientException("Error parsing response message: "+e.getMessage(), e); 357 } 358 } 359 if(error != null) { 360 throw new EFhirClientException("Error from server: "+ResourceUtilities.getErrorDescription(error), error); 361 } 362 return resource; 363 } 364 365 /** 366 * Unmarshals Bundle from response stream. 367 * 368 * @param response 369 * @return 370 */ 371 protected Bundle unmarshalFeed(HttpResponse response, String format) { 372 Bundle feed = null; 373 byte[] cnt = log(response); 374 String contentType = response.getHeaders("Content-Type")[0].getValue(); 375 OperationOutcome error = null; 376 try { 377 if (cnt != null) { 378 if(contentType.contains(ResourceFormat.RESOURCE_XML.getHeader()) || contentType.contains("text/xml+fhir")) { 379 Resource rf = getParser(format).parse(cnt); 380 if (rf instanceof Bundle) 381 feed = (Bundle) rf; 382 else if (rf instanceof OperationOutcome && hasError((OperationOutcome) rf)) { 383 error = (OperationOutcome) rf; 384 } else { 385 throw new EFhirClientException("Error reading server response: a resource was returned instead"); 386 } 387 } 388 } 389 } catch(IOException ioe) { 390 throw new EFhirClientException("Error reading Http Response", ioe); 391 } catch(Exception e) { 392 throw new EFhirClientException("Error parsing response message", e); 393 } 394 if(error != null) { 395 throw new EFhirClientException("Error from server: "+ResourceUtilities.getErrorDescription(error), error); 396 } 397 return feed; 398 } 399 400 private boolean hasError(OperationOutcome oo) { 401 for (OperationOutcomeIssueComponent t : oo.getIssue()) 402 if (t.getSeverity() == IssueSeverity.ERROR || t.getSeverity() == IssueSeverity.FATAL) 403 return true; 404 return false; 405 } 406 407 protected String getLocationHeader(HttpResponse response) { 408 String location = null; 409 if(response.getHeaders("location").length > 0) {//TODO Distinguish between both cases if necessary 410 location = response.getHeaders("location")[0].getValue(); 411 } else if(response.getHeaders("content-location").length > 0) { 412 location = response.getHeaders("content-location")[0].getValue(); 413 } 414 return location; 415 } 416 417 418 /***************************************************************** 419 * Client connection methods 420 * ***************************************************************/ 421 422 public HttpURLConnection buildConnection(URI baseServiceUri, String tail) { 423 try { 424 HttpURLConnection client = (HttpURLConnection) baseServiceUri.resolve(tail).toURL().openConnection(); 425 return client; 426 } catch(MalformedURLException mue) { 427 throw new EFhirClientException("Invalid Service URL", mue); 428 } catch(IOException ioe) { 429 throw new EFhirClientException("Unable to establish connection to server: " + baseServiceUri.toString() + tail, ioe); 430 } 431 } 432 433 public HttpURLConnection buildConnection(URI baseServiceUri, ResourceType resourceType, String id) { 434 return buildConnection(baseServiceUri, ResourceAddress.buildRelativePathFromResourceType(resourceType, id)); 435 } 436 437 /****************************************************************** 438 * Other general helper methods 439 * ****************************************************************/ 440 441 442 public <T extends Resource> byte[] getResourceAsByteArray(T resource, boolean pretty, boolean isJson) { 443 ByteArrayOutputStream baos = null; 444 byte[] byteArray = null; 445 try { 446 baos = new ByteArrayOutputStream(); 447 IParser parser = null; 448 if(isJson) { 449 parser = new JsonParser(); 450 } else { 451 parser = new XmlParser(); 452 } 453 parser.setOutputStyle(pretty ? OutputStyle.PRETTY : OutputStyle.NORMAL); 454 parser.compose(baos, resource); 455 baos.close(); 456 byteArray = baos.toByteArray(); 457 baos.close(); 458 } catch (Exception e) { 459 try{ 460 baos.close(); 461 }catch(Exception ex) { 462 throw new EFhirClientException("Error closing output stream", ex); 463 } 464 throw new EFhirClientException("Error converting output stream to byte array", e); 465 } 466 return byteArray; 467 } 468 469 public byte[] getFeedAsByteArray(Bundle feed, boolean pretty, boolean isJson) { 470 ByteArrayOutputStream baos = null; 471 byte[] byteArray = null; 472 try { 473 baos = new ByteArrayOutputStream(); 474 IParser parser = null; 475 if(isJson) { 476 parser = new JsonParser(); 477 } else { 478 parser = new XmlParser(); 479 } 480 parser.setOutputStyle(pretty ? OutputStyle.PRETTY : OutputStyle.NORMAL); 481 parser.compose(baos, feed); 482 baos.close(); 483 byteArray = baos.toByteArray(); 484 baos.close(); 485 } catch (Exception e) { 486 try{ 487 baos.close(); 488 }catch(Exception ex) { 489 throw new EFhirClientException("Error closing output stream", ex); 490 } 491 throw new EFhirClientException("Error converting output stream to byte array", e); 492 } 493 return byteArray; 494 } 495 496 public Calendar getLastModifiedResponseHeaderAsCalendarObject(URLConnection serverConnection) { 497 String dateTime = null; 498 try { 499 dateTime = serverConnection.getHeaderField("Last-Modified"); 500 SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz"); 501 Date lastModifiedTimestamp = format.parse(dateTime); 502 Calendar calendar=Calendar.getInstance(); 503 calendar.setTime(lastModifiedTimestamp); 504 return calendar; 505 } catch(ParseException pe) { 506 throw new EFhirClientException("Error parsing Last-Modified response header " + dateTime, pe); 507 } 508 } 509 510 protected IParser getParser(String format) { 511 if(StringUtils.isBlank(format)) { 512 format = ResourceFormat.RESOURCE_XML.getHeader(); 513 } 514 if(format.equalsIgnoreCase("json") || format.equalsIgnoreCase(ResourceFormat.RESOURCE_JSON.getHeader()) || format.equalsIgnoreCase(ResourceFormat.RESOURCE_JSON.getHeader())) { 515 return new JsonParser(); 516 } else if(format.equalsIgnoreCase("xml") || format.equalsIgnoreCase(ResourceFormat.RESOURCE_XML.getHeader()) || format.equalsIgnoreCase(ResourceFormat.RESOURCE_XML.getHeader())) { 517 return new XmlParser(); 518 } else { 519 throw new EFhirClientException("Invalid format: " + format); 520 } 521 } 522 523 public Bundle issuePostFeedRequest(URI resourceUri, Map<String, String> parameters, String resourceName, Resource resource, String resourceFormat) throws IOException { 524 HttpPost httppost = new HttpPost(resourceUri); 525 String boundary = "----WebKitFormBoundarykbMUo6H8QaUnYtRy"; 526 httppost.addHeader("Content-Type", "multipart/form-data; boundary="+boundary); 527 httppost.addHeader("Accept", resourceFormat); 528 configureFhirRequest(httppost, null); 529 HttpResponse response = sendPayload(httppost, encodeFormSubmission(parameters, resourceName, resource, boundary)); 530 return unmarshalFeed(response, resourceFormat); 531 } 532 533 private byte[] encodeFormSubmission(Map<String, String> parameters, String resourceName, Resource resource, String boundary) throws IOException { 534 ByteArrayOutputStream b = new ByteArrayOutputStream(); 535 OutputStreamWriter w = new OutputStreamWriter(b, "UTF-8"); 536 for (String name : parameters.keySet()) { 537 w.write("--"); 538 w.write(boundary); 539 w.write("\r\nContent-Disposition: form-data; name=\""+name+"\"\r\n\r\n"); 540 w.write(parameters.get(name)+"\r\n"); 541 } 542 w.write("--"); 543 w.write(boundary); 544 w.write("\r\nContent-Disposition: form-data; name=\""+resourceName+"\"\r\n\r\n"); 545 w.close(); 546 JsonParser json = new JsonParser(); 547 json.setOutputStyle(OutputStyle.NORMAL); 548 json.compose(b, resource); 549 b.close(); 550 w = new OutputStreamWriter(b, "UTF-8"); 551 w.write("\r\n--"); 552 w.write(boundary); 553 w.write("--"); 554 w.close(); 555 return b.toByteArray(); 556 } 557 558 /** 559 * Method posts request payload 560 * 561 * @param request 562 * @param payload 563 * @return 564 */ 565 protected HttpResponse sendPayload(HttpEntityEnclosingRequestBase request, byte[] payload) { 566 HttpResponse response = null; 567 try { 568 log(request); 569 HttpClient httpclient = new DefaultHttpClient(); 570 request.setEntity(new ByteArrayEntity(payload)); 571 response = httpclient.execute(request); 572 log(response); 573 } catch(IOException ioe) { 574 throw new EFhirClientException("Error sending HTTP Post/Put Payload: "+ioe.getMessage(), ioe); 575 } 576 return response; 577 } 578 579 private void log(HttpUriRequest request) { 580 if (logger != null) { 581 List<String> headers = new ArrayList<>(); 582 for (Header h : request.getAllHeaders()) { 583 headers.add(h.toString()); 584 } 585 logger.logRequest(request.getMethod(), request.getURI().toString(), headers, null); 586 } 587 } 588 private void log(HttpEntityEnclosingRequestBase request) { 589 if (logger != null) { 590 List<String> headers = new ArrayList<>(); 591 for (Header h : request.getAllHeaders()) { 592 headers.add(h.toString()); 593 } 594 byte[] cnt = null; 595 InputStream s; 596 try { 597 s = request.getEntity().getContent(); 598 cnt = IOUtils.toByteArray(s); 599 s.close(); 600 } catch (Exception e) { 601 } 602 logger.logRequest(request.getMethod(), request.getURI().toString(), headers, cnt); 603 } 604 } 605 606 private byte[] log(HttpResponse response) { 607 byte[] cnt = null; 608 try { 609 InputStream s = response.getEntity().getContent(); 610 cnt = IOUtils.toByteArray(s); 611 s.close(); 612 } catch (Exception e) { 613 } 614 if (logger != null) { 615 List<String> headers = new ArrayList<>(); 616 for (Header h : response.getAllHeaders()) { 617 headers.add(h.toString()); 618 } 619 logger.logResponse(response.getStatusLine().toString(), headers, cnt); 620 } 621 return cnt; 622 } 623 624 public ToolingClientLogger getLogger() { 625 return logger; 626 } 627 628 public void setLogger(ToolingClientLogger logger) { 629 this.logger = logger; 630 } 631 632 633 /** 634 * Used for debugging 635 * 636 * @param instream 637 * @return 638 */ 639 protected String writeInputStreamAsString(InputStream instream) { 640 String value = null; 641 try { 642 value = IOUtils.toString(instream, "UTF-8"); 643 System.out.println(value); 644 645 } catch(IOException ioe) { 646 //Do nothing 647 } 648 return value; 649 } 650 651 652}