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 fromNothing() throws FileNotFoundException, IOException, FHIRException { 151 SimpleWorkerContext res = new SimpleWorkerContext(); 152 return res; 153 } 154 155 public static SimpleWorkerContext fromPackage(NpmPackage pi, boolean allowDuplicates) throws FileNotFoundException, IOException, FHIRException { 156 SimpleWorkerContext res = new SimpleWorkerContext(); 157 res.setAllowLoadingDuplicates(allowDuplicates); 158 res.loadFromPackage(pi, null); 159 return res; 160 } 161 162 public static SimpleWorkerContext fromPackage(NpmPackage pi) throws FileNotFoundException, IOException, FHIRException { 163 SimpleWorkerContext res = new SimpleWorkerContext(); 164 res.loadFromPackage(pi, null); 165 return res; 166 } 167 168 public static SimpleWorkerContext fromPackage(NpmPackage pi, IContextResourceLoader loader) throws FileNotFoundException, IOException, FHIRException { 169 SimpleWorkerContext res = new SimpleWorkerContext(); 170 res.setAllowLoadingDuplicates(true); 171 res.version = pi.getNpm().asString("version"); 172 res.loadFromPackage(pi, loader); 173 return res; 174 } 175 176 public static SimpleWorkerContext fromPack(String path, boolean allowDuplicates) throws FileNotFoundException, IOException, FHIRException { 177 SimpleWorkerContext res = new SimpleWorkerContext(); 178 res.setAllowLoadingDuplicates(allowDuplicates); 179 res.loadFromPack(path, null); 180 return res; 181 } 182 183 public static SimpleWorkerContext fromPack(String path, IContextResourceLoader loader) throws FileNotFoundException, IOException, FHIRException { 184 SimpleWorkerContext res = new SimpleWorkerContext(); 185 res.loadFromPack(path, loader); 186 return res; 187 } 188 189 public static SimpleWorkerContext fromClassPath() throws IOException, FHIRException { 190 SimpleWorkerContext res = new SimpleWorkerContext(); 191 res.loadFromStream(SimpleWorkerContext.class.getResourceAsStream("validation.json.zip"), null); 192 return res; 193 } 194 195 public static SimpleWorkerContext fromClassPath(String name) throws IOException, FHIRException { 196 InputStream s = SimpleWorkerContext.class.getResourceAsStream("/"+name); 197 SimpleWorkerContext res = new SimpleWorkerContext(); 198 res.loadFromStream(s, null); 199 return res; 200 } 201 202 public static SimpleWorkerContext fromDefinitions(Map<String, byte[]> source) throws IOException, FHIRException { 203 SimpleWorkerContext res = new SimpleWorkerContext(); 204 for (String name : source.keySet()) { 205 res.loadDefinitionItem(name, new ByteArrayInputStream(source.get(name)), null); 206 } 207 return res; 208 } 209 210 public static SimpleWorkerContext fromDefinitions(Map<String, byte[]> source, IContextResourceLoader loader) throws FileNotFoundException, IOException, FHIRException { 211 SimpleWorkerContext res = new SimpleWorkerContext(); 212 for (String name : source.keySet()) { 213 try { 214 res.loadDefinitionItem(name, new ByteArrayInputStream(source.get(name)), loader); 215 } catch (Exception e) { 216 System.out.println("Error loading "+name+": "+e.getMessage()); 217 throw new FHIRException("Error loading "+name+": "+e.getMessage(), e); 218 } 219 } 220 return res; 221 } 222 private void loadDefinitionItem(String name, InputStream stream, IContextResourceLoader loader) throws IOException, FHIRException { 223 if (name.endsWith(".xml")) 224 loadFromFile(stream, name, loader); 225 else if (name.endsWith(".json")) 226 loadFromFileJson(stream, name, loader); 227 else if (name.equals("version.info")) 228 readVersionInfo(stream); 229 else 230 loadBytes(name, stream); 231 } 232 233 234 public String connectToTSServer(TerminologyClient client, String log) throws URISyntaxException, FHIRException { 235 tlog("Connect to "+client.getAddress()); 236 txClient = client; 237 txLog = new HTMLClientLogger(log); 238 txClient.setLogger(txLog); 239 return txClient.getCapabilitiesStatementQuick().getSoftware().getVersion(); 240 } 241 242 public void loadFromFile(InputStream stream, String name, IContextResourceLoader loader) throws IOException, FHIRException { 243 Resource f; 244 try { 245 if (loader != null) 246 f = loader.loadBundle(stream, false); 247 else { 248 XmlParser xml = new XmlParser(); 249 f = xml.parse(stream); 250 } 251 } catch (DataFormatException e1) { 252 throw new org.hl7.fhir.exceptions.FHIRFormatError("Error parsing "+name+":" +e1.getMessage(), e1); 253 } catch (Exception e1) { 254 throw new org.hl7.fhir.exceptions.FHIRFormatError("Error parsing "+name+":" +e1.getMessage(), e1); 255 } 256 if (f instanceof Bundle) { 257 Bundle bnd = (Bundle) f; 258 for (BundleEntryComponent e : bnd.getEntry()) { 259 if (e.getFullUrl() == null) { 260 logger.logDebugMessage(LogCategory.CONTEXT, "unidentified resource in " + name+" (no fullUrl)"); 261 } 262 cacheResource(e.getResource()); 263 } 264 } else if (f instanceof MetadataResource) { 265 MetadataResource m = (MetadataResource) f; 266 cacheResource(m); 267 } 268 } 269 270 private void loadFromFileJson(InputStream stream, String name, IContextResourceLoader loader) throws IOException, FHIRException { 271 Bundle f = null; 272 try { 273 if (loader != null) 274 f = loader.loadBundle(stream, true); 275 else { 276 JsonParser json = new JsonParser(); 277 Resource r = json.parse(stream); 278 if (r instanceof Bundle) 279 f = (Bundle) r; 280 else 281 cacheResource(r); 282 } 283 } catch (FHIRFormatError e1) { 284 throw new org.hl7.fhir.exceptions.FHIRFormatError(e1.getMessage(), e1); 285 } 286 if (f != null) 287 for (BundleEntryComponent e : f.getEntry()) { 288 cacheResource(e.getResource()); 289 } 290 } 291 292 private void loadFromPack(String path, IContextResourceLoader loader) throws FileNotFoundException, IOException, FHIRException { 293 loadFromStream(new CSFileInputStream(path), loader); 294 } 295 296 public void loadFromPackage(NpmPackage pi, IContextResourceLoader loader, String... types) throws FileNotFoundException, IOException, FHIRException { 297 if (types.length == 0) 298 types = new String[] { "StructureDefinition", "ValueSet", "CodeSystem", "SearchParameter", "OperationDefinition", "Questionnaire","ConceptMap","StructureMap", "NamingSystem"}; 299 for (String s : pi.listResources(types)) { 300 loadDefinitionItem(s, pi.load("package", s), loader); 301 } 302 version = pi.version(); 303 } 304 305 public void loadFromFile(String file, IContextResourceLoader loader) throws IOException, FHIRException { 306 loadDefinitionItem(file, new CSFileInputStream(file), loader); 307 } 308 309 private void loadFromStream(InputStream stream, IContextResourceLoader loader) throws IOException, FHIRException { 310 ZipInputStream zip = new ZipInputStream(stream); 311 ZipEntry ze; 312 while ((ze = zip.getNextEntry()) != null) { 313 loadDefinitionItem(ze.getName(), zip, loader); 314 zip.closeEntry(); 315 } 316 zip.close(); 317 } 318 319 private void readVersionInfo(InputStream stream) throws IOException, DefinitionException { 320 byte[] bytes = IOUtils.toByteArray(stream); 321 binaries.put("version.info", bytes); 322 323 String[] vi = new String(bytes).split("\\r?\\n"); 324 for (String s : vi) { 325 if (s.startsWith("version=")) { 326 if (version == null) 327 version = s.substring(8); 328 else if (!version.equals(s.substring(8))) 329 throw new DefinitionException("Version mismatch. The context has version "+version+" loaded, and the new content being loaded is version "+s.substring(8)); 330 } 331 if (s.startsWith("revision=")) 332 revision = s.substring(9); 333 if (s.startsWith("date=")) 334 date = s.substring(5); 335 } 336 } 337 338 private void loadBytes(String name, InputStream stream) throws IOException { 339 byte[] bytes = IOUtils.toByteArray(stream); 340 binaries.put(name, bytes); 341 } 342 343 @Override 344 public IParser getParser(ParserType type) { 345 switch (type) { 346 case JSON: return newJsonParser(); 347 case XML: return newXmlParser(); 348 default: 349 throw new Error("Parser Type "+type.toString()+" not supported"); 350 } 351 } 352 353 @Override 354 public IParser getParser(String type) { 355 if (type.equalsIgnoreCase("JSON")) 356 return new JsonParser(); 357 if (type.equalsIgnoreCase("XML")) 358 return new XmlParser(); 359 throw new Error("Parser Type "+type.toString()+" not supported"); 360 } 361 362 @Override 363 public IParser newJsonParser() { 364 return new JsonParser(); 365 } 366 @Override 367 public IParser newXmlParser() { 368 return new XmlParser(); 369 } 370 371 @Override 372 public INarrativeGenerator getNarrativeGenerator(String prefix, String basePath) { 373 return new NarrativeGenerator(prefix, basePath, this); 374 } 375 376 @Override 377 public IResourceValidator newValidator() throws FHIRException { 378 if (validatorFactory == null) 379 throw new Error("No validator configured"); 380 return validatorFactory.makeValidator(this); 381 } 382 383 384 385 386 @Override 387 public List<String> getResourceNames() { 388 List<String> result = new ArrayList<String>(); 389 for (StructureDefinition sd : listStructures()) { 390 if (sd.getKind() == StructureDefinitionKind.RESOURCE && sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) 391 result.add(sd.getName()); 392 } 393 Collections.sort(result); 394 return result; 395 } 396 397 @Override 398 public List<String> getTypeNames() { 399 List<String> result = new ArrayList<String>(); 400 for (StructureDefinition sd : listStructures()) { 401 if (sd.getKind() != StructureDefinitionKind.LOGICAL && sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) 402 result.add(sd.getName()); 403 } 404 Collections.sort(result); 405 return result; 406 } 407 408 @Override 409 public String getAbbreviation(String name) { 410 return "xxx"; 411 } 412 413 @Override 414 public boolean isDatatype(String typeSimple) { 415 // TODO Auto-generated method stub 416 return false; 417 } 418 419 @Override 420 public boolean isResource(String t) { 421 StructureDefinition sd; 422 try { 423 sd = fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+t); 424 } catch (Exception e) { 425 return false; 426 } 427 if (sd == null) 428 return false; 429 if (sd.getDerivation() == TypeDerivationRule.CONSTRAINT) 430 return false; 431 return sd.getKind() == StructureDefinitionKind.RESOURCE; 432 } 433 434 @Override 435 public boolean hasLinkFor(String typeSimple) { 436 return false; 437 } 438 439 @Override 440 public String getLinkFor(String corePath, String typeSimple) { 441 return null; 442 } 443 444 @Override 445 public BindingResolution resolveBinding(StructureDefinition profile, ElementDefinitionBindingComponent binding, String path) { 446 return null; 447 } 448 449 @Override 450 public BindingResolution resolveBinding(StructureDefinition profile, String url, String path) { 451 return null; 452 } 453 454 @Override 455 public String getLinkForProfile(StructureDefinition profile, String url) { 456 return null; 457 } 458 459 public Questionnaire getQuestionnaire() { 460 return questionnaire; 461 } 462 463 public void setQuestionnaire(Questionnaire questionnaire) { 464 this.questionnaire = questionnaire; 465 } 466 467 @Override 468 public Set<String> typeTails() { 469 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")); 470 } 471 472 @Override 473 public List<StructureDefinition> allStructures() { 474 List<StructureDefinition> result = new ArrayList<StructureDefinition>(); 475 Set<StructureDefinition> set = new HashSet<StructureDefinition>(); 476 for (StructureDefinition sd : listStructures()) { 477 if (!set.contains(sd)) { 478 try { 479 generateSnapshot(sd); 480 } catch (Exception e) { 481 System.out.println("Unable to generate snapshot for "+sd.getUrl()+" because "+e.getMessage()); 482 } 483 result.add(sd); 484 set.add(sd); 485 } 486 } 487 return result; 488 } 489 490 public void loadBinariesFromFolder(String folder) throws FileNotFoundException, Exception { 491 for (String n : new File(folder).list()) { 492 loadBytes(n, new FileInputStream(Utilities.path(folder, n))); 493 } 494 } 495 496 public void loadBinariesFromFolder(NpmPackage pi) throws FileNotFoundException, Exception { 497 for (String n : pi.list("other")) { 498 loadBytes(n, pi.load("other", n)); 499 } 500 } 501 502 public void loadFromFolder(String folder) throws FileNotFoundException, Exception { 503 for (String n : new File(folder).list()) { 504 if (n.endsWith(".json")) 505 loadFromFile(Utilities.path(folder, n), new JsonParser()); 506 else if (n.endsWith(".xml")) 507 loadFromFile(Utilities.path(folder, n), new XmlParser()); 508 } 509 } 510 511 private void loadFromFile(String filename, IParser p) throws FileNotFoundException, Exception { 512 Resource r; 513 try { 514 r = p.parse(new FileInputStream(filename)); 515 if (r.getResourceType() == ResourceType.Bundle) { 516 for (BundleEntryComponent e : ((Bundle) r).getEntry()) { 517 cacheResource(e.getResource()); 518 } 519 } else { 520 cacheResource(r); 521 } 522 } catch (Exception e) { 523 return; 524 } 525 } 526 527 public Map<String, byte[]> getBinaries() { 528 return binaries; 529 } 530 531 @Override 532 public boolean prependLinks() { 533 return false; 534 } 535 536 @Override 537 public boolean hasCache() { 538 return false; 539 } 540 541 @Override 542 public String getVersion() { 543 return version; 544 } 545 546 547 public List<StructureMap> findTransformsforSource(String url) { 548 List<StructureMap> res = new ArrayList<StructureMap>(); 549 for (StructureMap map : listTransforms()) { 550 boolean match = false; 551 boolean ok = true; 552 for (StructureMapStructureComponent t : map.getStructure()) { 553 if (t.getMode() == StructureMapModelMode.SOURCE) { 554 match = match || t.getUrl().equals(url); 555 ok = ok && t.getUrl().equals(url); 556 } 557 } 558 if (match && ok) 559 res.add(map); 560 } 561 return res; 562 } 563 564 public IValidatorFactory getValidatorFactory() { 565 return validatorFactory; 566 } 567 568 public void setValidatorFactory(IValidatorFactory validatorFactory) { 569 this.validatorFactory = validatorFactory; 570 } 571 572 @Override 573 public <T extends Resource> T fetchResource(Class<T> class_, String uri) { 574 T r = super.fetchResource(class_, uri); 575 if (r instanceof StructureDefinition) { 576 StructureDefinition p = (StructureDefinition)r; 577 try { 578 generateSnapshot(p); 579 } catch (Exception e) { 580 // not sure what to do in this case? 581 System.out.println("Unable to generate snapshot for "+uri+": "+e.getMessage()); 582 } 583 } 584 return r; 585 } 586 587 public void generateSnapshot(StructureDefinition p) throws DefinitionException, FHIRException { 588 if (!p.hasSnapshot() && p.getKind() != StructureDefinitionKind.LOGICAL) { 589 if (!p.hasBaseDefinition()) 590 throw new DefinitionException("Profile "+p.getName()+" ("+p.getUrl()+") has no base and no snapshot"); 591 StructureDefinition sd = fetchResource(StructureDefinition.class, p.getBaseDefinition()); 592 if (sd == null) 593 throw new DefinitionException("Profile "+p.getName()+" ("+p.getUrl()+") base "+p.getBaseDefinition()+" could not be resolved"); 594 List<ValidationMessage> msgs = new ArrayList<ValidationMessage>(); 595 List<String> errors = new ArrayList<String>(); 596 ProfileUtilities pu = new ProfileUtilities(this, msgs, this); 597 pu.setThrowException(false); 598 pu.sortDifferential(sd, p, p.getUrl(), errors); 599 for (String err : errors) 600 msgs.add(new ValidationMessage(Source.ProfileValidator, IssueType.EXCEPTION, p.getUserString("path"), "Error sorting Differential: "+err, ValidationMessage.IssueSeverity.ERROR)); 601 pu.generateSnapshot(sd, p, p.getUrl(), Utilities.extractBaseUrl(sd.getUserString("path")), p.getName()); 602 for (ValidationMessage msg : msgs) { 603 if ((!ignoreProfileErrors && msg.getLevel() == ValidationMessage.IssueSeverity.ERROR) || msg.getLevel() == ValidationMessage.IssueSeverity.FATAL) 604 throw new DefinitionException("Profile "+p.getName()+" ("+p.getUrl()+"). Error generating snapshot: "+msg.getMessage()); 605 } 606 if (!p.hasSnapshot()) 607 throw new FHIRException("Profile "+p.getName()+" ("+p.getUrl()+"). Error generating snapshot"); 608 pu = null; 609 } 610 } 611 612 public boolean isIgnoreProfileErrors() { 613 return ignoreProfileErrors; 614 } 615 616 public void setIgnoreProfileErrors(boolean ignoreProfileErrors) { 617 this.ignoreProfileErrors = ignoreProfileErrors; 618 } 619 620 public String listMapUrls() { 621 return Utilities.listCanonicalUrls(transforms.keySet()); 622 } 623 624 625 626 627}