001package org.hl7.fhir.validation.cli.services;
002
003import java.io.FileOutputStream;
004import java.io.PrintStream;
005import java.lang.management.ManagementFactory;
006import java.lang.management.MemoryMXBean;
007import java.util.ArrayList;
008import java.util.List;
009
010import org.hl7.fhir.r5.conformance.R5ExtensionsLoader;
011import org.hl7.fhir.r5.context.ContextUtilities;
012import org.hl7.fhir.r5.context.SimpleWorkerContext;
013import org.hl7.fhir.r5.context.SystemOutLoggingService;
014import org.hl7.fhir.r5.context.TerminologyCache;
015import org.hl7.fhir.r5.elementmodel.Manager;
016import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat;
017import org.hl7.fhir.r5.formats.IParser;
018import org.hl7.fhir.r5.model.Bundle;
019import org.hl7.fhir.r5.model.CanonicalResource;
020import org.hl7.fhir.r5.model.CodeSystem;
021import org.hl7.fhir.r5.model.ConceptMap;
022import org.hl7.fhir.r5.model.OperationOutcome;
023import org.hl7.fhir.r5.model.Resource;
024import org.hl7.fhir.r5.model.StructureDefinition;
025import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind;
026import org.hl7.fhir.r5.model.StructureMap;
027import org.hl7.fhir.r5.model.ValueSet;
028import org.hl7.fhir.r5.renderers.spreadsheets.CodeSystemSpreadsheetGenerator;
029import org.hl7.fhir.r5.renderers.spreadsheets.ConceptMapSpreadsheetGenerator;
030import org.hl7.fhir.r5.renderers.spreadsheets.StructureDefinitionSpreadsheetGenerator;
031import org.hl7.fhir.r5.renderers.spreadsheets.ValueSetSpreadsheetGenerator;
032import org.hl7.fhir.r5.terminologies.CodeSystemUtilities;
033import org.hl7.fhir.utilities.FhirPublication;
034import org.hl7.fhir.utilities.TextFile;
035import org.hl7.fhir.utilities.TimeTracker;
036import org.hl7.fhir.utilities.Utilities;
037import org.hl7.fhir.utilities.VersionUtilities;
038import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
039import org.hl7.fhir.utilities.npm.ToolsVersion;
040import org.hl7.fhir.utilities.validation.ValidationMessage;
041import org.hl7.fhir.validation.IgLoader;
042import org.hl7.fhir.validation.ValidationEngine;
043import org.hl7.fhir.validation.ValidationRecord;
044import org.hl7.fhir.validation.cli.model.CliContext;
045import org.hl7.fhir.validation.cli.model.FileInfo;
046import org.hl7.fhir.validation.cli.model.ValidationOutcome;
047import org.hl7.fhir.validation.cli.model.ValidationRequest;
048import org.hl7.fhir.validation.cli.model.ValidationResponse;
049import org.hl7.fhir.validation.cli.renderers.CSVRenderer;
050import org.hl7.fhir.validation.cli.renderers.DefaultRenderer;
051import org.hl7.fhir.validation.cli.renderers.ESLintCompactRenderer;
052import org.hl7.fhir.validation.cli.renderers.NativeRenderer;
053import org.hl7.fhir.validation.cli.renderers.ValidationOutputRenderer;
054import org.hl7.fhir.validation.cli.utils.EngineMode;
055import org.hl7.fhir.validation.cli.utils.VersionSourceInformation;
056
057public class ValidationService {
058
059  private final SessionCache sessionCache;
060
061  public ValidationService() {
062    sessionCache = new SessionCache();
063  }
064
065  protected ValidationService(SessionCache cache) {
066    this.sessionCache = cache;
067  }
068
069  public ValidationResponse validateSources(ValidationRequest request) throws Exception {
070    if (request.getCliContext().getSv() == null) {
071      String sv = determineVersion(request.getCliContext(), request.sessionId);
072      request.getCliContext().setSv(sv);
073    }
074
075    String definitions = VersionUtilities.packageForVersion(request.getCliContext().getSv()) + "#" + VersionUtilities.getCurrentVersion(request.getCliContext().getSv());
076
077    String sessionId = initializeValidator(request.getCliContext(), definitions, new TimeTracker(), request.sessionId);
078    ValidationEngine validator = sessionCache.fetchSessionValidatorEngine(sessionId);
079
080    if (request.getCliContext().getProfiles().size() > 0) {
081      System.out.println("  .. validate " + request.listSourceFiles() + " against " + request.getCliContext().getProfiles().toString());
082    } else {
083      System.out.println("  .. validate " + request.listSourceFiles());
084    }
085
086    ValidationResponse response = new ValidationResponse().setSessionId(sessionId);
087
088    for (FileInfo fp : request.getFilesToValidate()) {
089      List<ValidationMessage> messages = new ArrayList<>();
090      validator.validate(fp.getFileContent().getBytes(), Manager.FhirFormat.getFhirFormat(fp.getFileType()),
091        request.getCliContext().getProfiles(), messages);
092      ValidationOutcome outcome = new ValidationOutcome().setFileInfo(fp);
093      messages.forEach(outcome::addMessage);
094      response.addOutcome(outcome);
095    }
096    System.out.println("  Max Memory: "+Runtime.getRuntime().maxMemory());
097    return response;
098  }
099
100  public VersionSourceInformation scanForVersions(CliContext cliContext) throws Exception {
101    VersionSourceInformation versions = new VersionSourceInformation();
102    IgLoader igLoader = new IgLoader(
103      new FilesystemPackageCacheManager(true, ToolsVersion.TOOLS_VERSION),
104      new SimpleWorkerContext.SimpleWorkerContextBuilder().fromNothing(),
105      null);
106    for (String src : cliContext.getIgs()) {
107      igLoader.scanForIgVersion(src, cliContext.isRecursive(), versions);
108    }
109    igLoader.scanForVersions(cliContext.getSources(), versions);
110    return versions;
111  }
112
113  public void validateSources(CliContext cliContext, ValidationEngine validator) throws Exception {
114    long start = System.currentTimeMillis();
115    List<ValidationRecord> records = new ArrayList<>();
116    Resource r = validator.validate(cliContext.getSources(), cliContext.getProfiles(), records);
117    MemoryMXBean mbean = ManagementFactory.getMemoryMXBean();
118    System.out.println("Done. " + validator.getContext().clock().report()+". Memory = "+Utilities.describeSize(mbean.getHeapMemoryUsage().getUsed()+mbean.getNonHeapMemoryUsage().getUsed()));
119    System.out.println();
120
121    PrintStream dst = null;
122    if (cliContext.getOutput() == null) {
123      dst = System.out;
124    } else {
125      dst = new PrintStream(new FileOutputStream(cliContext.getOutput()));
126    }
127
128    ValidationOutputRenderer renderer = makeValidationOutputRenderer(cliContext);
129    renderer.setOutput(dst);
130    renderer.setCrumbTrails(validator.isCrumbTrails());
131    
132    int ec = 0;
133    
134    if (r instanceof Bundle) {
135      if (renderer.handlesBundleDirectly()) {
136        renderer.render((Bundle) r);
137      } else {
138        renderer.start(((Bundle) r).getEntry().size() > 1);
139        for (Bundle.BundleEntryComponent e : ((Bundle) r).getEntry()) {
140          OperationOutcome op = (OperationOutcome) e.getResource();
141          ec = ec + countErrors(op); 
142          renderer.render(op);
143        }
144        renderer.finish();
145      }
146    } else if (r == null) {
147      ec = ec + 1;
148      System.out.println("No output from validation - nothing to validate");
149    } else {
150      renderer.start(false);
151      OperationOutcome op = (OperationOutcome) r;
152      ec = countErrors(op);
153      renderer.render((OperationOutcome) r);
154      renderer.finish();
155    }
156    
157    if (cliContext.getOutput() != null) {
158      dst.close();
159    }
160
161    if (cliContext.getHtmlOutput() != null) {
162      String html = new HTMLOutputGenerator(records).generate(System.currentTimeMillis() - start);
163      TextFile.stringToFile(html, cliContext.getHtmlOutput());
164      System.out.println("HTML Summary in " + cliContext.getHtmlOutput());
165    }
166    System.exit(ec > 0 ? 1 : 0);
167  }
168
169  private int countErrors(OperationOutcome oo) {
170    int error = 0;
171    for (OperationOutcome.OperationOutcomeIssueComponent issue : oo.getIssue()) {
172      if (issue.getSeverity() == OperationOutcome.IssueSeverity.FATAL || issue.getSeverity() == OperationOutcome.IssueSeverity.ERROR)
173        error++;
174    }
175    return error;    
176  }
177
178  private ValidationOutputRenderer makeValidationOutputRenderer(CliContext cliContext) {
179    String style = cliContext.getOutputStyle();
180    // adding to this list? 
181    // Must document the option at https://confluence.hl7.org/display/FHIR/Using+the+FHIR+Validator#UsingtheFHIRValidator-ManagingOutput
182    // if you're going to make a PR, document the link where the outputstyle is documented, along with a sentence that describes it, in the PR notes 
183    if (Utilities.noString(style)) {
184      if (cliContext.getOutput() == null) {
185        return new DefaultRenderer();        
186      } else if (cliContext.getOutput().endsWith(".json")) {
187        return new NativeRenderer(FhirFormat.JSON);
188      } else {
189        return new NativeRenderer(FhirFormat.XML);
190      }
191    } else if (Utilities.existsInList(style, "eslint-compact")) {
192      return new ESLintCompactRenderer();
193    } else if (Utilities.existsInList(style, "csv")) {
194      return new CSVRenderer();
195    } else if (Utilities.existsInList(style, "xml")) {
196      return new NativeRenderer(FhirFormat.XML);
197    } else if (Utilities.existsInList(style, "json")) {
198      return new NativeRenderer(FhirFormat.JSON);
199    } else {
200      System.out.println("Unknown output style '"+style+"'");
201      return new DefaultRenderer();      
202    }
203  }
204
205  public void convertSources(CliContext cliContext, ValidationEngine validator) throws Exception {
206
207      if (!((cliContext.getOutput() == null) ^ (cliContext.getOutputSuffix() == null))) {
208        throw new Exception("Convert requires one of {-output, -outputSuffix} parameter to be set");
209      }
210
211      List<String> sources = cliContext.getSources();
212      if ((sources.size() == 1) && (cliContext.getOutput() != null)) {
213        System.out.println(" ...convert");
214        validator.convert(sources.get(0), cliContext.getOutput());
215      } else {
216        if (cliContext.getOutputSuffix() == null) {
217          throw new Exception("Converting multiple/wildcard sources requires a -outputSuffix parameter to be set");
218        }
219        for (int i = 0; i < sources.size(); i++) {
220            String output = sources.get(i) + "." + cliContext.getOutputSuffix();
221            validator.convert(sources.get(i), output);
222            System.out.println(" ...convert [" + i +  "] (" + sources.get(i) + " to " + output + ")");
223        }
224      }
225  }
226
227  public void evaluateFhirpath(CliContext cliContext, ValidationEngine validator) throws Exception {
228    System.out.println(" ...evaluating " + cliContext.getFhirpath());
229    System.out.println(validator.evaluateFhirPath(cliContext.getSources().get(0), cliContext.getFhirpath()));
230  }
231
232  public void generateSnapshot(CliContext cliContext, ValidationEngine validator) throws Exception {
233
234      if (!((cliContext.getOutput() == null) ^ (cliContext.getOutputSuffix() == null))) {
235        throw new Exception("Snapshot generation requires one of {-output, -outputSuffix} parameter to be set");
236      }
237
238      List<String> sources = cliContext.getSources();
239      if ((sources.size() == 1) && (cliContext.getOutput() != null)) {
240        StructureDefinition r = validator.snapshot(sources.get(0), cliContext.getSv());
241        System.out.println(" ...generated snapshot successfully");
242        validator.handleOutput(r, cliContext.getOutput(), cliContext.getSv());
243      } else {
244        if (cliContext.getOutputSuffix() == null) {
245          throw new Exception("Snapshot generation for multiple/wildcard sources requires a -outputSuffix parameter to be set");
246        }
247        for (int i = 0; i < sources.size(); i++) {
248          StructureDefinition r = validator.snapshot(sources.get(i), cliContext.getSv());
249          String output = sources.get(i) + "." + cliContext.getOutputSuffix();
250          validator.handleOutput(r, output, cliContext.getSv());
251          System.out.println(" ...generated snapshot [" + i +  "] successfully (" + sources.get(i) + " to " + output + ")");
252        }
253      }
254
255  }
256
257  public void generateNarrative(CliContext cliContext, ValidationEngine validator) throws Exception {
258    Resource r = validator.generate(cliContext.getSources().get(0), cliContext.getSv());
259    System.out.println(" ...generated narrative successfully");
260    if (cliContext.getOutput() != null) {
261      validator.handleOutput(r, cliContext.getOutput(), cliContext.getSv());
262    }
263  }
264
265  public void transform(CliContext cliContext, ValidationEngine validator) throws Exception {
266    if (cliContext.getSources().size() > 1)
267      throw new Exception("Can only have one source when doing a transform (found " + cliContext.getSources() + ")");
268    if (cliContext.getTxServer() == null)
269      throw new Exception("Must provide a terminology server when doing a transform");
270    if (cliContext.getMap() == null)
271      throw new Exception("Must provide a map when doing a transform");
272    try {
273      ContextUtilities cu = new ContextUtilities(validator.getContext());
274      List<StructureDefinition> structures =  cu.allStructures();
275      for (StructureDefinition sd : structures) {
276        if (!sd.hasSnapshot()) {
277          if (sd.getKind() != null && sd.getKind() == StructureDefinitionKind.LOGICAL) {
278            cu.generateSnapshot(sd, true);
279          } else {
280            cu.generateSnapshot(sd, false);
281          }
282        }
283      }
284      validator.setMapLog(cliContext.getMapLog());
285      org.hl7.fhir.r5.elementmodel.Element r = validator.transform(cliContext.getSources().get(0), cliContext.getMap());
286      System.out.println(" ...success");
287      if (cliContext.getOutput() != null) {
288        FileOutputStream s = new FileOutputStream(cliContext.getOutput());
289        if (cliContext.getOutput() != null && cliContext.getOutput().endsWith(".json"))
290          new org.hl7.fhir.r5.elementmodel.JsonParser(validator.getContext()).compose(r, s, IParser.OutputStyle.PRETTY, null);
291        else
292          new org.hl7.fhir.r5.elementmodel.XmlParser(validator.getContext()).compose(r, s, IParser.OutputStyle.PRETTY, null);
293        s.close();
294      }
295    } catch (Exception e) {
296      System.out.println(" ...Failure: " + e.getMessage());
297      e.printStackTrace();
298    }
299  }
300
301  public void compile(CliContext cliContext, ValidationEngine validator) throws Exception {
302    if (cliContext.getSources().size() > 0)
303      throw new Exception("Cannot specify sources when compling transform (found " + cliContext.getSources() + ")");
304    if (cliContext.getMap() == null)
305      throw new Exception("Must provide a map when compiling a transform");
306    if (cliContext.getOutput() == null)
307      throw new Exception("Must provide an output name when compiling a transform");
308    try {
309      ContextUtilities cu = new ContextUtilities(validator.getContext());
310      List<StructureDefinition> structures = cu.allStructures();
311      for (StructureDefinition sd : structures) {
312        if (!sd.hasSnapshot()) {
313          if (sd.getKind() != null && sd.getKind() == StructureDefinitionKind.LOGICAL) {
314            cu.generateSnapshot(sd, true);
315          } else {
316            cu.generateSnapshot(sd, false);
317          }
318        }
319      }
320      validator.setMapLog(cliContext.getMapLog());
321      StructureMap map = validator.compile(cliContext.getMap());
322      if (map == null)
323        throw new Exception("Unable to locate map " + cliContext.getMap());
324      validator.handleOutput(map, cliContext.getOutput(), validator.getVersion());
325      System.out.println(" ...success");
326    } catch (Exception e) {
327      System.out.println(" ...Failure: " + e.getMessage());
328      e.printStackTrace();
329    }
330  }
331
332  public void transformVersion(CliContext cliContext, ValidationEngine validator) throws Exception {
333    if (cliContext.getSources().size() > 1) {
334      throw new Exception("Can only have one source when converting versions (found " + cliContext.getSources() + ")");
335    }
336    if (cliContext.getTargetVer() == null) {
337      throw new Exception("Must provide a map when converting versions");
338    }
339    if (cliContext.getOutput() == null) {
340      throw new Exception("Must nominate an output when converting versions");
341    }
342    try {
343      if (cliContext.getMapLog() != null) {
344        validator.setMapLog(cliContext.getMapLog());
345      }
346      byte[] r = validator.transformVersion(cliContext.getSources().get(0), cliContext.getTargetVer(), cliContext.getOutput().endsWith(".json") ? Manager.FhirFormat.JSON : Manager.FhirFormat.XML, cliContext.getCanDoNative());
347      System.out.println(" ...success");
348      TextFile.bytesToFile(r, cliContext.getOutput());
349    } catch (Exception e) {
350      System.out.println(" ...Failure: " + e.getMessage());
351      e.printStackTrace();
352    }
353  }
354
355  public ValidationEngine initializeValidator(CliContext cliContext, String definitions, TimeTracker tt) throws Exception {
356    return sessionCache.fetchSessionValidatorEngine(initializeValidator(cliContext, definitions, tt, null));
357  }
358
359  public String initializeValidator(CliContext cliContext, String definitions, TimeTracker tt, String sessionId) throws Exception {
360    tt.milestone();
361    sessionCache.removeExpiredSessions();
362    if (!sessionCache.sessionExists(sessionId)) {
363      if (sessionId != null) {
364        System.out.println("No such cached session exists for session id " + sessionId + ", re-instantiating validator.");
365      }
366      System.out.print("  Load FHIR v" + cliContext.getSv() + " from " + definitions);
367      ValidationEngine validator = new ValidationEngine.ValidationEngineBuilder().withTHO(false).withVersion(cliContext.getSv()).withTimeTracker(tt).withUserAgent("fhir/validator").fromSource(definitions);
368
369      sessionId = sessionCache.cacheSession(validator);
370
371      FhirPublication ver = FhirPublication.fromCode(cliContext.getSv());
372      System.out.println(" - " + validator.getContext().countAllCaches() + " resources (" + tt.milestone() + ")");
373      IgLoader igLoader = new IgLoader(validator.getPcm(), validator.getContext(), validator.getVersion(), validator.isDebug());
374      igLoader.loadIg(validator.getIgs(), validator.getBinaries(), "hl7.terminology", false);
375      if (!VersionUtilities.isR5Ver(validator.getContext().getVersion())) {
376        System.out.print("  Load R5 Extensions");
377        R5ExtensionsLoader r5e = new R5ExtensionsLoader(validator.getPcm(), validator.getContext());
378        r5e.load();
379        r5e.loadR5Extensions();
380        System.out.println(" - " + r5e.getCount() + " resources (" + tt.milestone() + ")");
381      }
382      System.out.print("  Terminology server " + cliContext.getTxServer());
383      String txver = validator.setTerminologyServer(cliContext.getTxServer(), cliContext.getTxLog(), ver);
384      System.out.println(" - Version " + txver + " (" + tt.milestone() + ")");
385      validator.setDebug(cliContext.isDoDebug());
386      validator.getContext().setLogger(new SystemOutLoggingService(cliContext.isDoDebug()));
387      for (String src : cliContext.getIgs()) {
388        igLoader.loadIg(validator.getIgs(), validator.getBinaries(), src, cliContext.isRecursive());
389      }
390      System.out.println("  Package Summary: "+validator.getContext().loadedPackageSummary());
391      System.out.print("  Get set... ");
392      validator.setQuestionnaireMode(cliContext.getQuestionnaireMode());
393      validator.setLevel(cliContext.getLevel());
394      validator.setDoNative(cliContext.isDoNative());
395      validator.setHintAboutNonMustSupport(cliContext.isHintAboutNonMustSupport());
396      for (String s : cliContext.getExtensions()) {
397        if ("any".equals(s)) {
398          validator.setAnyExtensionsAllowed(true);
399        } else {          
400          validator.getExtensionDomains().add(s);
401        }
402      }
403      validator.setLanguage(cliContext.getLang());
404      validator.setLocale(cliContext.getLocale());
405      validator.setSnomedExtension(cliContext.getSnomedCTCode());
406      validator.setAssumeValidRestReferences(cliContext.isAssumeValidRestReferences());
407      validator.setShowMessagesFromReferences(cliContext.isShowMessagesFromReferences());
408      validator.setDoImplicitFHIRPathStringConversion(cliContext.isDoImplicitFHIRPathStringConversion());
409      validator.setHtmlInMarkdownCheck(cliContext.getHtmlInMarkdownCheck());
410      validator.setNoExtensibleBindingMessages(cliContext.isNoExtensibleBindingMessages());
411      validator.setNoUnicodeBiDiControlChars(cliContext.isNoUnicodeBiDiControlChars());
412      validator.setNoInvariantChecks(cliContext.isNoInvariants());
413      validator.setWantInvariantInMessage(cliContext.isWantInvariantsInMessages());
414      validator.setSecurityChecks(cliContext.isSecurityChecks());
415      validator.setCrumbTrails(cliContext.isCrumbTrails());
416      validator.setForPublication(cliContext.isForPublication());
417      validator.setShowTimes(cliContext.isShowTimes());
418      validator.setAllowExampleUrls(cliContext.isAllowExampleUrls());
419      StandAloneValidatorFetcher fetcher = new StandAloneValidatorFetcher(validator.getPcm(), validator.getContext(), validator);    
420      validator.setFetcher(fetcher);
421      validator.getContext().setLocator(fetcher);
422      validator.getBundleValidationRules().addAll(cliContext.getBundleValidationRules());
423      validator.setJurisdiction(CodeSystemUtilities.readCoding(cliContext.getJurisdiction()));
424      TerminologyCache.setNoCaching(cliContext.isNoInternalCaching());
425      validator.prepare(); // generate any missing snapshots
426      System.out.println(" go (" + tt.milestone() + ")");
427    } else {
428      System.out.println("Cached session exists for session id " + sessionId + ", returning stored validator session id.");
429    }
430    return sessionId;
431  }
432
433
434  
435
436  public String determineVersion(CliContext cliContext) throws Exception {
437    return determineVersion(cliContext, null);
438  }
439
440  public String determineVersion(CliContext cliContext, String sessionId) throws Exception {
441    if (cliContext.getMode() != EngineMode.VALIDATION) {
442      return "current";
443    }
444    System.out.println("Scanning for versions (no -version parameter):");
445    VersionSourceInformation versions = scanForVersions(cliContext);
446    for (String s : versions.getReport()) {
447      if (!s.equals("(nothing found)")) {
448        System.out.println("  " + s);
449      }
450    }
451    if (versions.isEmpty()) {
452      System.out.println("  No Version Info found: Using Default version '" + VersionUtilities.CURRENT_DEFAULT_VERSION + "'");
453      return VersionUtilities.CURRENT_DEFAULT_FULL_VERSION;
454    }
455    if (versions.size() == 1) {
456      System.out.println("-> use version " + versions.version());
457      return versions.version();
458    }
459    throw new Exception("-> Multiple versions found. Specify a particular version using the -version parameter");
460  }
461
462  public void generateSpreadsheet(CliContext cliContext, ValidationEngine validator) throws Exception {
463    CanonicalResource cr = validator.loadCanonicalResource(cliContext.getSources().get(0), cliContext.getSv());
464    boolean ok = true;
465    if (cr instanceof StructureDefinition) {
466      new StructureDefinitionSpreadsheetGenerator(validator.getContext(), false, false).renderStructureDefinition((StructureDefinition) cr, false).finish(new FileOutputStream(cliContext.getOutput()));
467    } else if (cr instanceof CodeSystem) {
468      new CodeSystemSpreadsheetGenerator(validator.getContext()).renderCodeSystem((CodeSystem) cr).finish(new FileOutputStream(cliContext.getOutput()));
469    } else if (cr instanceof ValueSet) {
470      new ValueSetSpreadsheetGenerator(validator.getContext()).renderValueSet((ValueSet) cr).finish(new FileOutputStream(cliContext.getOutput()));
471    } else if (cr instanceof ConceptMap) {
472      new ConceptMapSpreadsheetGenerator(validator.getContext()).renderConceptMap((ConceptMap) cr).finish(new FileOutputStream(cliContext.getOutput()));
473    } else {
474      ok = false;
475      System.out.println(" ...Unable to generate spreadsheet for "+cliContext.getSources().get(0)+": no way to generate a spreadsheet for a "+cr.fhirType());
476    }
477    
478    if (ok) {
479      System.out.println(" ...generated spreadsheet successfully");
480    } 
481  }
482}