001package org.hl7.fhir.r4.context; 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 032 033 034import java.io.ByteArrayInputStream; 035import java.io.File; 036import java.io.FileInputStream; 037import java.io.FileNotFoundException; 038import java.io.IOException; 039import java.io.InputStream; 040import java.net.URISyntaxException; 041import java.util.ArrayList; 042import java.util.Arrays; 043import java.util.Collections; 044import java.util.HashMap; 045import java.util.HashSet; 046import java.util.List; 047import java.util.Map; 048import java.util.Set; 049import java.util.zip.ZipEntry; 050import java.util.zip.ZipInputStream; 051 052import org.apache.commons.io.IOUtils; 053import org.hl7.fhir.exceptions.DefinitionException; 054import org.hl7.fhir.exceptions.FHIRException; 055import org.hl7.fhir.exceptions.FHIRFormatError; 056import org.hl7.fhir.r4.conformance.ProfileUtilities; 057import org.hl7.fhir.r4.conformance.ProfileUtilities.ProfileKnowledgeProvider; 058import org.hl7.fhir.r4.context.IWorkerContext.ILoggingService.LogCategory; 059import org.hl7.fhir.r4.formats.IParser; 060import org.hl7.fhir.r4.formats.JsonParser; 061import org.hl7.fhir.r4.formats.ParserType; 062import org.hl7.fhir.r4.formats.XmlParser; 063import org.hl7.fhir.r4.model.Bundle; 064import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; 065import org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionBindingComponent; 066import org.hl7.fhir.r4.model.MetadataResource; 067import org.hl7.fhir.r4.model.Questionnaire; 068import org.hl7.fhir.r4.model.Resource; 069import org.hl7.fhir.r4.model.ResourceType; 070import org.hl7.fhir.r4.model.StructureDefinition; 071import org.hl7.fhir.r4.model.StructureDefinition.StructureDefinitionKind; 072import org.hl7.fhir.r4.model.StructureDefinition.TypeDerivationRule; 073import org.hl7.fhir.r4.model.StructureMap; 074import org.hl7.fhir.r4.model.StructureMap.StructureMapModelMode; 075import org.hl7.fhir.r4.model.StructureMap.StructureMapStructureComponent; 076import org.hl7.fhir.r4.terminologies.TerminologyClient; 077import org.hl7.fhir.r4.utils.INarrativeGenerator; 078import org.hl7.fhir.r4.utils.IResourceValidator; 079import org.hl7.fhir.r4.utils.NarrativeGenerator; 080import org.hl7.fhir.utilities.CSFileInputStream; 081import org.hl7.fhir.utilities.Utilities; 082import org.hl7.fhir.utilities.cache.NpmPackage; 083import org.hl7.fhir.utilities.validation.ValidationMessage; 084import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; 085import org.hl7.fhir.utilities.validation.ValidationMessage.Source; 086 087import ca.uhn.fhir.parser.DataFormatException; 088 089/* 090 * This is a stand alone implementation of worker context for use inside a tool. 091 * It loads from the validation package (validation-min.xml.zip), and has a 092 * very light client to connect to an open unauthenticated terminology service 093 */ 094 095public class SimpleWorkerContext extends BaseWorkerContext implements IWorkerContext, ProfileKnowledgeProvider { 096 097 public interface IContextResourceLoader { 098 Bundle loadBundle(InputStream stream, boolean isJson) throws FHIRException, IOException; 099 } 100 101 public interface IValidatorFactory { 102 IResourceValidator makeValidator(IWorkerContext ctxts) throws FHIRException; 103 } 104 105 private Questionnaire questionnaire; 106 private Map<String, byte[]> binaries = new HashMap<String, byte[]>(); 107 private String version; 108 private String revision; 109 private String date; 110 private IValidatorFactory validatorFactory; 111 private boolean ignoreProfileErrors; 112 113 public SimpleWorkerContext() throws FileNotFoundException, IOException, FHIRException { 114 super(); 115 } 116 117 public SimpleWorkerContext(SimpleWorkerContext other) throws FileNotFoundException, IOException, FHIRException { 118 super(); 119 copy(other); 120 } 121 122 protected void copy(SimpleWorkerContext other) { 123 super.copy(other); 124 questionnaire = other.questionnaire; 125 binaries.putAll(other.binaries); 126 version = other.version; 127 revision = other.revision; 128 date = other.date; 129 validatorFactory = other.validatorFactory; 130 } 131 132 // -- Initializations 133 /** 134 * Load the working context from the validation pack 135 * 136 * @param path 137 * filename of the validation pack 138 * @return 139 * @throws IOException 140 * @throws FileNotFoundException 141 * @throws FHIRException 142 * @throws Exception 143 */ 144 public static SimpleWorkerContext fromPack(String path) throws FileNotFoundException, IOException, FHIRException { 145 SimpleWorkerContext res = new SimpleWorkerContext(); 146 res.loadFromPack(path, null); 147 return res; 148 } 149 150 public static SimpleWorkerContext fromPackage(NpmPackage pi, boolean allowDuplicates) throws FileNotFoundException, IOException, FHIRException { 151 SimpleWorkerContext res = new SimpleWorkerContext(); 152 res.setAllowLoadingDuplicates(allowDuplicates); 153 res.loadFromPackage(pi, null); 154 return res; 155 } 156 157 public static SimpleWorkerContext fromPackage(NpmPackage pi) throws FileNotFoundException, IOException, FHIRException { 158 SimpleWorkerContext res = new SimpleWorkerContext(); 159 res.loadFromPackage(pi, null); 160 return res; 161 } 162 163 public static SimpleWorkerContext fromPackage(NpmPackage pi, IContextResourceLoader loader) throws FileNotFoundException, IOException, FHIRException { 164 SimpleWorkerContext res = new SimpleWorkerContext(); 165 res.setAllowLoadingDuplicates(true); 166 res.version = pi.getNpm().get("version").getAsString(); 167 res.loadFromPackage(pi, loader); 168 return res; 169 } 170 171 public static SimpleWorkerContext fromPack(String path, boolean allowDuplicates) throws FileNotFoundException, IOException, FHIRException { 172 SimpleWorkerContext res = new SimpleWorkerContext(); 173 res.setAllowLoadingDuplicates(allowDuplicates); 174 res.loadFromPack(path, null); 175 return res; 176 } 177 178 public static SimpleWorkerContext fromPack(String path, IContextResourceLoader loader) throws FileNotFoundException, IOException, FHIRException { 179 SimpleWorkerContext res = new SimpleWorkerContext(); 180 res.loadFromPack(path, loader); 181 return res; 182 } 183 184 public static SimpleWorkerContext fromClassPath() throws IOException, FHIRException { 185 SimpleWorkerContext res = new SimpleWorkerContext(); 186 res.loadFromStream(SimpleWorkerContext.class.getResourceAsStream("validation.json.zip"), null); 187 return res; 188 } 189 190 public static SimpleWorkerContext fromClassPath(String name) throws IOException, FHIRException { 191 InputStream s = SimpleWorkerContext.class.getResourceAsStream("/"+name); 192 SimpleWorkerContext res = new SimpleWorkerContext(); 193 res.loadFromStream(s, null); 194 return res; 195 } 196 197 public static SimpleWorkerContext fromDefinitions(Map<String, byte[]> source) throws IOException, FHIRException { 198 SimpleWorkerContext res = new SimpleWorkerContext(); 199 for (String name : source.keySet()) { 200 res.loadDefinitionItem(name, new ByteArrayInputStream(source.get(name)), null); 201 } 202 return res; 203 } 204 205 public static SimpleWorkerContext fromDefinitions(Map<String, byte[]> source, IContextResourceLoader loader) throws FileNotFoundException, IOException, FHIRException { 206 SimpleWorkerContext res = new SimpleWorkerContext(); 207 for (String name : source.keySet()) { 208 try { 209 res.loadDefinitionItem(name, new ByteArrayInputStream(source.get(name)), loader); 210 } catch (Exception e) { 211 System.out.println("Error loading "+name+": "+e.getMessage()); 212 throw new FHIRException("Error loading "+name+": "+e.getMessage(), e); 213 } 214 } 215 return res; 216 } 217 private void loadDefinitionItem(String name, InputStream stream, IContextResourceLoader loader) throws IOException, FHIRException { 218 if (name.endsWith(".xml")) 219 loadFromFile(stream, name, loader); 220 else if (name.endsWith(".json")) 221 loadFromFileJson(stream, name, loader); 222 else if (name.equals("version.info")) 223 readVersionInfo(stream); 224 else 225 loadBytes(name, stream); 226 } 227 228 229 public String connectToTSServer(TerminologyClient client, String log) throws URISyntaxException, FHIRException { 230 tlog("Connect to "+client.getAddress()); 231 txClient = client; 232 txLog = new HTMLClientLogger(log); 233 txClient.setLogger(txLog); 234 return txClient.getCapabilitiesStatementQuick().getSoftware().getVersion(); 235 } 236 237 public void loadFromFile(InputStream stream, String name, IContextResourceLoader loader) throws IOException, FHIRException { 238 Resource f; 239 try { 240 if (loader != null) 241 f = loader.loadBundle(stream, false); 242 else { 243 XmlParser xml = new XmlParser(); 244 f = xml.parse(stream); 245 } 246 } catch (DataFormatException e1) { 247 throw new org.hl7.fhir.exceptions.FHIRFormatError("Error parsing "+name+":" +e1.getMessage(), e1); 248 } catch (Exception e1) { 249 throw new org.hl7.fhir.exceptions.FHIRFormatError("Error parsing "+name+":" +e1.getMessage(), e1); 250 } 251 if (f instanceof Bundle) { 252 Bundle bnd = (Bundle) f; 253 for (BundleEntryComponent e : bnd.getEntry()) { 254 if (e.getFullUrl() == null) { 255 logger.logDebugMessage(LogCategory.CONTEXT, "unidentified resource in " + name+" (no fullUrl)"); 256 } 257 cacheResource(e.getResource()); 258 } 259 } else if (f instanceof MetadataResource) { 260 MetadataResource m = (MetadataResource) f; 261 cacheResource(m); 262 } 263 } 264 265 private void loadFromFileJson(InputStream stream, String name, IContextResourceLoader loader) throws IOException, FHIRException { 266 Bundle f = null; 267 try { 268 if (loader != null) 269 f = loader.loadBundle(stream, true); 270 else { 271 JsonParser json = new JsonParser(); 272 Resource r = json.parse(stream); 273 if (r instanceof Bundle) 274 f = (Bundle) r; 275 else 276 cacheResource(r); 277 } 278 } catch (FHIRFormatError e1) { 279 throw new org.hl7.fhir.exceptions.FHIRFormatError(e1.getMessage(), e1); 280 } 281 if (f != null) 282 for (BundleEntryComponent e : f.getEntry()) { 283 cacheResource(e.getResource()); 284 } 285 } 286 287 private void loadFromPack(String path, IContextResourceLoader loader) throws FileNotFoundException, IOException, FHIRException { 288 loadFromStream(new CSFileInputStream(path), loader); 289 } 290 291 public void loadFromPackage(NpmPackage pi, IContextResourceLoader loader, String... types) throws FileNotFoundException, IOException, FHIRException { 292 if (types.length == 0) 293 types = new String[] { "StructureDefinition", "ValueSet", "CodeSystem", "SearchParameter", "OperationDefinition", "Questionnaire","ConceptMap","StructureMap", "NamingSystem"}; 294 for (String s : pi.listResources(types)) { 295 loadDefinitionItem(s, pi.load("package", s), loader); 296 } 297 version = pi.version(); 298 } 299 300 public void loadFromFile(String file, IContextResourceLoader loader) throws IOException, FHIRException { 301 loadDefinitionItem(file, new CSFileInputStream(file), loader); 302 } 303 304 private void loadFromStream(InputStream stream, IContextResourceLoader loader) throws IOException, FHIRException { 305 ZipInputStream zip = new ZipInputStream(stream); 306 ZipEntry ze; 307 while ((ze = zip.getNextEntry()) != null) { 308 loadDefinitionItem(ze.getName(), zip, loader); 309 zip.closeEntry(); 310 } 311 zip.close(); 312 } 313 314 private void readVersionInfo(InputStream stream) throws IOException, DefinitionException { 315 byte[] bytes = IOUtils.toByteArray(stream); 316 binaries.put("version.info", bytes); 317 318 String[] vi = new String(bytes).split("\\r?\\n"); 319 for (String s : vi) { 320 if (s.startsWith("version=")) { 321 if (version == null) 322 version = s.substring(8); 323 else if (!version.equals(s.substring(8))) 324 throw new DefinitionException("Version mismatch. The context has version "+version+" loaded, and the new content being loaded is version "+s.substring(8)); 325 } 326 if (s.startsWith("revision=")) 327 revision = s.substring(9); 328 if (s.startsWith("date=")) 329 date = s.substring(5); 330 } 331 } 332 333 private void loadBytes(String name, InputStream stream) throws IOException { 334 byte[] bytes = IOUtils.toByteArray(stream); 335 binaries.put(name, bytes); 336 } 337 338 @Override 339 public IParser getParser(ParserType type) { 340 switch (type) { 341 case JSON: return newJsonParser(); 342 case XML: return newXmlParser(); 343 default: 344 throw new Error("Parser Type "+type.toString()+" not supported"); 345 } 346 } 347 348 @Override 349 public IParser getParser(String type) { 350 if (type.equalsIgnoreCase("JSON")) 351 return new JsonParser(); 352 if (type.equalsIgnoreCase("XML")) 353 return new XmlParser(); 354 throw new Error("Parser Type "+type.toString()+" not supported"); 355 } 356 357 @Override 358 public IParser newJsonParser() { 359 return new JsonParser(); 360 } 361 @Override 362 public IParser newXmlParser() { 363 return new XmlParser(); 364 } 365 366 @Override 367 public INarrativeGenerator getNarrativeGenerator(String prefix, String basePath) { 368 return new NarrativeGenerator(prefix, basePath, this); 369 } 370 371 @Override 372 public IResourceValidator newValidator() throws FHIRException { 373 if (validatorFactory == null) 374 throw new Error("No validator configured"); 375 return validatorFactory.makeValidator(this); 376 } 377 378 379 380 381 @Override 382 public List<String> getResourceNames() { 383 List<String> result = new ArrayList<String>(); 384 for (StructureDefinition sd : listStructures()) { 385 if (sd.getKind() == StructureDefinitionKind.RESOURCE && sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) 386 result.add(sd.getName()); 387 } 388 Collections.sort(result); 389 return result; 390 } 391 392 @Override 393 public List<String> getTypeNames() { 394 List<String> result = new ArrayList<String>(); 395 for (StructureDefinition sd : listStructures()) { 396 if (sd.getKind() != StructureDefinitionKind.LOGICAL && sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) 397 result.add(sd.getName()); 398 } 399 Collections.sort(result); 400 return result; 401 } 402 403 @Override 404 public String getAbbreviation(String name) { 405 return "xxx"; 406 } 407 408 @Override 409 public boolean isDatatype(String typeSimple) { 410 // TODO Auto-generated method stub 411 return false; 412 } 413 414 @Override 415 public boolean isResource(String t) { 416 StructureDefinition sd; 417 try { 418 sd = fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+t); 419 } catch (Exception e) { 420 return false; 421 } 422 if (sd == null) 423 return false; 424 if (sd.getDerivation() == TypeDerivationRule.CONSTRAINT) 425 return false; 426 return sd.getKind() == StructureDefinitionKind.RESOURCE; 427 } 428 429 @Override 430 public boolean hasLinkFor(String typeSimple) { 431 return false; 432 } 433 434 @Override 435 public String getLinkFor(String corePath, String typeSimple) { 436 return null; 437 } 438 439 @Override 440 public BindingResolution resolveBinding(StructureDefinition profile, ElementDefinitionBindingComponent binding, String path) { 441 return null; 442 } 443 444 @Override 445 public BindingResolution resolveBinding(StructureDefinition profile, String url, String path) { 446 return null; 447 } 448 449 @Override 450 public String getLinkForProfile(StructureDefinition profile, String url) { 451 return null; 452 } 453 454 public Questionnaire getQuestionnaire() { 455 return questionnaire; 456 } 457 458 public void setQuestionnaire(Questionnaire questionnaire) { 459 this.questionnaire = questionnaire; 460 } 461 462 @Override 463 public Set<String> typeTails() { 464 return new HashSet<String>(Arrays.asList("Integer","UnsignedInt","PositiveInt","Decimal","DateTime","Date","Time","Instant","String","Uri","Url","Canonical","Oid","Uuid","Id","Boolean","Code","Markdown","Base64Binary","Coding","CodeableConcept","Attachment","Identifier","Quantity","SampledData","Range","Period","Ratio","HumanName","Address","ContactPoint","Timing","Reference","Annotation","Signature","Meta")); 465 } 466 467 @Override 468 public List<StructureDefinition> allStructures() { 469 List<StructureDefinition> result = new ArrayList<StructureDefinition>(); 470 Set<StructureDefinition> set = new HashSet<StructureDefinition>(); 471 for (StructureDefinition sd : listStructures()) { 472 if (!set.contains(sd)) { 473 try { 474 generateSnapshot(sd); 475 } catch (Exception e) { 476 System.out.println("Unable to generate snapshot for "+sd.getUrl()+" because "+e.getMessage()); 477 } 478 result.add(sd); 479 set.add(sd); 480 } 481 } 482 return result; 483 } 484 485 public void loadBinariesFromFolder(String folder) throws FileNotFoundException, Exception { 486 for (String n : new File(folder).list()) { 487 loadBytes(n, new FileInputStream(Utilities.path(folder, n))); 488 } 489 } 490 491 public void loadBinariesFromFolder(NpmPackage pi) throws FileNotFoundException, Exception { 492 for (String n : pi.list("other")) { 493 loadBytes(n, pi.load("other", n)); 494 } 495 } 496 497 public void loadFromFolder(String folder) throws FileNotFoundException, Exception { 498 for (String n : new File(folder).list()) { 499 if (n.endsWith(".json")) 500 loadFromFile(Utilities.path(folder, n), new JsonParser()); 501 else if (n.endsWith(".xml")) 502 loadFromFile(Utilities.path(folder, n), new XmlParser()); 503 } 504 } 505 506 private void loadFromFile(String filename, IParser p) throws FileNotFoundException, Exception { 507 Resource r; 508 try { 509 r = p.parse(new FileInputStream(filename)); 510 if (r.getResourceType() == ResourceType.Bundle) { 511 for (BundleEntryComponent e : ((Bundle) r).getEntry()) { 512 cacheResource(e.getResource()); 513 } 514 } else { 515 cacheResource(r); 516 } 517 } catch (Exception e) { 518 return; 519 } 520 } 521 522 public Map<String, byte[]> getBinaries() { 523 return binaries; 524 } 525 526 @Override 527 public boolean prependLinks() { 528 return false; 529 } 530 531 @Override 532 public boolean hasCache() { 533 return false; 534 } 535 536 @Override 537 public String getVersion() { 538 return version; 539 } 540 541 542 public List<StructureMap> findTransformsforSource(String url) { 543 List<StructureMap> res = new ArrayList<StructureMap>(); 544 for (StructureMap map : listTransforms()) { 545 boolean match = false; 546 boolean ok = true; 547 for (StructureMapStructureComponent t : map.getStructure()) { 548 if (t.getMode() == StructureMapModelMode.SOURCE) { 549 match = match || t.getUrl().equals(url); 550 ok = ok && t.getUrl().equals(url); 551 } 552 } 553 if (match && ok) 554 res.add(map); 555 } 556 return res; 557 } 558 559 public IValidatorFactory getValidatorFactory() { 560 return validatorFactory; 561 } 562 563 public void setValidatorFactory(IValidatorFactory validatorFactory) { 564 this.validatorFactory = validatorFactory; 565 } 566 567 @Override 568 public <T extends Resource> T fetchResource(Class<T> class_, String uri) { 569 T r = super.fetchResource(class_, uri); 570 if (r instanceof StructureDefinition) { 571 StructureDefinition p = (StructureDefinition)r; 572 try { 573 generateSnapshot(p); 574 } catch (Exception e) { 575 // not sure what to do in this case? 576 System.out.println("Unable to generate snapshot for "+uri+": "+e.getMessage()); 577 } 578 } 579 return r; 580 } 581 582 public void generateSnapshot(StructureDefinition p) throws DefinitionException, FHIRException { 583 if (!p.hasSnapshot() && p.getKind() != StructureDefinitionKind.LOGICAL) { 584 if (!p.hasBaseDefinition()) 585 throw new DefinitionException("Profile "+p.getName()+" ("+p.getUrl()+") has no base and no snapshot"); 586 StructureDefinition sd = fetchResource(StructureDefinition.class, p.getBaseDefinition()); 587 if (sd == null) 588 throw new DefinitionException("Profile "+p.getName()+" ("+p.getUrl()+") base "+p.getBaseDefinition()+" could not be resolved"); 589 List<ValidationMessage> msgs = new ArrayList<ValidationMessage>(); 590 List<String> errors = new ArrayList<String>(); 591 ProfileUtilities pu = new ProfileUtilities(this, msgs, this); 592 pu.setThrowException(false); 593 pu.sortDifferential(sd, p, p.getUrl(), errors); 594 for (String err : errors) 595 msgs.add(new ValidationMessage(Source.ProfileValidator, IssueType.EXCEPTION, p.getUserString("path"), "Error sorting Differential: "+err, ValidationMessage.IssueSeverity.ERROR)); 596 pu.generateSnapshot(sd, p, p.getUrl(), Utilities.extractBaseUrl(sd.getUserString("path")), p.getName()); 597 for (ValidationMessage msg : msgs) { 598 if ((!ignoreProfileErrors && msg.getLevel() == ValidationMessage.IssueSeverity.ERROR) || msg.getLevel() == ValidationMessage.IssueSeverity.FATAL) 599 throw new DefinitionException("Profile "+p.getName()+" ("+p.getUrl()+"). Error generating snapshot: "+msg.getMessage()); 600 } 601 if (!p.hasSnapshot()) 602 throw new FHIRException("Profile "+p.getName()+" ("+p.getUrl()+"). Error generating snapshot"); 603 pu = null; 604 } 605 } 606 607 public boolean isIgnoreProfileErrors() { 608 return ignoreProfileErrors; 609 } 610 611 public void setIgnoreProfileErrors(boolean ignoreProfileErrors) { 612 this.ignoreProfileErrors = ignoreProfileErrors; 613 } 614 615 public String listMapUrls() { 616 return Utilities.listCanonicalUrls(transforms.keySet()); 617 } 618 619 620 621 622}