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}