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.validation.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.npm.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}