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