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