All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.hl7.fhir.validation.special.R4R5MapTester Maven / Gradle / Ivy

The newest version!
package org.hl7.fhir.validation.special;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import org.hl7.fhir.exceptions.DefinitionException;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.exceptions.FHIRFormatError;
import org.hl7.fhir.r5.context.IContextResourceLoader;
import org.hl7.fhir.r5.context.SimpleWorkerContext;
import org.hl7.fhir.r5.context.SimpleWorkerContext.SimpleWorkerContextBuilder;
import org.hl7.fhir.r5.elementmodel.Element;
import org.hl7.fhir.r5.elementmodel.Manager;
import org.hl7.fhir.r5.formats.IParser.OutputStyle;
import org.hl7.fhir.r5.model.CanonicalResource;
import org.hl7.fhir.r5.model.Enumerations.PublicationStatus;
import org.hl7.fhir.r5.model.Parameters;
import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.model.ResourceFactory;
import org.hl7.fhir.r5.model.StructureDefinition;
import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind;
import org.hl7.fhir.r5.model.StructureMap;
import org.hl7.fhir.r5.model.StructureMap.StructureMapInputMode;
import org.hl7.fhir.r5.utils.structuremap.ResolvedGroup;
import org.hl7.fhir.r5.utils.structuremap.StructureMapUtilities;
import org.hl7.fhir.r5.utils.validation.IResourceValidator;
import org.hl7.fhir.r5.utils.validation.IValidatorResourceFetcher;
import org.hl7.fhir.r5.utils.validation.constants.BestPracticeWarningLevel;
import org.hl7.fhir.r5.utils.validation.constants.IdStatus;
import org.hl7.fhir.utilities.TextFile;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.VersionUtilities;
import org.hl7.fhir.utilities.filesystem.ManagedFileAccess;
import org.hl7.fhir.utilities.json.JsonException;
import org.hl7.fhir.utilities.json.model.JsonObject;
import org.hl7.fhir.utilities.json.model.JsonProperty;
import org.hl7.fhir.utilities.json.parser.JsonParser;
import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
import org.hl7.fhir.utilities.npm.NpmPackage;
import org.hl7.fhir.utilities.validation.ValidationMessage;
import org.hl7.fhir.validation.ValidatorUtils;
import org.hl7.fhir.validation.instance.InstanceValidator;

public class R4R5MapTester implements IValidatorResourceFetcher {

  public class Stats {

    private Set errors = new HashSet<>();
    private int total;
    private int parsed;
    private int forward;
    private int validated;
    private int error;
    private int back;
    private int elements;
    private int lostElements;
    
    public void example() {
      total++;
    }

    public void parsed() {
      parsed++;
    }

    public void error(String s) {
      errors.add(s);
    }

    public void forward() {
     forward++; 
    }

    public int totalCount() {
      return total;
    }

    public int parseCount() {
      return parsed;
    }

    public int forwardCount() {
      return forward;
    }

    public int validatedCount() {
      return validated;
    }

    public int errorCount() {
      return error;
    }

    public int backCount() {
      return back;
    }

    public String summary() {
      if (errors.size() == 0) {
        return "All OK (~"+(elements == 0 ? "n/a" : ((lostElements * 100) / elements))+"% lost in "+total+" examples)";
      } else {
        return String.join(", ", errors);
      }
    }

    public boolean ok() {
      return errors.size() == 0;
    }

    public void valid(boolean valid) {
      validated++;
      if (!valid) {
        error++;
      }
    }
    

    public void back() {
      back++;
    }

    public void elements(int i) {
      elements = elements+i;
    }

    public void lost(int i) {
      lostElements = lostElements + i;
    }

    public int elementsCount() {
      return elements;
    }

    public int lostCount() {
      return lostElements;
    }
  }

  private boolean saveProcess = true;

  private SimpleWorkerContext context;
  private FilesystemPackageCacheManager pcm;
  private StructureMapUtilities utils;
  private List allMaps;

  private InstanceValidator validator;

  private StringBuilder log;

  public static void main(String[] args) throws JsonException, IOException {
    
    // arg[0] is the location of the fhir-extensions repo
    new R4R5MapTester().testMaps(args[0], args[1], args[2]);
  }

  public void testMaps(String src, String maps, String filter) throws JsonException, IOException {
    log = new StringBuilder();
    log("Load Test Outcomes");
    JsonObject json = JsonParser.parseObjectFromFile(Utilities.path(src, "input", "_data", "conversions.json"));
    log("Load R5");
    pcm = new FilesystemPackageCacheManager.Builder().build();
    context = new SimpleWorkerContextBuilder().withAllowLoadingDuplicates(true).fromPackage(pcm.loadPackage("hl7.fhir.r5.core#current"));
    log("Load Maps");
//     context.loadFromPackage(pcm.loadPackage(), null);
    
    loadPackage("hl7.terminology.r5#5.0.0", false);
    utils = new StructureMapUtilities(context);
    utils.setDebug(false);
        
    loadPackage("hl7.fhir.uv.extensions#dev", maps == null);
    if (maps != null) {
      loadFromFolder(Utilities.path(maps, "r4-2-r5"));
      loadFromFolder(Utilities.path(maps, "r4b-2-r5"));
      loadFromFolder(Utilities.path(maps, "r5-2-r4"));
      loadFromFolder(Utilities.path(maps, "r5-2-r4b"));
    }
    loadPackage("hl7.fhir.r4.core#4.0.1", false);
    loadPackage("hl7.fhir.r4b.core#4.3.0", false);
    
    validator = new InstanceValidator(context, null, null, null);
    validator.setSuppressLoincSnomedMessages(true);
    validator.setResourceIdRule(IdStatus.REQUIRED);
    validator.setBestPracticeWarningLevel(BestPracticeWarningLevel.Warning);
    validator.getExtensionDomains().add("http://hl7.org/fhir/us");
    validator.setFetcher(this);
    validator.setAllowExamples(true);
    validator.setDebug(false);
    validator.setForPublication(true);
    validator.setNoTerminologyChecks(true);
    context.setExpansionParameters(new Parameters());
    
    log("Load R4 Examples");
    NpmPackage r4Examples = pcm.loadPackage("hl7.fhir.r4.examples");
    log("Load R4B Examples");
    NpmPackage r4bExamples = pcm.loadPackage("hl7.fhir.r4b.examples");

    allMaps = context.fetchResourcesByType(StructureMap.class);
    
    log("Go. "+context.getResourceNames().size()+" types of resources");
    log("Map Count = "+allMaps.size());
    boolean changed = false;
    for (JsonProperty jp : json.getProperties()) {
      String rn = jp.getName();
      if ("*".equals(filter) || rn.equals(filter)) {
        log("  "+rn);
        JsonObject o = json.getJsonObject(rn);
        StructureDefinition sd = context.fetchTypeDefinition(rn);
        if (sd != null) {
          List mapSrc = utils.getMapsForUrl(allMaps, sd.getUrl(), StructureMapInputMode.SOURCE);
          List mapTgt = utils.getMapsForUrl(allMaps, sd.getUrl(), StructureMapInputMode.TARGET);
          changed = checkMaps(sd, o.getJsonObject("r4"), "r4", "http://hl7.org/fhir/4.0", mapSrc, mapTgt, r4Examples) || changed;
          changed = checkMaps(sd, o.getJsonObject("r4b"), "r4b", "http://hl7.org/fhir/4.3", mapSrc, mapTgt, r4bExamples) || changed;
          JsonParser.compose(json, ManagedFileAccess.outStream(Utilities.path(src, "input", "_data", "conversions.json")), true);
        }
        System.out.println("   .. done");
      }
    }
    TextFile.stringToFile(log.toString(), Utilities.path(src, "input", "_data", "validation.log"));
    log("Done!");
//    load R4
//    load R4B
//    load the maps 
//    load the existing outcomes 
//    
//    for R4: 
//      fetch the examples package
//      for each R5 resource type
//      find the map
//      get the source name
//      iterate the examples
//        find the applicable map
//        do the conversion
//        write outcome
    
  }

  private void loadFromFolder(String path) throws FHIRFormatError, FHIRException, FileNotFoundException, IOException {
    log("Load "+path);
    for (File f : ManagedFileAccess.file(path).listFiles()) {
      if (f.getName().endsWith(".json")) {
        context.cacheResource(new org.hl7.fhir.r5.formats.JsonParser().parse(ManagedFileAccess.inStream(f)));
      }
      if (f.getName().endsWith(".fml")) {
        context.cacheResource(utils.parse(TextFile.fileToString(f), f.getName()));
      }
    }
    
  }

  private void loadPackage(String pid, boolean loadMaps) throws FHIRException, IOException {
    log("Load "+pid);
    NpmPackage npm = pcm.loadPackage(pid);
    IContextResourceLoader loader = ValidatorUtils.loaderForVersion(npm.fhirVersion());
    if (!loadMaps && loader.getTypes().contains("StructureMap")) {
      loader.getTypes().remove("StructureMap");
    }
    loader.setPatchUrls(VersionUtilities.isCorePackage(npm.id()));
    context.loadFromPackage(npm, loader);
  }

  private boolean checkMaps(StructureDefinition sd, JsonObject json, String code, String ns, List mapSrc, List mapTgt, NpmPackage examples) throws IOException {
    List src = utils.getMapsForUrlPrefix(mapSrc, ns, StructureMapInputMode.TARGET);
    List tgt = utils.getMapsForUrlPrefix(mapTgt, ns, StructureMapInputMode.SOURCE);
    if (src.size() + tgt.size() == 0) {
      json.set("status", "No Maps Defined");
      json.set("testColor", "#dddddd");
      json.set("testMessage", "--");
    } else {
      boolean isDraft = false;
      for (StructureMap map : src) {
        isDraft = map.getStatus() == PublicationStatus.DRAFT || isDraft;
      }
      for (StructureMap map : tgt) {
        isDraft = map.getStatus() == PublicationStatus.DRAFT || isDraft;
      }
      json.set("status", ""+(src.size()+tgt.size())+" Maps Defined"+(isDraft ? " (draft)" : ""));
      json.set("testColor", "#ffcccc");
      if (sd.getKind() == StructureDefinitionKind.RESOURCE) {
        if (tgt.size() == 1 && src.size() == 1) {
          StructureMap tgtM = tgt.get(0);
          ResolvedGroup tgtG = utils.getGroupForUrl(tgtM, sd.getUrl(), StructureMapInputMode.TARGET);
          String tgtU = utils.getInputType(tgtG, StructureMapInputMode.SOURCE);
          assert tgtU.startsWith(ns);
          StructureMap srcM = src.get(0);
          ResolvedGroup srcG = utils.getGroupForUrl(srcM, sd.getUrl(), StructureMapInputMode.SOURCE);
          String srcU = utils.getInputType(srcG, StructureMapInputMode.TARGET);
          assert srcU.startsWith(ns);
          if (!srcU.equals(tgtU)) {
            json.set("testMessage", "Maps do not round trip to same resource ("+Utilities.tail(srcU)+" -> "+Utilities.tail(tgtU)+") - can't test");            
          } else {
            StructureDefinition tsd = context.fetchResource(StructureDefinition.class, srcU);
            if (tsd == null) {
              json.set("testMessage", "Undefined type "+srcU);
            } else {
              testRoundTrips(sd, json, tgtG, srcG, tsd, examples, code);
            }
          }
        } else {
          json.set("testMessage", "Multiple matching maps ("+src.size()+"/"+tgt.size()+") - no tests performed");
        }
      } else {
        json.set("testColor", "#eeeeee");
        json.set("testMessage", "n/a");
      }
    }
    return true;
  }

  private void testRoundTrips(StructureDefinition sd, JsonObject json, ResolvedGroup tgtG, ResolvedGroup srcG, StructureDefinition tsd, NpmPackage examples, String code) throws IOException {
    Stats stats = new Stats();
    for (String s : examples.listResources(tsd.getType())) {
      log("  Test "+examples.id()+"::"+s, false);
      try {
        log("  "+testRoundTrip(json, sd, tsd, tgtG, srcG, stats, examples.load("package", s), code)+"%");
      } catch (Exception e) {
        log("error: "+e.getMessage());
        e.printStackTrace();
        stats.error("Error: "+e.getMessage());
      }
    }
    json.set("totalInstances", stats.totalCount());
    json.set("parsedOk", stats.parseCount());
    json.set("convertToR5OK", stats.forwardCount());
    json.set("testMessage", stats.summary());
    json.set("r5validated", stats.validatedCount());
    json.set("r5InError", stats.errorCount());
    json.remove("r5validatedOK");
    json.set("convertToR4OK", stats.backCount());
    json.set("elementsConverted", stats.elementsCount());
    if (stats.elementsCount() > 0) {
      json.set("elementsLost", (stats.lostCount() * 100) / stats.elementsCount());
    } else {
      json.set("elementsLost", 0);
    }
    if (stats.ok()) {
      json.set("testColor", "#d4ffdf");
    }
  }

  private int testRoundTrip(JsonObject json, StructureDefinition sd, StructureDefinition tsd, ResolvedGroup tgtG, ResolvedGroup srcG, Stats stats, InputStream stream, String code) throws FHIRFormatError, DefinitionException, FHIRException, IOException {
    stats.example();
    Element r4 = new org.hl7.fhir.r5.elementmodel.JsonParser(context).setLogical(tsd).parseSingle(stream, null);
    stats.parsed();
    int elementCountBefore = r4.countDescendents()+1;
    String id = r4.getIdBase();
    json.remove(id);
    checkSave(id+"."+code, "loaded", r4);

    Resource r5 = null;
    try {
      r5 = ResourceFactory.createResource(sd.getType());
      utils.transform(context, r4, tgtG.getTargetMap(), r5);
      stats.forward();
      checkSave(id+"."+code,"converted", r5);
    } catch (Exception e) {
      json.forceObject(id).set("conversion-error", e.getMessage());
      throw e;
    }

    try {
      List r5validationErrors = new ArrayList();
      validator.validate(null, r5validationErrors, r5);
      boolean valid = true;
      for (ValidationMessage vm : r5validationErrors) {
        if (vm.isError()) {
          log.append(vm.summary());
          log.append("\r\n");
          valid = false;
        }
      }
      if (valid) {
        log.append("All OK\r\n");
      }
      log.append("\r\n");
      stats.valid(valid);
    } catch (Exception e) {
      json.forceObject(id).set("validation-error", e.getMessage());
      throw e;
    }
    
    Element rt4 = null;
    try {
      rt4 = Manager.build(context, tsd);
      utils.transform(context, r5, srcG.getTargetMap(), rt4);
      stats.back();
      checkSave(id+"."+code, "returned", r4);
    } catch (Exception e) {
      json.forceObject(id).set("return-error", e.getMessage());
      throw e;
    }   
    int elementCountAfter = rt4.countDescendents()+1;
    stats.elements(elementCountBefore);
    stats.lost(elementCountBefore - elementCountAfter);
    return (elementCountAfter * 100) / elementCountBefore;
  }

  private void checkSave(String id, String state, Element e) throws FHIRException, FileNotFoundException, IOException {
    if (saveProcess) {
      new org.hl7.fhir.r5.elementmodel.JsonParser(context).compose(e, ManagedFileAccess.outStream(Utilities.path("[tmp]", "r4r5", e.fhirType()+"-"+id+"-"+state+".json")), OutputStyle.PRETTY, id);
    }
  }

  private void checkSave(String id, String state, Resource r) throws FHIRException, FileNotFoundException, IOException {
    if (saveProcess) {
      new org.hl7.fhir.r5.formats.JsonParser().setOutputStyle(OutputStyle.PRETTY).compose(ManagedFileAccess.outStream(Utilities.path("[tmp]", "r4r5", r.fhirType()+"-"+id+"-"+state+".json")), r);
    }
  }
  
  private void log(String msg) {
    log(msg, true);
  }
  private void log(String msg, boolean ln) {
    log.append(msg+"\r\n");
    if (ln) {
      System.out.println(msg);
    } else {
      System.out.print(msg+" ");
    }
  }

  @Override
  public Element fetch(IResourceValidator validator, Object appContext, String url) throws FHIRException, IOException {
    return null;
  }

  @Override
  public boolean resolveURL(IResourceValidator validator, Object appContext, String path, String url, String type, boolean canonical)
      throws IOException, FHIRException {
    return true;
  }

  @Override
  public byte[] fetchRaw(IResourceValidator validator, String url) throws IOException {
    throw new Error("Not done yet");
  }

  @Override
  public IValidatorResourceFetcher setLocale(Locale locale) {
    throw new Error("Not done yet");
  }

  @Override
  public CanonicalResource fetchCanonicalResource(IResourceValidator validator, Object appContext, String url) throws URISyntaxException {
    return null;
  }

  @Override
  public boolean fetchesCanonicalResource(IResourceValidator validator, String url) {
    return false;
  }

  @Override
  public Set fetchCanonicalResourceVersions(IResourceValidator validator, Object appContext, String url) {
    return new HashSet<>();
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy