001package org.hl7.fhir.r4b.utils.client;
002
003import okhttp3.Headers;
004import okhttp3.internal.http2.Header;
005import org.hl7.fhir.exceptions.FHIRException;
006
007/*
008  Copyright (c) 2011+, HL7, Inc.
009  All rights reserved.
010  
011  Redistribution and use in source and binary forms, with or without modification, 
012  are permitted provided that the following conditions are met:
013  
014   * Redistributions of source code must retain the above copyright notice, this 
015     list of conditions and the following disclaimer.
016   * Redistributions in binary form must reproduce the above copyright notice, 
017     this list of conditions and the following disclaimer in the documentation 
018     and/or other materials provided with the distribution.
019   * Neither the name of HL7 nor the names of its contributors may be used to 
020     endorse or promote products derived from this software without specific 
021     prior written permission.
022  
023  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
024  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
025  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
026  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
027  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
028  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
029  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
030  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
031  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
032  POSSIBILITY OF SUCH DAMAGE.
033  
034*/
035
036import org.hl7.fhir.r4b.model.*;
037import org.hl7.fhir.r4b.model.Parameters.ParametersParameterComponent;
038import org.hl7.fhir.r4b.utils.client.network.ByteUtils;
039import org.hl7.fhir.r4b.utils.client.network.Client;
040import org.hl7.fhir.r4b.utils.client.network.ClientHeaders;
041import org.hl7.fhir.r4b.utils.client.network.ResourceRequest;
042import org.hl7.fhir.utilities.ToolingClientLogger;
043import org.hl7.fhir.utilities.Utilities;
044
045import java.io.IOException;
046import java.net.MalformedURLException;
047import java.net.URI;
048import java.net.URISyntaxException;
049import java.util.*;
050
051/**
052 * Very Simple RESTful client. This is purely for use in the standalone
053 * tools jar packages. It doesn't support many features, only what the tools
054 * need.
055 * <p>
056 * To use, initialize class and set base service URI as follows:
057 *
058 * <pre><code>
059 * FHIRSimpleClient fhirClient = new FHIRSimpleClient();
060 * fhirClient.initialize("http://my.fhir.domain/myServiceRoot");
061 * </code></pre>
062 * <p>
063 * Default Accept and Content-Type headers are application/fhir+xml and application/fhir+json.
064 * <p>
065 * These can be changed by invoking the following setter functions:
066 *
067 * <pre><code>
068 * setPreferredResourceFormat()
069 * setPreferredFeedFormat()
070 * </code></pre>
071 * <p>
072 * TODO Review all sad paths.
073 *
074 * @author Claude Nanjo
075 */
076public class FHIRToolingClient {
077
078  public static final String DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ssK";
079  public static final String DATE_FORMAT = "yyyy-MM-dd";
080  public static final String hostKey = "http.proxyHost";
081  public static final String portKey = "http.proxyPort";
082
083  private static final int TIMEOUT_NORMAL = 1500;
084  private static final int TIMEOUT_OPERATION = 30000;
085  private static final int TIMEOUT_ENTRY = 500;
086  private static final int TIMEOUT_OPERATION_LONG = 60000;
087  private static final int TIMEOUT_OPERATION_EXPAND = 120000;
088
089  private String base;
090  private ResourceAddress resourceAddress;
091  private ResourceFormat preferredResourceFormat;
092  private int maxResultSetSize = -1;//_count
093  private CapabilityStatement capabilities;
094  private Client client = new Client();
095  private ArrayList<Header> headers = new ArrayList<>();
096  private String username;
097  private String password;
098  private String userAgent;
099
100  //Pass endpoint for client - URI
101  public FHIRToolingClient(String baseServiceUrl, String userAgent) throws URISyntaxException {
102    preferredResourceFormat = ResourceFormat.RESOURCE_XML;
103    this.userAgent = userAgent;
104    initialize(baseServiceUrl);
105  }
106
107  public void initialize(String baseServiceUrl) throws URISyntaxException {
108    base = baseServiceUrl;
109    resourceAddress = new ResourceAddress(baseServiceUrl);
110    this.maxResultSetSize = -1;
111    checkCapabilities();
112  }
113
114  public Client getClient() {
115    return client;
116  }
117
118  public void setClient(Client client) {
119    this.client = client;
120  }
121
122  private void checkCapabilities() {
123    try {
124      capabilities = getCapabilitiesStatementQuick();
125    } catch (Throwable e) {
126    }
127  }
128
129  public String getPreferredResourceFormat() {
130    return preferredResourceFormat.getHeader();
131  }
132
133  public void setPreferredResourceFormat(ResourceFormat resourceFormat) {
134    preferredResourceFormat = resourceFormat;
135  }
136
137  public int getMaximumRecordCount() {
138    return maxResultSetSize;
139  }
140
141  public void setMaximumRecordCount(int maxResultSetSize) {
142    this.maxResultSetSize = maxResultSetSize;
143  }
144
145  public TerminologyCapabilities getTerminologyCapabilities() {
146    TerminologyCapabilities capabilities = null;
147    try {
148      capabilities = (TerminologyCapabilities) client.issueGetResourceRequest(resourceAddress.resolveMetadataTxCaps(),
149        getPreferredResourceFormat(),
150        generateHeaders(),
151        "TerminologyCapabilities",
152        TIMEOUT_NORMAL).getReference();
153    } catch (Exception e) {
154      throw new FHIRException("Error fetching the server's terminology capabilities", e);
155    }
156    return capabilities;
157  }
158
159  public CapabilityStatement getCapabilitiesStatement() {
160    CapabilityStatement conformance = null;
161    try {
162      conformance = (CapabilityStatement) client.issueGetResourceRequest(resourceAddress.resolveMetadataUri(false),
163        getPreferredResourceFormat(),
164        generateHeaders(),
165        "CapabilitiesStatement",
166        TIMEOUT_NORMAL).getReference();
167    } catch (Exception e) {
168      throw new FHIRException("Error fetching the server's conformance statement", e);
169    }
170    return conformance;
171  }
172
173  public CapabilityStatement getCapabilitiesStatementQuick() throws EFhirClientException {
174    if (capabilities != null) return capabilities;
175    try {
176      capabilities = (CapabilityStatement) client.issueGetResourceRequest(resourceAddress.resolveMetadataUri(true),
177        getPreferredResourceFormat(),
178        generateHeaders(),
179        "CapabilitiesStatement-Quick",
180        TIMEOUT_NORMAL).getReference();
181    } catch (Exception e) {
182      throw new FHIRException("Error fetching the server's capability statement: "+e.getMessage(), e);
183    }
184    return capabilities;
185  }
186
187  public <T extends Resource> T read(Class<T> resourceClass, String id) {//TODO Change this to AddressableResource
188    ResourceRequest<T> result = null;
189    try {
190      result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id),
191        getPreferredResourceFormat(),
192        generateHeaders(),
193        "Read " + resourceClass.getName() + "/" + id,
194        TIMEOUT_NORMAL);
195      if (result.isUnsuccessfulRequest()) {
196        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
197      }
198    } catch (Exception e) {
199      throw new FHIRException(e);
200    }
201    return result.getPayload();
202  }
203
204  public <T extends Resource> T vread(Class<T> resourceClass, String id, String version) {
205    ResourceRequest<T> result = null;
206    try {
207      result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndIdAndVersion(resourceClass, id, version),
208        getPreferredResourceFormat(),
209        generateHeaders(),
210        "VRead " + resourceClass.getName() + "/" + id + "/?_history/" + version,
211        TIMEOUT_NORMAL);
212      if (result.isUnsuccessfulRequest()) {
213        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
214      }
215    } catch (Exception e) {
216      throw new FHIRException("Error trying to read this version of the resource", e);
217    }
218    return result.getPayload();
219  }
220
221  public <T extends Resource> T getCanonical(Class<T> resourceClass, String canonicalURL) {
222    ResourceRequest<T> result = null;
223    try {
224      result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndCanonical(resourceClass, canonicalURL),
225        getPreferredResourceFormat(),
226        generateHeaders(),
227        "Read " + resourceClass.getName() + "?url=" + canonicalURL,
228        TIMEOUT_NORMAL);
229      if (result.isUnsuccessfulRequest()) {
230        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
231      }
232    } catch (Exception e) {
233      handleException("An error has occurred while trying to read this version of the resource", e);
234    }
235    Bundle bnd = (Bundle) result.getPayload();
236    if (bnd.getEntry().size() == 0)
237      throw new EFhirClientException("No matching resource found for canonical URL '" + canonicalURL + "'");
238    if (bnd.getEntry().size() > 1)
239      throw new EFhirClientException("Multiple matching resources found for canonical URL '" + canonicalURL + "'");
240    return (T) bnd.getEntry().get(0).getResource();
241  }
242
243  public Resource update(Resource resource) {
244    org.hl7.fhir.r4b.utils.client.network.ResourceRequest<Resource> result = null;
245    try {
246      result = client.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resource.getClass(), resource.getId()),
247        ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat())),
248        getPreferredResourceFormat(),
249        generateHeaders(),
250        "Update " + resource.fhirType() + "/" + resource.getId(),
251        TIMEOUT_OPERATION);
252      if (result.isUnsuccessfulRequest()) {
253        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
254      }
255    } catch (Exception e) {
256      throw new EFhirClientException("An error has occurred while trying to update this resource", e);
257    }
258    // TODO oe 26.1.2015 could be made nicer if only OperationOutcome locationheader is returned with an operationOutcome would be returned (and not  the resource also) we make another read
259    try {
260      OperationOutcome operationOutcome = (OperationOutcome) result.getPayload();
261      ResourceAddress.ResourceVersionedIdentifier resVersionedIdentifier = ResourceAddress.parseCreateLocation(result.getLocation());
262      return this.vread(resource.getClass(), resVersionedIdentifier.getId(), resVersionedIdentifier.getVersionId());
263    } catch (ClassCastException e) {
264      // if we fall throught we have the correct type already in the create
265    }
266
267    return result.getPayload();
268  }
269
270  public <T extends Resource> T update(Class<T> resourceClass, T resource, String id) {
271    ResourceRequest<T> result = null;
272    try {
273      result = client.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id),
274        ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat())),
275        getPreferredResourceFormat(),
276        generateHeaders(),
277        "Update " + resource.fhirType() + "/" + id,
278        TIMEOUT_OPERATION);
279      if (result.isUnsuccessfulRequest()) {
280        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
281      }
282    } catch (Exception e) {
283      throw new EFhirClientException("An error has occurred while trying to update this resource", e);
284    }
285    // TODO oe 26.1.2015 could be made nicer if only OperationOutcome   locationheader is returned with an operationOutcome would be returned (and not  the resource also) we make another read
286    try {
287      OperationOutcome operationOutcome = (OperationOutcome) result.getPayload();
288      ResourceAddress.ResourceVersionedIdentifier resVersionedIdentifier = ResourceAddress.parseCreateLocation(result.getLocation());
289      return this.vread(resourceClass, resVersionedIdentifier.getId(), resVersionedIdentifier.getVersionId());
290    } catch (ClassCastException e) {
291      // if we fall through we have the correct type already in the create
292    }
293
294    return result.getPayload();
295  }
296
297  public <T extends Resource> Parameters operateType(Class<T> resourceClass, String name, Parameters params) {
298    boolean complex = false;
299    for (ParametersParameterComponent p : params.getParameter())
300      complex = complex || !(p.getValue() instanceof PrimitiveType);
301    String ps = "";
302    try {
303      if (!complex)
304        for (ParametersParameterComponent p : params.getParameter())
305          if (p.getValue() instanceof PrimitiveType)
306            ps += p.getName() + "=" + Utilities.encodeUri(((PrimitiveType) p.getValue()).asStringValue()) + "&";
307      ResourceRequest<T> result;
308      URI url = resourceAddress.resolveOperationURLFromClass(resourceClass, name, ps);
309      if (complex) {
310        byte[] body = ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat()));
311        result = client.issuePostRequest(url, body, getPreferredResourceFormat(), generateHeaders(),
312            "POST " + resourceClass.getName() + "/$" + name, TIMEOUT_OPERATION_LONG);
313      } else {
314        result = client.issueGetResourceRequest(url, getPreferredResourceFormat(), generateHeaders(), "GET " + resourceClass.getName() + "/$" + name, TIMEOUT_OPERATION_LONG);
315      }
316      if (result.isUnsuccessfulRequest()) {
317        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
318      }
319      if (result.getPayload() instanceof Parameters) {
320        return (Parameters) result.getPayload();
321      } else {
322        Parameters p_out = new Parameters();
323        p_out.addParameter().setName("return").setResource(result.getPayload());
324        return p_out;
325      }
326    } catch (Exception e) {
327      handleException("Error performing tx5 operation '"+name+": "+e.getMessage()+"' (parameters = \"" + ps+"\")", e);                  
328    }
329    return null;
330  }
331
332  public Bundle transaction(Bundle batch) {
333    Bundle transactionResult = null;
334    try {
335      transactionResult = client.postBatchRequest(resourceAddress.getBaseServiceUri(), ByteUtils.resourceToByteArray(batch, false, isJson(getPreferredResourceFormat())), getPreferredResourceFormat(),
336          generateHeaders(),
337          "transaction", TIMEOUT_OPERATION + (TIMEOUT_ENTRY * batch.getEntry().size()));
338    } catch (Exception e) {
339      handleException("An error occurred trying to process this transaction request", e);
340    }
341    return transactionResult;
342  }
343
344  @SuppressWarnings("unchecked")
345  public <T extends Resource> OperationOutcome validate(Class<T> resourceClass, T resource, String id) {
346    ResourceRequest<T> result = null;
347    try {
348      result = client.issuePostRequest(resourceAddress.resolveValidateUri(resourceClass, id),
349        ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat())),
350        getPreferredResourceFormat(), generateHeaders(),
351        "POST " + resourceClass.getName() + (id != null ? "/" + id : "") + "/$validate", TIMEOUT_OPERATION_LONG);
352      if (result.isUnsuccessfulRequest()) {
353        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
354      }
355    } catch (Exception e) {
356      handleException("An error has occurred while trying to validate this resource", e);
357    }
358    return (OperationOutcome) result.getPayload();
359  }
360
361  /**
362   * Helper method to prevent nesting of previously thrown EFhirClientExceptions
363   *
364   * @param e
365   * @throws EFhirClientException
366   */
367  protected void handleException(String message, Exception e) throws EFhirClientException {
368    if (e instanceof EFhirClientException) {
369      throw (EFhirClientException) e;
370    } else {
371      throw new EFhirClientException(message, e);
372    }
373  }
374
375  /**
376   * Helper method to determine whether desired resource representation
377   * is Json or XML.
378   *
379   * @param format
380   * @return
381   */
382  protected boolean isJson(String format) {
383    boolean isJson = false;
384    if (format.toLowerCase().contains("json")) {
385      isJson = true;
386    }
387    return isJson;
388  }
389
390  public Bundle fetchFeed(String url) {
391    Bundle feed = null;
392    try {
393      feed = client.issueGetFeedRequest(new URI(url), getPreferredResourceFormat());
394    } catch (Exception e) {
395      handleException("An error has occurred while trying to retrieve history since last update", e);
396    }
397    return feed;
398  }
399
400  public ValueSet expandValueset(ValueSet source, Parameters expParams) {
401    Parameters p = expParams == null ? new Parameters() : expParams.copy();
402    p.addParameter().setName("valueSet").setResource(source);
403    org.hl7.fhir.r4b.utils.client.network.ResourceRequest<Resource> result = null;
404    try {
405      result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand"),
406        ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat())),
407        getPreferredResourceFormat(),
408        generateHeaders(),
409        "ValueSet/$expand?url=" + source.getUrl(),
410        TIMEOUT_OPERATION_EXPAND);
411      if (result.isUnsuccessfulRequest()) {
412        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
413      }
414    } catch (IOException e) {
415      e.printStackTrace();
416    }
417    return result == null ? null : (ValueSet) result.getPayload();
418  }
419
420
421  public Parameters lookupCode(Map<String, String> params) {
422    org.hl7.fhir.r4b.utils.client.network.ResourceRequest<Resource> result = null;
423    try {
424      result = client.issueGetResourceRequest(resourceAddress.resolveOperationUri(CodeSystem.class, "lookup", params),
425        getPreferredResourceFormat(),
426        generateHeaders(),
427        "CodeSystem/$lookup",
428        TIMEOUT_NORMAL);
429    } catch (IOException e) {
430      e.printStackTrace();
431    }
432    if (result.isUnsuccessfulRequest()) {
433      throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
434    }
435    return (Parameters) result.getPayload();
436  }
437
438  public ValueSet expandValueset(ValueSet source, Parameters expParams, Map<String, String> params) {
439    Parameters p = expParams == null ? new Parameters() : expParams.copy();
440    p.addParameter().setName("valueSet").setResource(source);
441    for (String n : params.keySet()) {
442      p.addParameter().setName(n).setValue(new StringType(params.get(n)));
443    }
444    org.hl7.fhir.r4b.utils.client.network.ResourceRequest<Resource> result = null;
445    try {
446
447      result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand", params),
448        ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat())),
449        getPreferredResourceFormat(),
450        generateHeaders(),
451        "ValueSet/$expand?url=" + source.getUrl(),
452        TIMEOUT_OPERATION_EXPAND);
453      if (result.isUnsuccessfulRequest()) {
454        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
455      }
456    } catch (IOException e) {
457      e.printStackTrace();
458    }
459    return result == null ? null : (ValueSet) result.getPayload();
460  }
461
462  public String getAddress() {
463    return base;
464  }
465
466  public ConceptMap initializeClosure(String name) {
467    Parameters params = new Parameters();
468    params.addParameter().setName("name").setValue(new StringType(name));
469    ResourceRequest<Resource> result = null;
470    try {
471      result = client.issuePostRequest(resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()),
472        ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat())),
473        getPreferredResourceFormat(),
474        generateHeaders(),
475        "Closure?name=" + name,
476        TIMEOUT_NORMAL);
477      if (result.isUnsuccessfulRequest()) {
478        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
479      }
480    } catch (IOException e) {
481      e.printStackTrace();
482    }
483    return result == null ? null : (ConceptMap) result.getPayload();
484  }
485
486  public ConceptMap updateClosure(String name, Coding coding) {
487    Parameters params = new Parameters();
488    params.addParameter().setName("name").setValue(new StringType(name));
489    params.addParameter().setName("concept").setValue(coding);
490    org.hl7.fhir.r4b.utils.client.network.ResourceRequest<Resource> result = null;
491    try {
492      result = client.issuePostRequest(resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()),
493        ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat())),
494        getPreferredResourceFormat(),
495        generateHeaders(),
496        "UpdateClosure?name=" + name,
497        TIMEOUT_OPERATION);
498      if (result.isUnsuccessfulRequest()) {
499        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
500      }
501    } catch (IOException e) {
502      e.printStackTrace();
503    }
504    return result == null ? null : (ConceptMap) result.getPayload();
505  }
506
507  public String getUsername() {
508    return username;
509  }
510
511  public void setUsername(String username) {
512    this.username = username;
513  }
514
515  public String getPassword() {
516    return password;
517  }
518
519  public void setPassword(String password) {
520    this.password = password;
521  }
522
523  public long getTimeout() {
524    return client.getTimeout();
525  }
526
527  public void setTimeout(long timeout) {
528    client.setTimeout(timeout);
529  }
530
531  public ToolingClientLogger getLogger() {
532    return client.getLogger();
533  }
534
535  public void setLogger(ToolingClientLogger logger) {
536    client.setLogger(logger);
537  }
538
539  public int getRetryCount() {
540    return client.getRetryCount();
541  }
542
543  public void setRetryCount(int retryCount) {
544    client.setRetryCount(retryCount);
545  }
546
547  public void setClientHeaders(ArrayList<Header> headers) {
548    this.headers = headers;
549  }
550
551  private Headers generateHeaders() {
552    Headers.Builder builder = new Headers.Builder();
553    // Add basic auth header if it exists
554    if (basicAuthHeaderExists()) {
555      builder.add(getAuthorizationHeader().toString());
556    }
557    // Add any other headers
558    if(this.headers != null) {
559      this.headers.forEach(header -> builder.add(header.toString()));
560    }
561    if (!Utilities.noString(userAgent)) {
562      builder.add("User-Agent: "+userAgent);
563    }
564    return builder.build();
565  }
566
567  public boolean basicAuthHeaderExists() {
568    return (username != null) && (password != null);
569  }
570
571  public Header getAuthorizationHeader() {
572    String usernamePassword = username + ":" + password;
573    String base64usernamePassword = Base64.getEncoder().encodeToString(usernamePassword.getBytes());
574    return new Header("Authorization", "Basic " + base64usernamePassword);
575  }
576
577  public String getUserAgent() {
578    return userAgent;
579  }
580
581  public void setUserAgent(String userAgent) {
582    this.userAgent = userAgent;
583  }
584
585  public String getServerVersion() {
586    return capabilities == null ? null : capabilities.getSoftware().getVersion();
587  }
588  
589  
590}
591