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}