From 34a3dfb43e8c4eda124554f75d2a8479e847a735 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Thu, 29 Aug 2019 18:11:33 +1000 Subject: [PATCH] work on API comparison --- .../CapabilityStatementUtilities.java | 682 ++++++++++++++++++ .../fhir/r5/conformance/ProfileComparer.java | 228 +++++- .../fhir/r5/conformance/ProfileUtilities.java | 2 +- .../hl7/fhir/utilities/xhtml/XhtmlNode.java | 32 +- .../fhir/r5/validation/ValidationEngine.java | 2 +- .../org/hl7/fhir/r5/validation/Validator.java | 68 +- 6 files changed, 971 insertions(+), 43 deletions(-) create mode 100644 org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/CapabilityStatementUtilities.java diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/CapabilityStatementUtilities.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/CapabilityStatementUtilities.java new file mode 100644 index 000000000..8f6796227 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/CapabilityStatementUtilities.java @@ -0,0 +1,682 @@ +package org.hl7.fhir.r5.conformance; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.net.URLConnection; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.hl7.fhir.exceptions.DefinitionException; +import org.hl7.fhir.exceptions.FHIRFormatError; +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.context.SimpleWorkerContext; +import org.hl7.fhir.r5.model.CapabilityStatement; +import org.hl7.fhir.r5.model.CapabilityStatement.CapabilityStatementRestComponent; +import org.hl7.fhir.r5.model.CapabilityStatement.CapabilityStatementRestResourceComponent; +import org.hl7.fhir.r5.model.CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent; +import org.hl7.fhir.r5.model.CapabilityStatement.CapabilityStatementRestSecurityComponent; +import org.hl7.fhir.r5.model.CapabilityStatement.ResourceInteractionComponent; +import org.hl7.fhir.r5.model.CapabilityStatement.RestfulCapabilityMode; +import org.hl7.fhir.r5.model.CodeableConcept; +import org.hl7.fhir.r5.model.Coding; +import org.hl7.fhir.r5.model.Element; +import org.hl7.fhir.r5.model.StructureDefinition; +import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; +import org.hl7.fhir.utilities.MarkDownProcessor; +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.MarkDownProcessor.Dialect; +import org.hl7.fhir.utilities.TextFile; +import org.hl7.fhir.utilities.validation.ValidationMessage; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; +import org.hl7.fhir.utilities.validation.ValidationMessage.Source; +import org.hl7.fhir.utilities.xhtml.NodeType; +import org.hl7.fhir.utilities.xhtml.XhtmlComposer; +import org.hl7.fhir.utilities.xhtml.XhtmlDocument; +import org.hl7.fhir.utilities.xhtml.XhtmlNode; +import org.hl7.fhir.utilities.xhtml.XhtmlParser; + +public class CapabilityStatementUtilities { + + private IWorkerContext context; + private String selfName; + private String otherName; + private List output; + private XhtmlDocument html; + private MarkDownProcessor markdown = new MarkDownProcessor(Dialect.COMMON_MARK); + private String folder; +// private String css; + + public CapabilityStatementUtilities(SimpleWorkerContext context, String folder) throws IOException { + super(); + this.context = context; + this.folder = folder; + String f = Utilities.path(folder, "comparison.zip"); + download("http://fhir.org/archive/comparison.zip", f); + unzip(f, folder); + } + + /** + * Size of the buffer to read/write data + */ + private static final int BUFFER_SIZE = 4096; + /** + * Extracts a zip file specified by the zipFilePath to a directory specified by + * destDirectory (will be created if does not exists) + * @param zipFilePath + * @param destDirectory + * @throws IOException + */ + public void unzip(String zipFilePath, String destDirectory) throws IOException { + File destDir = new File(destDirectory); + if (!destDir.exists()) { + destDir.mkdir(); + } + ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath)); + ZipEntry entry = zipIn.getNextEntry(); + // iterates over entries in the zip file + while (entry != null) { + String filePath = destDirectory + File.separator + entry.getName(); + if (!entry.isDirectory()) { + // if the entry is a file, extracts it + extractFile(zipIn, filePath); + } else { + // if the entry is a directory, make the directory + File dir = new File(filePath); + dir.mkdir(); + } + zipIn.closeEntry(); + entry = zipIn.getNextEntry(); + } + zipIn.close(); + } + /** + * Extracts a zip entry (file entry) + * @param zipIn + * @param filePath + * @throws IOException + */ + private void extractFile(ZipInputStream zipIn, String filePath) throws IOException { + BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath)); + byte[] bytesIn = new byte[BUFFER_SIZE]; + int read = 0; + while ((read = zipIn.read(bytesIn)) != -1) { + bos.write(bytesIn, 0, read); + } + bos.close(); + } + + public void saveToFile() throws IOException { + String s = new XhtmlComposer(true, true).compose(html); + TextFile.stringToFile(s, Utilities.path(folder, "index.html")); + } + + /** + * Compares 2 capability statements to see if self is a subset of other + * + * e.g. is a system implementing "+otherName+" also an implementation of self? + * + * the output is a series of messages identifying ways in which it is not, + * or warning messages where the algorithm is unable to determine the + * relationship + * + * Note that there are aspects of this question that are not computable. + * the + * + * @param self + * @param other + * @return + * @throws IOException + * @throws FHIRFormatError + * @throws DefinitionException + */ + public List isCompatible(String selfName, String otherName, CapabilityStatement self, CapabilityStatement other) throws DefinitionException, FHIRFormatError, IOException { + this.selfName = selfName; + this.otherName = otherName; + this.output = new ArrayList<>(); + XhtmlNode x = startHtml(); + + information(x, IssueType.INVARIANT, self.getUrl(), "Comparing "+selfName+" to "+otherName+", to see if a server that implements "+otherName+" also implements "+selfName+""); + information(x, IssueType.INVARIANT, self.getUrl(), " "+selfName+": "+self.getUrl()+"|"+self.getVersion()); + information(x, IssueType.INVARIANT, self.getUrl(), " "+otherName+": "+other.getUrl()+"|"+other.getVersion()); + + if (self.getRest().size() != 1 || self.getRestFirstRep().getMode() != RestfulCapabilityMode.SERVER) + fatal(x, IssueType.INVARIANT, self.getUrl()+"#rest", "The CapabilityStatement Comparison tool can only compare CapabilityStatements with a single server component"); + if (other.getRest().size() != 1 || other.getRestFirstRep().getMode() != RestfulCapabilityMode.SERVER) + fatal(x, IssueType.INVARIANT, other.getUrl()+"#rest", "The CapabilityStatement Comparison tool can only compare CapabilityStatements with a single server component"); + + if (self.getRest().size() == 1 && other.getRest().size() == 1) { + XhtmlNode tbl = startTable(x, self, other); + compareRest(tbl, self.getUrl(), self.getRest().get(0), other.getRest().get(0)); + } + if (folder != null) + saveToFile(); + return output; + } + + private void compareRest(XhtmlNode tbl, String path, CapabilityStatementRestComponent self, CapabilityStatementRestComponent other) throws DefinitionException, FHIRFormatError, IOException { + compareSecurity(tbl, path, self, other); + + // check resources + List ol = new ArrayList<>(); + List olr = new ArrayList<>(); + ol.addAll(other.getResource()); + for (CapabilityStatementRestResourceComponent r : self.getResource()) { + CapabilityStatementRestResourceComponent o = null; + for (CapabilityStatementRestResourceComponent t : ol) { + if (t.getType().equals(r.getType())) { + o = t; + break; + } + } + XhtmlNode tr = tbl.tr(); + tr.style("background-color: #dddddd"); + tr.td().b().addText(r.getType()); + tr.td().tx("Present"); + + + if (o == null) { + XhtmlNode p = tr.td().para("Absent"); + XhtmlNode td = tr.td(); + String s = getConfStatus(r); + if (Utilities.existsInList(s, "SHALL", "SHOULD")) { + error(td, IssueType.NOTFOUND, path+".resource.where(type = '"+r.getType()+"')", selfName+" specifies the resource "+r.getType()+" as "+s+" but "+otherName+" does not cover it"); + p.style("background-color: #ffe6e6; border: 1px solid #ff1a1a; margin-width: 10px"); + } + + tr = tbl.tr(); + tr.td().para().tx(XhtmlNode.NBSP+" - Conformance"); + genConf(tr.td().para(), r, null); + tr.td().style("background-color: #eeeeee"); + tr.td(); + + tr = tbl.tr(); + tr.td().para().tx(XhtmlNode.NBSP+" - Profile"); + genProfile(tr.td().para(), r, null); + tr.td().style("background-color: #eeeeee"); + tr.td(); + + tr = tbl.tr(); + tr.td().para().tx(XhtmlNode.NBSP+" - Interactions"); + genInt(tr.td(), r, null, false); + tr.td().style("background-color: #eeeeee"); + tr.td(); + + tr = tbl.tr(); + tr.td().para().tx(XhtmlNode.NBSP+" - Search Parameters"); + genSP(tr.td(), r, null, false); + tr.td().style("background-color: #eeeeee"); + tr.td(); + + } else { + olr.add(o); + tr.td().tx("Present"); + tr.td().nbsp(); + compareResource(path+".resource.where(type = '"+r.getType()+"')", r, o, tbl); + } + } + for (CapabilityStatementRestResourceComponent t : ol) { + XhtmlNode tr = tbl.tr(); + if (!olr.contains(t)) { + tr.td().addText(t.getType()); + XhtmlNode td = tr.td(); + td.style("background-color: #eeeeee").para("Absent"); + td = tr.td(); + genConf(td, t, null); + genProfile(td, t, null); + genInt(td, t, null, false); + genSP(td, t, null, false); + if (isProhibited(t)) { + error(td, IssueType.INVARIANT, path+".resource", selfName+" does not specify the resource "+t.getType()+" but "+otherName+" prohibits it"); + } + } + } + // check interactions + // check search parameters + // check operation + // check compartments + } + + private void genConf(XhtmlNode x, CapabilityStatementRestResourceComponent r, CapabilityStatementRestResourceComponent other) { + String s = getConfStatus(r); + String so = other != null ? getConfStatus(other) : null; + x.add(same(s == null ? "(not specified)" : s, (s == null && so == null) || (s != null && s.equals(so)))); + } + + private void genProfile(XhtmlNode x, CapabilityStatementRestResourceComponent r, CapabilityStatementRestResourceComponent other) { + String s = getProfile(r); + if (s == null) + s = "(not specified)"; + String so = other == null ? null : getProfile(other) == null ? getProfile(other) : "(not specified)"; + x.add(same(s, (s == null && so == null) || (s != null && s.equals(so)))); + } + + private String getProfile(CapabilityStatementRestResourceComponent r) { + if (r.hasSupportedProfile() && r.getSupportedProfile().size() == 1) + return r.getSupportedProfile().get(0).asStringValue(); + return r.getProfile(); + } + + private void genInt(XhtmlNode td, CapabilityStatementRestResourceComponent r, CapabilityStatementRestResourceComponent other, boolean errorIfNoMatch) { + boolean first = true; + for (ResourceInteractionComponent i : r.getInteraction()) { + if (first) first = false; else td.tx(", "); + if (exists(other, i)) { + td.code().span("background-color: #bbff99; border: 1px solid #44cc00; margin-width: 10px", null).tx(i.getCode().toCode()); + } else if (errorIfNoMatch){ + td.code().span("background-color: #ffe6e6; border: 1px solid #ff1a1a; margin-width: 10px", null).tx(i.getCode().toCode()); + } else { + td.code(i.getCode().toCode()); + } + } + } + + private boolean exists(CapabilityStatementRestResourceComponent other, ResourceInteractionComponent i) { + if (other == null) + return false; + for (ResourceInteractionComponent t : other.getInteraction()) + if (t.getCode().equals(i.getCode())) + return true; + return false; + } + + private void genSP(XhtmlNode td, CapabilityStatementRestResourceComponent r, CapabilityStatementRestResourceComponent other, boolean errorIfNoMatch) { + boolean first = true; + for (CapabilityStatementRestResourceSearchParamComponent i : r.getSearchParam()) { + if (first) first = false; else td.tx(", "); + if (exists(other, i)) { + td.code().span("background-color: #bbff99; border: 1px solid #44cc00; margin-width: 10px", null).tx(i.getName()); + } else if (errorIfNoMatch){ + td.code().span("background-color: #ffe6e6; border: 1px solid #ff1a1a; margin-width: 10px", null).tx(i.getName()); + } else { + td.code(i.getName()); + } + } + } + + private boolean exists(CapabilityStatementRestResourceComponent other, CapabilityStatementRestResourceSearchParamComponent i) { + if (other == null) + return false; + for (CapabilityStatementRestResourceSearchParamComponent t : other.getSearchParam()) + if (t.getName().equals(i.getName())) + return true; + return false; + } + + public void compareSecurity(XhtmlNode tbl, String path, CapabilityStatementRestComponent self, CapabilityStatementRestComponent other) { + XhtmlNode tr = tbl.tr(); + tr.td().b().addText("Security"); + tr.td().para(gen(self.getSecurity())); + tr.td().para(gen(other.getSecurity())); + XhtmlNode td = tr.td(); + + if (self.hasSecurity() && !other.hasSecurity()) + error(td, IssueType.CONFLICT, path+".security", selfName+" specifies some security requirements ("+gen(self.getSecurity())+") but "+otherName+" doesn't"); + else if (!self.hasSecurity() && other.hasSecurity()) + error(td, IssueType.CONFLICT, path+".security", selfName+" does not specify security requirements but "+otherName+" does ("+gen(self.getSecurity())+")"); + else if (self.hasSecurity() && other.hasSecurity()) + compareSecurity(td, path+".security", self.getSecurity(), other.getSecurity()); + } + + private void compareResource(String path, CapabilityStatementRestResourceComponent self, CapabilityStatementRestResourceComponent other, XhtmlNode tbl) throws DefinitionException, FHIRFormatError, IOException { + XhtmlNode tr = tbl.tr(); + tr.td().para().tx(XhtmlNode.NBSP+" - Conformance"); + genConf(tr.td(), self, other); + genConf(tr.td(), other, self); + tr.td().nbsp(); + + tr = tbl.tr(); + tr.td().para().tx(XhtmlNode.NBSP+" - Profile"); + genProfile(tr.td(), self, other); + genProfile(tr.td(), other, self); + compareProfiles(tr.td(), path, getProfile(self), getProfile(other), self.getType()); + + // compare the interactions + compareResourceInteractions(path, self, other, tbl); + compareResourceSearchParams(path, self, other, tbl); + // compare the search parameters + // compare the operations + + // compare the profile? + + } + + private void compareProfiles(XhtmlNode td, String path, String urlL, String urlR, String type) throws DefinitionException, FHIRFormatError, IOException { + if (urlL == null) { + urlL = "http://hl7.org/fhir/StructureDefinition/"+type; + } + if (urlR == null) { + urlR = "http://hl7.org/fhir/StructureDefinition/"+type; + } + StructureDefinition sdL = context.fetchResource(StructureDefinition.class, urlL); + StructureDefinition sdR = context.fetchResource(StructureDefinition.class, urlR); + if (sdL == null) + error(td, IssueType.NOTFOUND, path, "Unable to resolve "+urlL); + if (sdR == null) + error(td, IssueType.NOTFOUND, path, "Unable to resolve "+urlR); + + if (sdL != null && sdR != null && sdL != sdR) { + // ok they are different... + if (sdR.getUrl().equals(sdL.getBaseDefinition())) { + information(td, null, path, "The profile specified by "+selfName+" is inherited from the profile specified by "+otherName); + } else if (folder != null) { + try { + ProfileComparer pc = new ProfileComparer(context); + pc.setId("api-ep."+type); + pc.setTitle("Comparison - "+selfName+" vs "+otherName); + pc.setLeftName(selfName+": "+sdL.present()); + pc.setLeftLink(sdL.getUserString("path")); + pc.setRightName(otherName+": "+sdR.present()); + pc.setRightLink(sdR.getUserString("path")); + pc.compareProfiles(sdL, sdR); + pc.generate(folder); + td.ah(pc.getId()+".html").tx("Comparison..."); + td.tx(pc.getErrCount()+" "+Utilities.pluralize("error", pc.getErrCount())); + } catch (Exception e) { + e.printStackTrace(); + error(td, IssueType.EXCEPTION, path, "Error comparing profiles: "+e.getMessage()); + } + } else { + information(td, null, path, "Use the validator to compare the profiles"); + } + } + } + + private void compareResourceInteractions(String path, CapabilityStatementRestResourceComponent self, CapabilityStatementRestResourceComponent other, XhtmlNode tbl) { + XhtmlNode tr = tbl.tr(); + tr.td().para().tx(XhtmlNode.NBSP+" - Interactions"); + genInt(tr.td(), self, other, true); + genInt(tr.td(), other, self, false); + XhtmlNode td = tr.td(); + List ol = new ArrayList<>(); + List olr = new ArrayList<>(); + ol.addAll(other.getInteraction()); + for (ResourceInteractionComponent r : self.getInteraction()) { + ResourceInteractionComponent o = null; + for (ResourceInteractionComponent t : ol) { + if (t.getCode().equals(r.getCode())) { + o = t; + break; + } + } + if (o == null) { + error(td, IssueType.NOTFOUND, path+".interaction.where(code = '"+r.getCode()+"')", selfName+" specifies the interaction "+r.getCode()+" but "+otherName+" does not"); + } else { + olr.add(o); + } + } + for (ResourceInteractionComponent t : ol) { + if (!olr.contains(t) && isProhibited(t)) + error(td, IssueType.CONFLICT, path+".interaction", selfName+" does not specify the interaction "+t.getCode()+" but "+otherName+" prohibits it"); + } + } + + private void compareResourceSearchParams(String path, CapabilityStatementRestResourceComponent self, CapabilityStatementRestResourceComponent other, XhtmlNode tbl) { + XhtmlNode tr = tbl.tr(); + tr.td().para().tx(XhtmlNode.NBSP+" - Search Params"); + genSP(tr.td(), self, other, true); + genSP(tr.td(), other, self, false); + XhtmlNode td = tr.td(); + + List ol = new ArrayList<>(); + List olr = new ArrayList<>(); + ol.addAll(other.getSearchParam()); + for (CapabilityStatementRestResourceSearchParamComponent r : self.getSearchParam()) { + CapabilityStatementRestResourceSearchParamComponent o = null; + for (CapabilityStatementRestResourceSearchParamComponent t : ol) { + if (t.getName().equals(r.getName())) { + o = t; + break; + } + } + if (o == null) { + error(td, IssueType.NOTFOUND, path+".searchParam.where(name = '"+r.getName()+"')", selfName+" specifies the search parameter "+r.getName()+" but "+otherName+" does not"); + } else { + olr.add(o); + } + } + for (CapabilityStatementRestResourceSearchParamComponent t : ol) { + if (!olr.contains(t) && isProhibited(t)) + error(td, IssueType.CONFLICT, path+"", selfName+" does not specify the search parameter "+t.getName()+" but "+otherName+" prohibits it"); + } + + } + + private String getConfStatus(Element t) { + return t.hasExtension("http://hl7.org/fhir/StructureDefinition/capabilitystatement-expectation") ? + t.getExtensionString("http://hl7.org/fhir/StructureDefinition/capabilitystatement-expectation") : null; + } + + private boolean isShouldOrShall(Element t) { + return t.hasExtension("http://hl7.org/fhir/StructureDefinition/capabilitystatement-expectation") && + ("SHALL".equals(t.getExtensionString("http://hl7.org/fhir/StructureDefinition/capabilitystatement-expectation")) || "SHOULD".equals(t.getExtensionString("http://hl7.org/fhir/StructureDefinition/capabilitystatement-expectation"))); + } + + private boolean isProhibited(Element t) { + return t.hasExtension("http://hl7.org/fhir/StructureDefinition/capabilitystatement-expectation") && "SHALL NOT".equals(t.getExtensionString("http://hl7.org/fhir/StructureDefinition/capabilitystatement-expectation")); + } + + private void compareSecurity(XhtmlNode td, String path, CapabilityStatementRestSecurityComponent self, CapabilityStatementRestSecurityComponent other) { + if (self.getCors() && !other.getCors()) + error(td, IssueType.CONFLICT, path+".security.cors", selfName+" specifies CORS but "+otherName+" doesn't"); + else if (!self.getCors() && other.getCors()) + error(td, IssueType.CONFLICT, path+".security.cors", selfName+" does not specify CORS but "+otherName+" does"); + + List ol = new ArrayList<>(); + List olr = new ArrayList<>(); + ol.addAll(other.getService()); + for (CodeableConcept cc : self.getService()) { + CodeableConcept o = null; + for (CodeableConcept t : ol) { + if (isMatch(t, cc)) { + o = t; + break; + } + } + if (o == null) { + error(td, IssueType.CONFLICT, path+".security.cors", selfName+" specifies the security option "+gen(cc)+" but "+otherName+" does not"); + } else { + olr.add(o); + } + } + for (CodeableConcept cc : ol) { + if (!olr.contains(cc)) + error(td, IssueType.CONFLICT, path+".security.cors", selfName+" does not specify the security option "+gen(cc)+" but "+otherName+" does"); + } + } + + private boolean isMatch(CodeableConcept self, CodeableConcept other) { + for (Coding s : self.getCoding()) + for (Coding o : other.getCoding()) + if (isMatch(s, o)) + return true; + return false; + } + + private boolean isMatch(Coding s, Coding o) { + return s.hasCode() && s.getCode().equals(o.getCode()) && s.hasSystem() && s.getSystem().equals(o.getSystem()); + } + + private String gen(CapabilityStatementRestSecurityComponent security) { + CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); + for (CodeableConcept cc : security.getService()) + b.append(gen(cc)); + if (security.getCors()) + b.append("(CORS)"); + if (Utilities.noString(b.toString())) + return "(none specified)"; + return b.toString(); + } + + private String gen(CodeableConcept cc) { + if (cc.hasText()) + return cc.getText(); + if (cc.hasCoding()) + return gen(cc.getCoding().get(0)); + return "??"; + } + + private String gen(Coding coding) { + if (coding.hasDisplay()) + return coding.getDisplay(); + if (coding.hasCode()) + return coding.getCode(); + return "???"; + } + + + private XhtmlNode startHtml() { + html = new XhtmlDocument(); + XhtmlNode doc = html.addTag("html"); + XhtmlNode head = doc.addTag("head"); + head.addTag("title").addText("Comparison of "+selfName+" to "+otherName); + head.addTag("link").setAttribute("rel", "stylesheet").setAttribute("href", "fhir.css"); + XhtmlNode body = doc.addTag("body").style("background-color: white"); + + body.h1().addText("Comparison of "+selfName+" to "+otherName); + return body; + } + + private void addMarkdown(XhtmlNode x, String text) throws FHIRFormatError, IOException, DefinitionException { + if (text != null) { + // 1. custom FHIR extensions + while (text.contains("[[[")) { + String left = text.substring(0, text.indexOf("[[[")); + String link = text.substring(text.indexOf("[[[")+3, text.indexOf("]]]")); + String right = text.substring(text.indexOf("]]]")+3); + String url = link; + String[] parts = link.split("\\#"); + StructureDefinition p = context.fetchResource(StructureDefinition.class, parts[0]); + if (p == null) + p = context.fetchTypeDefinition(parts[0]); + if (p == null) + p = context.fetchResource(StructureDefinition.class, link); + if (p != null) { + url = p.getUserString("path"); + if (url == null) + url = p.getUserString("filename"); + } else + throw new DefinitionException("Unable to resolve markdown link "+link); + + text = left+"["+link+"]("+url+")"+right; + } + + // 2. markdown + String s = markdown.process(Utilities.escapeXml(text), "narrative generator"); + XhtmlParser p = new XhtmlParser(); + XhtmlNode m; + try { + m = p.parse("
"+s+"
", "div"); + } catch (org.hl7.fhir.exceptions.FHIRFormatError e) { + throw new FHIRFormatError(e.getMessage(), e); + } + x.getChildNodes().addAll(m.getChildNodes()); + } + } + + + private XhtmlNode startTable(XhtmlNode x, CapabilityStatement self, CapabilityStatement other) { + XhtmlNode tbl = x.table("grid"); + XhtmlNode tr = tbl.tr(); + tr.td().b().nbsp(); + tr.td().b().addText(selfName); + tr.td().b().addText(otherName); + tr.td().b().addText("Comparison"); + return tbl; + } + + private void download(String address, String filename) throws IOException { + URL url = new URL(address); + URLConnection c = url.openConnection(); + InputStream s = c.getInputStream(); + FileOutputStream f = new FileOutputStream(filename); + transfer(s, f, 1024); + f.close(); + } + + + public static void transfer(InputStream in, OutputStream out, int buffer) throws IOException { + byte[] read = new byte[buffer]; // Your buffer size. + while (0 < (buffer = in.read(read))) + out.write(read, 0, buffer); + } + + private void fatal(XhtmlNode x, IssueType type, String path, String message) { + output.add(new ValidationMessage(Source.ProfileComparer, type, path, message, IssueSeverity.FATAL)); + XhtmlNode ul; + if ("ul".equals(x.getName())) { + ul = x; + } else { + ul = null; + for (XhtmlNode c : x.getChildNodes()) { + if ("ul".equals(c.getName())) { + ul = c; + } + } + if (ul == null) { + ul = x.ul(); + } + } + ul.li().b().style("color: maroon").addText(message); + } + + private void error(XhtmlNode x, IssueType type, String path, String message) { + output.add(new ValidationMessage(Source.ProfileComparer, type, path, message, IssueSeverity.ERROR)); + XhtmlNode ul; + if ("ul".equals(x.getName())) { + ul = x; + } else { + ul = null; + for (XhtmlNode c : x.getChildNodes()) { + if ("ul".equals(c.getName())) { + ul = c; + } + } + if (ul == null) { + ul = x.ul(); + } + } + ul.li().b().style("color: maroon").addText(message); + } + + private void information(XhtmlNode x, IssueType type, String path, String message) { + if (type != null) + output.add(new ValidationMessage(Source.ProfileComparer, type, path, message, IssueSeverity.INFORMATION)); + XhtmlNode ul; + if ("ul".equals(x.getName())) { + ul = x; + } else { + ul = null; + for (XhtmlNode c : x.getChildNodes()) { + if ("ul".equals(c.getName())) { + ul = c; + } + } + if (ul == null) { + ul = x.ul(); + } + } + ul.li().addText(message); + } + + private XhtmlNode same(String text, boolean test) { + XhtmlNode span = new XhtmlNode(NodeType.Element, "span"); + if (test) + span.style("background-color: #bbff99; border: 1px solid #44cc00; margin-width: 10px"); + span.tx(text); + return span; + } + +} diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/ProfileComparer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/ProfileComparer.java index 1fec6b769..c441ba1e8 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/ProfileComparer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/ProfileComparer.java @@ -33,7 +33,10 @@ import java.util.List; import java.util.Map; import org.hl7.fhir.exceptions.DefinitionException; +import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRFormatError; +import org.hl7.fhir.r5.conformance.ProfileComparer.ProfileComparison; +import org.hl7.fhir.r5.conformance.ProfileUtilities.ProfileKnowledgeProvider; import org.hl7.fhir.r5.context.IWorkerContext; import org.hl7.fhir.r5.formats.IParser; import org.hl7.fhir.r5.model.Base; @@ -58,12 +61,16 @@ import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent; import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent; import org.hl7.fhir.r5.terminologies.ValueSetExpander.ValueSetExpansionOutcome; import org.hl7.fhir.r5.utils.DefinitionNavigator; +import org.hl7.fhir.r5.utils.NarrativeGenerator; import org.hl7.fhir.r5.utils.ToolingExtensions; import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; +import org.hl7.fhir.utilities.Logger.LogMessageType; import org.hl7.fhir.utilities.TextFile; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.validation.ValidationMessage; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; import org.hl7.fhir.utilities.validation.ValidationMessage.Source; +import org.hl7.fhir.utilities.xhtml.XhtmlComposer; /** * A engine that generates difference analysis between two sets of structure @@ -83,7 +90,7 @@ import org.hl7.fhir.utilities.validation.ValidationMessage.Source; * @author Grahame Grieve * */ -public class ProfileComparer { +public class ProfileComparer implements ProfileKnowledgeProvider { private IWorkerContext context; @@ -223,28 +230,28 @@ public class ProfileComparer { return jp.composeString(val, "value"); } - public String getErrorCount() { + public int getErrorCount() { int c = 0; for (ValidationMessage vm : messages) if (vm.getLevel() == ValidationMessage.IssueSeverity.ERROR) c++; - return Integer.toString(c); + return c; } - public String getWarningCount() { + public int getWarningCount() { int c = 0; for (ValidationMessage vm : messages) if (vm.getLevel() == ValidationMessage.IssueSeverity.WARNING) c++; - return Integer.toString(c); + return c; } - public String getHintCount() { + public int getHintCount() { int c = 0; for (ValidationMessage vm : messages) if (vm.getLevel() == ValidationMessage.IssueSeverity.INFORMATION) c++; - return Integer.toString(c); + return c; } } @@ -644,7 +651,7 @@ public class ProfileComparer { re = context.expandVS(rvs, true, false); if (!closed(le.getValueset()) || !closed(re.getValueset())) throw new DefinitionException("unclosed value sets are not handled yet"); - cvs = intersectByExpansion(lvs, rvs); + cvs = intersectByExpansion(path, le.getValueset(), re.getValueset()); if (!cvs.getCompose().hasInclude()) { outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "The value sets "+lvs.getUrl()+" and "+rvs.getUrl()+" do not intersect", ValidationMessage.IssueSeverity.ERROR)); status(subset, ProfileUtilities.STATUS_ERROR); @@ -688,6 +695,7 @@ public class ProfileComparer { private ValueSet unite(ElementDefinition ed, ProfileComparison outcome, String path, ValueSet lvs, ValueSet rvs) { ValueSet vs = new ValueSet(); + vs.setName(path); if (lvs.hasCompose()) { for (ConceptSetComponent inc : lvs.getCompose().getInclude()) vs.getCompose().getInclude().add(inc); @@ -712,7 +720,7 @@ public class ProfileComparer { for (ConceptSetComponent dst : include) { if (Base.compareDeep(dst, inc, false)) return true; // they're actually the same - if (dst.getSystem().equals(inc.getSystem())) { + if (dst.hasSystem() && dst.getSystem().equals(inc.getSystem())) { if (inc.hasFilter() || dst.hasFilter()) { return false; // just add the new one as a a parallel } else if (inc.hasConcept() && dst.hasConcept()) { @@ -749,9 +757,10 @@ public class ProfileComparer { return null; } - private ValueSet intersectByExpansion(ValueSet lvs, ValueSet rvs) { + private ValueSet intersectByExpansion(String path, ValueSet lvs, ValueSet rvs) { // this is pretty straight forward - we intersect the lists, and build a compose out of the intersection ValueSet vs = new ValueSet(); + vs.setName(path); vs.setStatus(PublicationStatus.DRAFT); Map left = new HashMap(); @@ -840,8 +849,8 @@ public class ProfileComparer { tfound = true; c.setTargetProfile(r.getTargetProfile()); } else { - StructureDefinition sdl = resolveProfile(ed, outcome, path, l.getProfile().get(0).getValue(), outcome.leftName()); - StructureDefinition sdr = resolveProfile(ed, outcome, path, r.getProfile().get(0).getValue(), outcome.rightName()); + StructureDefinition sdl = resolveProfile(ed, outcome, path, l.getTargetProfile().get(0).getValue(), outcome.leftName()); + StructureDefinition sdr = resolveProfile(ed, outcome, path, r.getTargetProfile().get(0).getValue(), outcome.rightName()); if (sdl != null && sdr != null) { if (sdl == sdr) { tfound = true; @@ -1187,9 +1196,43 @@ public class ProfileComparer { } private String genPCLink(String leftName, String leftLink) { - return ""+Utilities.escapeXml(leftName)+""; + if (leftLink == null) + return leftName; + else + return ""+Utilities.escapeXml(leftName)+""; } + private String genValueSets(String base) throws IOException { + StringBuilder b = new StringBuilder(); + b.append("
    \r\n"); + for (ValueSet vs : getValuesets()) { + b.append("
  • "); + b.append(" "+Utilities.escapeXml(vs.present())+""); + b.append("
  • \r\n"); + genValueSetFile(base+"-"+vs.getId()+".html", vs); + } + b.append("
\r\n"); + return b.toString(); + } + + private void genValueSetFile(String filename, ValueSet vs) throws IOException { + NarrativeGenerator gen = new NarrativeGenerator("", "http://hl7.org/fhir", context); + gen.generate(null, vs, false); + String s = new XhtmlComposer(XhtmlComposer.HTML).compose(vs.getText().getDiv()); + StringBuilder b = new StringBuilder(); + b.append(""); + b.append(""); + b.append(""+vs.present()+""); + b.append("\r\n"); + b.append(""); + b.append(""); + b.append("

"+vs.present()+"

"); + b.append(s); + b.append(""); + b.append(""); + TextFile.stringToFile(b.toString(), filename); + } + private String genPCTable() { StringBuilder b = new StringBuilder(); @@ -1219,7 +1262,53 @@ public class ProfileComparer { } + private String genCmpMessages(ProfileComparison cmp) { + StringBuilder b = new StringBuilder(); + b.append("\r\n"); + b.append("\r\n"); + b.append("\r\n"); + boolean found = false; + for (ValidationMessage vm : cmp.getMessages()) + if (vm.getLevel() == IssueSeverity.ERROR || vm.getLevel() == IssueSeverity.FATAL) { + found = true; + b.append("\r\n"); + } + if (!found) + b.append("\r\n"); + + boolean first = true; + for (ValidationMessage vm : cmp.getMessages()) + if (vm.getLevel() == IssueSeverity.WARNING) { + if (first) { + first = false; + b.append("\r\n"); + } + b.append("\r\n"); + } + first = true; + for (ValidationMessage vm : cmp.getMessages()) + if (vm.getLevel() == IssueSeverity.INFORMATION) { + if (first) { + b.append("\r\n"); + first = false; + } + b.append("\r\n"); + } + b.append("
PathMessage
Errors Detected
"+vm.getLocation()+""+vm.getHtml()+(vm.getLevel() == IssueSeverity.FATAL ? "(This error terminated the comparison process)" : "")+"
(None)
Warnings about the comparison
"+vm.getLocation()+""+vm.getHtml()+"
Notes about differences (e.g. definitions)
"+vm.getLocation()+""+vm.getHtml()+"
\r\n"); + return b.toString(); + } + + private String genCompModel(StructureDefinition sd, String name, String base, String prefix, String dest) throws FHIRException, IOException { + if (sd == null) + return "

No "+name+" could be generated

\r\n"; + return new XhtmlComposer(XhtmlComposer.HTML).compose(new ProfileUtilities(context, null, this).generateTable("??", sd, false, dest, false, base, true, prefix, prefix, false, false, null)); + } + + public String generate(String dest) throws IOException { + for (ValueSet vs : valuesets) { + vs.setUserData("path", dest+"/"+getId()+"-vs-"+vs.getId()+".html"); + } // ok, all compared; now produce the output // first page we produce is simply the index Map vars = new HashMap(); @@ -1227,21 +1316,20 @@ public class ProfileComparer { vars.put("left", genPCLink(getLeftName(), getLeftLink())); vars.put("right", genPCLink(getRightName(), getRightLink())); vars.put("table", genPCTable()); + vars.put("valuesets", genValueSets(dest+"/"+getId()+"-vs")); producePage(summaryTemplate(), Utilities.path(dest, getId()+".html"), vars); -// page.log(" ... generate", LogMessageType.Process); -// String src = TextFile.fileToString(page.getFolders().srcDir + "template-comparison-set.html"); -// src = page.processPageIncludes(n+".html", src, "?type", null, "??path", null, null, "Comparison", pc, null, null, page.getDefinitions().getWorkgroups().get("fhir")); -// TextFile.stringToFile(src, Utilities.path(page.getFolders().dstDir, n+".html")); -// cachePage(n + ".html", src, "Comparison "+pc.getTitle(), false); -// -// // then we produce a comparison page for each pair -// for (ProfileComparison cmp : pc.getComparisons()) { -// src = TextFile.fileToString(page.getFolders().srcDir + "template-comparison.html"); -// src = page.processPageIncludes(n+"."+cmp.getId()+".html", src, "?type", null, "??path", null, null, "Comparison", cmp, null, null, page.getDefinitions().getWorkgroups().get("fhir")); -// TextFile.stringToFile(src, Utilities.path(page.getFolders().dstDir, n+"."+cmp.getId()+".html")); -// cachePage(n +"."+cmp.getId()+".html", src, "Comparison "+pc.getTitle(), false); -// } + // then we produce a comparison page for each pair + for (ProfileComparison cmp : getComparisons()) { + vars.clear(); + vars.put("title", getTitle()); + vars.put("left", genPCLink(getLeftName(), getLeftLink())); + vars.put("right", genPCLink(getRightName(), getRightLink())); + vars.put("messages", genCmpMessages(cmp)); + vars.put("subset", genCompModel(cmp.getSubset(), "intersection", getId()+"."+cmp.getId(), "", dest)); + vars.put("superset", genCompModel(cmp.getSuperset(), "union", getId()+"."+cmp.getId(), "", dest)); + producePage(singleTemplate(), Utilities.path(dest, getId()+"."+cmp.getId()+".html"), vars); + } // // and also individual pages for each pair outcome // // then we produce value set pages for each value set // @@ -1249,6 +1337,7 @@ public class ProfileComparer { return Utilities.path(dest, getId()+".html"); } + private void producePage(String src, String path, Map vars) throws IOException { while (src.contains("[%")) { @@ -1264,7 +1353,13 @@ public class ProfileComparer { } private String summaryTemplate() throws IOException { - return cachedFetch("04a9d69a-47f2-4250-8645-bf5d880a8eaa-1.fhir-template", "http://build.fhir.org/template-comparison-set.html.template"); + return TextFile.fileToString("C:\\work\\org.hl7.fhir\\build\\source\\template-comparison-set.html"); + // return cachedFetch("04a9d69a-47f2-4250-8645-bf5d880a8eaa-1.fhir-template", "http://build.fhir.org/template-comparison-set.html.template"); + } + + private String singleTemplate() throws IOException { + return TextFile.fileToString("C:\\work\\org.hl7.fhir\\build\\source\\template-comparison.html"); + // return cachedFetch("04a9d69a-47f2-4250-8645-bf5d880a8eaa-1.fhir-template", "http://build.fhir.org/template-comparison-set.html.template"); } private String cachedFetch(String id, String source) throws IOException { @@ -1280,6 +1375,85 @@ public class ProfileComparer { return result; } + @Override + public boolean isDatatype(String typeSimple) { + throw new Error("Not done yet"); + } + + @Override + public boolean isResource(String typeSimple) { + throw new Error("Not done yet"); + } + + @Override + public boolean hasLinkFor(String name) { + StructureDefinition sd = context.fetchTypeDefinition(name); + return sd != null && sd.hasUserData("path"); + } + + @Override + public String getLinkFor(String corePath, String name) { + StructureDefinition sd = context.fetchTypeDefinition(name); + return sd == null ? null : sd.getUserString("path"); + } + + @Override + public BindingResolution resolveBinding(StructureDefinition def, ElementDefinitionBindingComponent binding, String path) throws FHIRException { + return resolveBindingInt(def, binding.getValueSet(), binding.getDescription()); + } + + @Override + public BindingResolution resolveBinding(StructureDefinition def, String url, String path) throws FHIRException { + return resolveBindingInt(def, url, url); + } + + public BindingResolution resolveBindingInt(StructureDefinition def, String url, String desc) throws FHIRException { + ValueSet vs = null; + if (url.startsWith("#")) { + for (ValueSet t : valuesets) { + if (("#"+t.getId()).equals(url)) { + vs = t; + break; + } + } + } + if (vs == null) + context.fetchResource(ValueSet.class, url); + BindingResolution br = new BindingResolution(); + if (vs != null) { + br.display = vs.present(); + br.url = vs.getUserString("path"); + } else { + br.display = desc; + } + return br; + } + + @Override + public String getLinkForProfile(StructureDefinition profile, String url) { + StructureDefinition sd = context.fetchResource(StructureDefinition.class, url); + return sd == null ? null : sd.getUserString("path"); + } + + @Override + public boolean prependLinks() { + return false; + } + + @Override + public String getLinkForUrl(String corePath, String s) { + throw new Error("Not done yet"); + } + + public int getErrCount() { + int res = 0; + for (ProfileComparison pc : comparisons) { + res = res + pc.getErrorCount(); + } + return res; + + } + diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/ProfileUtilities.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/ProfileUtilities.java index a683e2653..03755d77e 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/ProfileUtilities.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/ProfileUtilities.java @@ -2337,7 +2337,7 @@ public class ProfileUtilities extends TranslatingUtilities { if (sd != null) { String disp = sd.hasTitle() ? sd.getTitle() : sd.getName(); String ref = pkp.getLinkForProfile(null, sd.getUrl()); - if (ref.contains("|")) + if (ref != null && ref.contains("|")) ref = ref.substring(0, ref.indexOf("|")); c.addPiece(checkForNoChange(t, gen.new Piece(ref, disp, null))); } else diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlNode.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlNode.java index da2d21864..c8dfd6105 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlNode.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlNode.java @@ -558,8 +558,12 @@ public class XhtmlNode implements IBaseXhtml { } - public void code(String text) { - addTag("code").tx(text); + public XhtmlNode code(String text) { + return addTag("code").tx(text); + } + + public XhtmlNode code() { + return addTag("code"); } @@ -613,4 +617,28 @@ public class XhtmlNode implements IBaseXhtml { return notPretty; } + + public XhtmlNode style(String style) { + setAttribute("style", style); + return this; + } + + + public XhtmlNode nbsp() { + return addText(NBSP); + } + + + public XhtmlNode para(String text) { + XhtmlNode p = para(); + p.addText(text); + return p; + + } + + public XhtmlNode add(XhtmlNode n) { + getChildNodes().add(n); + return this; + } + } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/ValidationEngine.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/ValidationEngine.java index f81b19fbe..038115f2e 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/ValidationEngine.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/ValidationEngine.java @@ -534,7 +534,7 @@ public class ValidationEngine { public Map loadPackage(NpmPackage pi) throws IOException { Map res = new HashMap(); for (String s : pi.list("package")) { - if (s.startsWith("CodeSystem-") || s.startsWith("ConceptMap-") || s.startsWith("ImplementationGuide-") || s.startsWith("StructureMap-") || s.startsWith("ValueSet-") || s.startsWith("StructureDefinition-")) + if (s.startsWith("CodeSystem-") || s.startsWith("ConceptMap-") || s.startsWith("ImplementationGuide-") || s.startsWith("CapabilityStatement-") || s.startsWith("StructureMap-") || s.startsWith("ValueSet-") || s.startsWith("StructureDefinition-")) res.put(s, TextFile.streamToBytes(pi.load("package", s))); } String ini = "[FHIR]\r\nversion="+pi.fhirVersion()+"\r\n"; diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/Validator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/Validator.java index 39da1fe72..ecaf0b545 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/Validator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/Validator.java @@ -61,6 +61,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementKind; +import org.hl7.fhir.r5.conformance.CapabilityStatementUtilities; import org.hl7.fhir.r5.conformance.ProfileComparer; import org.hl7.fhir.r5.formats.IParser; import org.hl7.fhir.r5.formats.IParser.OutputStyle; @@ -68,18 +70,23 @@ import org.hl7.fhir.r5.formats.XmlParser; import org.hl7.fhir.r5.formats.JsonParser; import org.hl7.fhir.r5.model.Bundle; import org.hl7.fhir.r5.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r5.model.CapabilityStatement; import org.hl7.fhir.r5.model.Constants; import org.hl7.fhir.r5.model.DomainResource; import org.hl7.fhir.r5.model.FhirPublication; import org.hl7.fhir.r5.model.ImplementationGuide; import org.hl7.fhir.r5.model.IntegerType; +import org.hl7.fhir.r5.model.MetadataResource; import org.hl7.fhir.r5.model.OperationOutcome; import org.hl7.fhir.r5.model.OperationOutcome.OperationOutcomeIssueComponent; import org.hl7.fhir.r5.model.Resource; import org.hl7.fhir.r5.model.StructureDefinition; import org.hl7.fhir.r5.utils.ToolingExtensions; +import org.hl7.fhir.utilities.TextFile; +import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.VersionUtil; import org.hl7.fhir.utilities.cache.ToolsVersion; +import org.hl7.fhir.utilities.validation.ValidationMessage; import org.hl7.fhir.utilities.xhtml.XhtmlComposer; /** @@ -218,6 +225,7 @@ public class Validator { else if ("1.0".equals(v)) v = "1.0.2"; else if ("1.4".equals(v)) v = "1.4.0"; else if ("3.0".equals(v)) v = "3.0.1"; + else if ("4.0".equals(v)) v = "4.0.0"; else if (v.startsWith(Constants.VERSION)) v = Constants.VERSION; String definitions = "hl7.fhir.core#"+v; System.out.println("Loading (v = "+v+", tx server http://tx.fhir.org)"); @@ -236,24 +244,53 @@ public class Validator { } } // ok now set up the comparison - ProfileComparer pc = new ProfileComparer(validator.getContext()); String left = getParam(args, "-left"); String right = getParam(args, "-right"); - StructureDefinition sdL = validator.getContext().fetchResource(StructureDefinition.class, left); - if (sdL == null) { - System.out.println("Unable to locate left profile " +left); - } else { - StructureDefinition sdR = validator.getContext().fetchResource(StructureDefinition.class, right); - if (sdR == null) { - System.out.println("Unable to locate right profile " +right); - } else { - System.out.println("Comparing "+left+" to "+right); + Resource resLeft = validator.getContext().fetchResource(Resource.class, left); + Resource resRight = validator.getContext().fetchResource(Resource.class, right); + if (resLeft == null) { + System.out.println("Unable to locate left resource " +left); + } + if (resRight == null) { + System.out.println("Unable to locate right resource " +right); + } + if (resLeft != null && resRight != null) { + if (resLeft instanceof StructureDefinition && resRight instanceof StructureDefinition) { + System.out.println("Comparing StructureDefinitions "+left+" to "+right); + ProfileComparer pc = new ProfileComparer(validator.getContext()); + StructureDefinition sdL = (StructureDefinition) resLeft; + StructureDefinition sdR = (StructureDefinition) resRight; pc.compareProfiles(sdL, sdR); - System.out.println("Generating output..."); + System.out.println("Generating output to "+dest+"..."); File htmlFile = new File(pc.generate(dest)); Desktop.getDesktop().browse(htmlFile.toURI()); System.out.println("Done"); - } + } else if (resLeft instanceof CapabilityStatement && resRight instanceof CapabilityStatement) { + String nameLeft = chooseName(args, "leftName", (MetadataResource) resLeft); + String nameRight = chooseName(args, "rightName", (MetadataResource) resRight); + System.out.println("Comparing CapabilityStatements "+left+" to "+right); + CapabilityStatementUtilities pc = new CapabilityStatementUtilities(validator.getContext(), dest); + CapabilityStatement capL = (CapabilityStatement) resLeft; + CapabilityStatement capR = (CapabilityStatement) resRight; + List msgs = pc.isCompatible(nameLeft, nameRight, capL, capR); + + String destTxt = Utilities.path(dest, "output.txt"); + System.out.println("Generating output to "+destTxt+"..."); + StringBuilder b = new StringBuilder(); + for (ValidationMessage msg : msgs) { + b.append(msg.summary()); + b.append("\r\n"); + } + TextFile.stringToFile(b.toString(), destTxt); + File txtFile = new File(destTxt); + Desktop.getDesktop().browse(txtFile.toURI()); + + String destHtml = Utilities.path(dest, "index.html"); + File htmlFile = new File(destHtml); + Desktop.getDesktop().browse(htmlFile.toURI()); + System.out.println("Done"); + } else + System.out.println("Unable to compare left resource " +left+" ("+resLeft.fhirType()+") with right resource "+right+" ("+resRight.fhirType()+")"); } } } else { @@ -476,6 +513,13 @@ public class Validator { } } + private static String chooseName(String[] args, String name, MetadataResource mr) { + String s = getParam(args, "-"+name); + if (Utilities.noString(s)) + s = mr.present(); + return s; + } + private static String getGitBuild() { return "??"; }