work on API comparison

This commit is contained in:
Grahame Grieve 2019-08-29 18:11:33 +10:00
parent 239084ed0f
commit 34a3dfb43e
6 changed files with 971 additions and 43 deletions

View File

@ -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<ValidationMessage> 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<ValidationMessage> 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<CapabilityStatementRestResourceComponent> ol = new ArrayList<>();
List<CapabilityStatementRestResourceComponent> 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<ResourceInteractionComponent> ol = new ArrayList<>();
List<ResourceInteractionComponent> 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<CapabilityStatementRestResourceSearchParamComponent> ol = new ArrayList<>();
List<CapabilityStatementRestResourceSearchParamComponent> 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<CodeableConcept> ol = new ArrayList<>();
List<CodeableConcept> 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("<div>"+s+"</div>", "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;
}
}

View File

@ -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<String, ValueSetExpansionContainsComponent> left = new HashMap<String, ValueSetExpansionContainsComponent>();
@ -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,7 +1196,41 @@ public class ProfileComparer {
}
private String genPCLink(String leftName, String leftLink) {
return "<a href=\""+leftLink+"\">"+Utilities.escapeXml(leftName)+"</a>";
if (leftLink == null)
return leftName;
else
return "<a href=\""+leftLink+"\">"+Utilities.escapeXml(leftName)+"</a>";
}
private String genValueSets(String base) throws IOException {
StringBuilder b = new StringBuilder();
b.append("<ul>\r\n");
for (ValueSet vs : getValuesets()) {
b.append("<li>");
b.append(" <td><a href=\""+base+"-"+vs.getId()+".html\">"+Utilities.escapeXml(vs.present())+"</a></td>");
b.append("</li>\r\n");
genValueSetFile(base+"-"+vs.getId()+".html", vs);
}
b.append("</ul>\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("<html>");
b.append("<head>");
b.append("<title>"+vs.present()+"</title>");
b.append("<link rel=\"stylesheet\" href=\"fhir.css\"/>\r\n");
b.append("</head>");
b.append("<body>");
b.append("<h2>"+vs.present()+"</h2>");
b.append(s);
b.append("</body>");
b.append("</html>");
TextFile.stringToFile(b.toString(), filename);
}
private String genPCTable() {
@ -1219,7 +1262,53 @@ public class ProfileComparer {
}
private String genCmpMessages(ProfileComparison cmp) {
StringBuilder b = new StringBuilder();
b.append("<table class=\"grid\">\r\n");
b.append("<tr><td><b>Path</b></td><td><b>Message</b></td></tr>\r\n");
b.append("<tr><td colspan=\"2\" style=\"background: #eeeeee\">Errors Detected</td></tr>\r\n");
boolean found = false;
for (ValidationMessage vm : cmp.getMessages())
if (vm.getLevel() == IssueSeverity.ERROR || vm.getLevel() == IssueSeverity.FATAL) {
found = true;
b.append("<tr><td>"+vm.getLocation()+"</td><td>"+vm.getHtml()+(vm.getLevel() == IssueSeverity.FATAL ? "(<span style=\"color: maroon\">This error terminated the comparison process</span>)" : "")+"</td></tr>\r\n");
}
if (!found)
b.append("<tr><td colspan=\"2\">(None)</td></tr>\r\n");
boolean first = true;
for (ValidationMessage vm : cmp.getMessages())
if (vm.getLevel() == IssueSeverity.WARNING) {
if (first) {
first = false;
b.append("<tr><td colspan=\"2\" style=\"background: #eeeeee\">Warnings about the comparison</td></tr>\r\n");
}
b.append("<tr><td>"+vm.getLocation()+"</td><td>"+vm.getHtml()+"</td></tr>\r\n");
}
first = true;
for (ValidationMessage vm : cmp.getMessages())
if (vm.getLevel() == IssueSeverity.INFORMATION) {
if (first) {
b.append("<tr><td colspan=\"2\" style=\"background: #eeeeee\">Notes about differences (e.g. definitions)</td></tr>\r\n");
first = false;
}
b.append("<tr><td>"+vm.getLocation()+"</td><td>"+vm.getHtml()+"</td></tr>\r\n");
}
b.append("</table>\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 "<p style=\"color: maroon\">No "+name+" could be generated</p>\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<String, String> vars = new HashMap<String, String>();
@ -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<String, String> 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;
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -534,7 +534,7 @@ public class ValidationEngine {
public Map<String, byte[]> loadPackage(NpmPackage pi) throws IOException {
Map<String, byte[]> res = new HashMap<String, byte[]>();
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";

View File

@ -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<ValidationMessage> 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 "??";
}