From c5255a0f80c5ed683540d5809baa2341e6715b67 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Fri, 30 Oct 2020 00:17:24 +1100 Subject: [PATCH] Gg v5118 (#368) * fix issue validating # references * Mark it has an error if a JSON Array is empty * Don't make wrong error reports for profiling resources in bundles * * Render binding description in profile tables if it doesn't contain paragraphs * fix bug with wrong value for contentReference in derived profiles (profiles do not and cannot change the value) * fix bug with wrong value for contentReference in derived profiles (profiles do not and cannot change the value) (missed testing change) * * fix bug not recognising some content as xml or json * improved markdown support in table generator * * fix bug checking unfixed values for HumanName patterns * fix bug checking patterns (missed in some circumstances) * fix bug checking type of resources in bundles * improve messages around cardinality errors in profiles * add parameter -html-output for enhanced presentation of slicing information (issue #283) --- RELEASE_NOTES.md | 12 ++ .../fhir/r5/conformance/ProfileUtilities.java | 55 ++++-- .../hl7/fhir/r5/elementmodel/JsonParser.java | 3 + .../r5/utils/OperationOutcomeUtilities.java | 1 + .../fhir/r5/test/ProfileUtilitiesTests.java | 2 + .../hl7/fhir/utilities/MarkDownProcessor.java | 5 + .../fhir/utilities/i18n/I18nConstants.java | 1 + .../xhtml/HierarchicalTableGenerator.java | 14 ++ .../src/main/resources/Messages.properties | 10 +- .../hl7/fhir/validation/ValidationEngine.java | 134 ++++++++++++- .../fhir/validation/cli/model/CliContext.java | 17 +- .../cli/services/HTMLOutputGenerator.java | 181 ++++++++++++++++++ .../cli/services/ValidationService.java | 16 +- .../hl7/fhir/validation/cli/utils/Params.java | 6 + .../instance/InstanceValidator.java | 126 ++++++++---- .../type/StructureDefinitionValidator.java | 16 +- .../validation/instance/utils/NodeStack.java | 8 + .../tests/ValidationEngineTests.java | 2 +- pom.xml | 2 +- 19 files changed, 541 insertions(+), 70 deletions(-) create mode 100644 org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/HTMLOutputGenerator.java diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index e69de29bb..3b0d388be 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -0,0 +1,12 @@ +Validator Changes: +* Mark it has an error if a JSON Array is empty +* Don't make wrong error reports for profiling resources in bundles +* fix bug checking unfixed values for HumanName patterns +* fix bug checking patterns (missed in some circumstances) +* fix bug checking type of resources in bundles +* improve messages around cardinality errors in profiles +* add parameter -html-output for enhanced presentation of slicing information + +Other code changes: +* Render binding description in profile tables if it doesn't contain paragraphs +* fix bug with wrong value for contentReference in derived profiles (profiles do not and cannot change the value) 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 fbb99dd77..7155da406 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 @@ -111,6 +111,7 @@ import org.hl7.fhir.r5.utils.XVerExtensionManager.XVerExtensionStatus; import org.hl7.fhir.r5.utils.formats.CSVWriter; import org.hl7.fhir.r5.utils.formats.XLSXWriter; import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; +import org.hl7.fhir.utilities.MarkDownProcessor; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.VersionUtilities; import org.hl7.fhir.utilities.i18n.I18nConstants; @@ -362,9 +363,28 @@ public class ProfileUtilities extends TranslatingUtilities { public List getChildMap(StructureDefinition profile, ElementDefinition element) throws DefinitionException { - if (element.getContentReference()!=null) { - for (ElementDefinition e : profile.getSnapshot().getElement()) { - if (element.getContentReference().equals("#"+e.getId())) + if (element.getContentReference() != null) { + List list = null; + String id = null; + if (element.getContentReference().startsWith("#")) { + // internal reference + id = element.getContentReference().substring(1); + list = profile.getSnapshot().getElement(); + } else if (element.getContentReference().contains("#")) { + // external reference + String ref = element.getContentReference(); + StructureDefinition sd = context.fetchResource(StructureDefinition.class, ref.substring(0, ref.indexOf("#"))); + if (sd == null) { + throw new DefinitionException("unable to process contentReference '"+element.getContentReference()+"' on element '"+element.getId()+"'"); + } + list = sd.getSnapshot().getElement(); + id = ref.substring(ref.indexOf("#")+1); + } else { + throw new DefinitionException("unable to process contentReference '"+element.getContentReference()+"' on element '"+element.getId()+"'"); + } + + for (ElementDefinition e : list) { + if (id.equals(e.getId())) return getChildMap(profile, e); } throw new DefinitionException(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_NAME_REFERENCE__AT_PATH_, element.getContentReference(), element.getPath())); @@ -3155,6 +3175,10 @@ public class ProfileUtilities extends TranslatingUtilities { c.getPieces().add(checkForNoChange(ved.getBinding(), gen.new Piece(corePath+"terminologies.html#"+ved.getBinding().getStrength().toCode(), egt(ved.getBinding().getStrengthElement()), ved.getBinding().getStrength().getDefinition()))); c.getPieces().add(gen.new Piece(null, ")", null)); } + if (ved.getBinding().hasDescription() && MarkDownProcessor.isSimpleMarkdown(ved.getBinding().getDescription())) { + c.getPieces().add(gen.new Piece(null, ": ", null)); + c.addMarkdownNoPara(ved.getBinding().getDescription()); + } } c.addPiece(gen.new Piece("br")).addPiece(gen.new Piece(null, describeExtensionContext(ed), null)); r.getCells().add(c); @@ -4282,6 +4306,10 @@ public class ProfileUtilities extends TranslatingUtilities { c.getPieces().add(checkForNoChange(binding, gen.new Piece(corePath+"extension-elementdefinition-minvalueset.html", translate("sd.table", "Min Binding")+": ", "Min Value Set Extension").addStyle("font-weight:bold"))); c.getPieces().add(checkForNoChange(binding, gen.new Piece(br.url == null ? null : Utilities.isAbsoluteUrl(br.url) || !pkp.prependLinks() ? br.url : corePath+br.url, br.display, null))); } + if (binding.hasDescription() && MarkDownProcessor.isSimpleMarkdown(binding.getDescription())) { + c.getPieces().add(gen.new Piece(null, ": ", null)); + c.addMarkdownNoPara(binding.getDescription()); + } } for (ElementDefinitionConstraintComponent inv : definition.getConstraint()) { if (!inv.hasSource() || profile == null || inv.getSource().equals(profile.getUrl()) || allInvariants) { @@ -4300,7 +4328,6 @@ public class ProfileUtilities extends TranslatingUtilities { // don't show this, this it's important: c.getPieces().add(gen.new Piece(null, "This repeating element has no defined order", null)); } } - if (definition.hasFixed()) { if (!c.getPieces().isEmpty()) { c.addPiece(gen.new Piece("br")); } c.getPieces().add(checkForNoChange(definition.getFixed(), gen.new Piece(null, translate("sd.table", "Fixed Value")+": ", null).addStyle("font-weight:bold"))); @@ -4594,6 +4621,10 @@ public class ProfileUtilities extends TranslatingUtilities { c.getPieces().add(checkForNoChange(binding, gen.new Piece(null, " (", null))); c.getPieces().add(checkForNoChange(binding, gen.new Piece(corePath+"terminologies.html#"+binding.getStrength().toCode(), binding.getStrength().toCode(), binding.getStrength().getDefinition()))); c.getPieces().add(gen.new Piece(null, ")", null)); } + if (binding.hasDescription() && MarkDownProcessor.isSimpleMarkdown(binding.getDescription())) { + c.getPieces().add(gen.new Piece(null, ": ", null)); + c.addMarkdownNoPara(binding.getDescription()); + } } for (ElementDefinitionConstraintComponent inv : definition.getConstraint()) { if (!c.getPieces().isEmpty()) { c.addPiece(gen.new Piece("br")); } @@ -5338,12 +5369,12 @@ public class ProfileUtilities extends TranslatingUtilities { if (!checkFirst || !sd.hasDifferential() || hasMissingIds(sd.getDifferential().getElement())) { if (!sd.hasDifferential()) sd.setDifferential(new StructureDefinitionDifferentialComponent()); - generateIds(sd.getDifferential().getElement(), sd.getUrl()); + generateIds(sd.getDifferential().getElement(), sd.getUrl(), sd.getType()); } if (!checkFirst || !sd.hasSnapshot() || hasMissingIds(sd.getSnapshot().getElement())) { if (!sd.hasSnapshot()) sd.setSnapshot(new StructureDefinitionSnapshotComponent()); - generateIds(sd.getSnapshot().getElement(), sd.getUrl()); + generateIds(sd.getSnapshot().getElement(), sd.getUrl(), sd.getType()); } } @@ -5388,11 +5419,10 @@ public class ProfileUtilities extends TranslatingUtilities { } - private void generateIds(List list, String name) throws DefinitionException { + private void generateIds(List list, String name, String type) throws DefinitionException { if (list.isEmpty()) return; - Map idMap = new HashMap(); Map idList = new HashMap(); SliceList sliceInfo = new SliceList(); @@ -5420,7 +5450,6 @@ public class ProfileUtilities extends TranslatingUtilities { } } String bs = b.toString(); - idMap.put(ed.hasId() ? ed.getId() : ed.getPath(), bs); ed.setId(bs); if (idList.containsKey(bs)) { if (exception || messages == null) { @@ -5429,11 +5458,9 @@ public class ProfileUtilities extends TranslatingUtilities { messages.add(new ValidationMessage(Source.ProfileValidator, ValidationMessage.IssueType.BUSINESSRULE, name+"."+bs, "Duplicate Element id "+bs, ValidationMessage.IssueSeverity.ERROR)); } idList.put(bs, ed.getPath()); - if (ed.hasContentReference()) { - String s = ed.getContentReference().substring(1); - if (idMap.containsKey(s)) - ed.setContentReference("#"+idMap.get(s)); - + if (ed.hasContentReference() && ed.getContentReference().startsWith("#")) { + String s = ed.getContentReference(); + ed.setContentReference("http://hl7.org/fhir/StructureDefinition/"+type+s); } } // second path - fix up any broken path based id references diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/JsonParser.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/JsonParser.java index 82701ee7f..0d29e0cf0 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/JsonParser.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/JsonParser.java @@ -208,6 +208,9 @@ public class JsonParser extends ParserBase { JsonElement e = object.get(name); if (property.isList() && (e instanceof JsonArray)) { JsonArray arr = (JsonArray) e; + if (arr.size() == 0) { + logError(line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.ARRAY_CANNOT_BE_EMPTY), IssueSeverity.ERROR); + } int c = 0; for (JsonElement am : arr) { parseChildComplexInstance(npath+"["+c+"]", object, element, property, name, am); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/OperationOutcomeUtilities.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/OperationOutcomeUtilities.java index 23c2ab5e7..d273381d4 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/OperationOutcomeUtilities.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/OperationOutcomeUtilities.java @@ -46,6 +46,7 @@ public class OperationOutcomeUtilities { public static OperationOutcomeIssueComponent convertToIssue(ValidationMessage message, OperationOutcome op) { OperationOutcomeIssueComponent issue = new OperationOutcome.OperationOutcomeIssueComponent(); + issue.setUserData("source.vm", message); issue.setCode(convert(message.getType())); if (message.getLocation() != null) { diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/ProfileUtilitiesTests.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/ProfileUtilitiesTests.java index 08b9264e2..0c53d558b 100644 --- a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/ProfileUtilitiesTests.java +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/ProfileUtilitiesTests.java @@ -104,6 +104,8 @@ public class ProfileUtilitiesTests { f.setComment(null); b.setDefinition(null); f.setDefinition(null); + b.setContentReference(null); + f.setContentReference(null); ok = Base.compareDeep(b, f, true); } } diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/MarkDownProcessor.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/MarkDownProcessor.java index d85ddc14a..eb6bb9a3e 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/MarkDownProcessor.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/MarkDownProcessor.java @@ -78,5 +78,10 @@ public class MarkDownProcessor { html = html.replace("", "
"); return html; } + + + public static boolean isSimpleMarkdown(String description) { + return !description.contains("\n"); + } } \ No newline at end of file diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java index 23c15cc22..36a9960a6 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java @@ -12,6 +12,7 @@ public class I18nConstants { public static final String ALL_OBSERVATIONS_SHOULD_HAVE_A_PERFORMER = "All_observations_should_have_a_performer"; public static final String ALL_OBSERVATIONS_SHOULD_HAVE_A_SUBJECT = "All_observations_should_have_a_subject"; public static final String ALL_OK = "ALL_OK"; + public static final String ARRAY_CANNOT_BE_EMPTY = "ARRAY_CANNOT_BE_EMPTY"; public static final String ATTEMPT_TO_A_SLICE_AN_ELEMENT_THAT_DOES_NOT_REPEAT__FROM__IN_ = "Attempt_to_a_slice_an_element_that_does_not_repeat__from__in_"; public static final String ATTEMPT_TO_REPLACE_ELEMENT_NAME_FOR_A_NONCHOICE_TYPE = "Attempt_to_replace_element_name_for_a_nonchoice_type"; public static final String ATTEMPT_TO_USE_A_SNAPSHOT_ON_PROFILE__AS__BEFORE_IT_IS_GENERATED = "Attempt_to_use_a_snapshot_on_profile__as__before_it_is_generated"; diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/HierarchicalTableGenerator.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/HierarchicalTableGenerator.java index 8edbee65b..94e337d33 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/HierarchicalTableGenerator.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/HierarchicalTableGenerator.java @@ -240,6 +240,7 @@ public class HierarchicalTableGenerator extends TranslatingUtilities { pieces.add(piece); return this; } + public Cell addMarkdown(String md) { try { Parser parser = Parser.builder().build(); @@ -253,6 +254,19 @@ public class HierarchicalTableGenerator extends TranslatingUtilities { return this; } + public Cell addMarkdownNoPara(String md) { + try { + Parser parser = Parser.builder().build(); + Node document = parser.parse(md); + HtmlRenderer renderer = HtmlRenderer.builder().escapeHtml(true).build(); + String html = renderer.render(document); + pieces.addAll(htmlToParagraphPieces(html)); + } catch (Exception e) { + e.printStackTrace(); + } + return this; + } + private List htmlToParagraphPieces(String html) { List myPieces = new ArrayList(); try { diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index 38e7af318..038b16b7c 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -209,11 +209,12 @@ Validation_BUNDLE_Message = The first entry in a message must be a MessageHeader Validation_VAL_Content_Unknown = Unrecognised Content {0} Validation_VAL_NoType = Unknown type {0} Validation_VAL_Profile_MatchMultiple = Profile {0}, Element matches more than one slice - {1}, {2} -Validation_VAL_Profile_Maximum = {0}: max allowed = {1}, but found {2} -Validation_VAL_Profile_Minimum = {0}: minimum required = {1}, but only found {2} +// for the next 4 messages, the available parameters are: 0: profile url, 1: ed.path, 2: ed.id, 3: ed.sliceName, 4: ed.label, 5: element.path, 6: ed.min and optionally 7: actual count +Validation_VAL_Profile_Maximum = {2}: max allowed = {6}, but found {7} (from {0}) +Validation_VAL_Profile_Minimum = {2}: minimum required = {6}, but only found {7} (from {0}) +Validation_VAL_Profile_NoCheckMax = {2}: Unable to check max allowed ({1}) due to lack of slicing validation (from {0}) +Validation_VAL_Profile_NoCheckMin = {2}: Unable to check minimum required ({1}) due to lack of slicing validation (from {0}) Validation_VAL_Profile_MultipleMatches = Found multiple matching profiles among choices: {0} -Validation_VAL_Profile_NoCheckMax = {0}: Unable to check max allowed ({1}) due to lack of slicing validation -Validation_VAL_Profile_NoCheckMin = {0}'': Unable to check minimum required ({1}) due to lack of slicing validation Validation_VAL_Profile_NoDefinition = No definition found for resource type ''{0}'' Validation_VAL_Profile_NoMatch = Unable to find matching profile among choices: {0} Validation_VAL_Profile_NoSnapshot = StructureDefinition has no snapshot - validation is against the snapshot, so it must be provided @@ -617,4 +618,5 @@ SD_ED_TYPE_PROFILE_NOTYPE = Found profile {0}, but unable to determine the type SD_ED_TYPE_PROFILE_WRONG = Profile {0} is for type {1}, but this element has type {2} TERMINOLOGY_TX_NOSVC_BOUND_REQ = Could not confirm that the codes provided are from the required value set {0} because there is no terminology service TERMINOLOGY_TX_NOSVC_BOUND_EXT = Could not confirm that the codes provided are from the extensible value set {0} because there is no terminology service +ARRAY_CANNOT_BE_EMPTY = Array cannot be empty - the property should not be present if it has no values \ No newline at end of file diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java index 648f57520..4784f88c3 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java @@ -29,6 +29,10 @@ import java.util.UUID; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + import org.apache.commons.io.IOUtils; import org.hl7.fhir.convertors.VersionConvertorAdvisor50; import org.hl7.fhir.convertors.VersionConvertor_10_30; @@ -56,6 +60,7 @@ import org.hl7.fhir.r5.context.SimpleWorkerContext; import org.hl7.fhir.r5.elementmodel.Element; import org.hl7.fhir.r5.elementmodel.Manager; import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat; +import org.hl7.fhir.r5.elementmodel.ParserBase.ValidationPolicy; import org.hl7.fhir.r5.elementmodel.ObjectConverter; import org.hl7.fhir.r5.formats.FormatUtilities; import org.hl7.fhir.r5.formats.IParser.OutputStyle; @@ -98,19 +103,23 @@ import org.hl7.fhir.utilities.TimeTracker; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.VersionUtilities; import org.hl7.fhir.utilities.i18n.I18nConstants; +import org.hl7.fhir.utilities.json.JsonTrackingParser; import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager; import org.hl7.fhir.utilities.npm.NpmPackage; import org.hl7.fhir.utilities.npm.ToolsVersion; +import org.hl7.fhir.utilities.turtle.Turtle; 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.XhtmlComposer; import org.hl7.fhir.validation.BaseValidator.ValidationControl; +import org.hl7.fhir.validation.ValidationEngine.ValidationRecord; import org.hl7.fhir.validation.cli.model.ScanOutputItem; import org.hl7.fhir.validation.cli.services.StandAloneValidatorFetcher.IPackageInstaller; import org.hl7.fhir.validation.cli.utils.*; import org.hl7.fhir.validation.instance.InstanceValidator; +import org.w3c.dom.Document; import org.xml.sax.SAXException; @@ -187,6 +196,50 @@ POSSIBILITY OF SUCH DAMAGE. */ public class ValidationEngine implements IValidatorResourceFetcher, IPackageInstaller { + public class ValidationRecord { + + private String location; + private List messages; + int err = 0; + int warn = 0; + int info = 0; + + public ValidationRecord(String location, List messages) { + this.location = location; + this.messages = messages; + for (ValidationMessage vm : messages) { + if (vm.getLevel().equals(ValidationMessage.IssueSeverity.FATAL)||vm.getLevel().equals(ValidationMessage.IssueSeverity.ERROR)) + err++; + else if (vm.getLevel().equals(ValidationMessage.IssueSeverity.WARNING)) + warn++; + else if (!vm.isSignpost()) { + info++; + } + } + } + + public String getLocation() { + return location; + } + + public List getMessages() { + return messages; + } + + public int getErr() { + return err; + } + + public int getWarn() { + return warn; + } + + public int getInfo() { + return info; + } + + } + public class TransformSupportServices implements ITransformerServices { private List outputs; @@ -538,7 +591,7 @@ public class ValidationEngine implements IValidatorResourceFetcher, IPackageInst throw new FHIRException("Unable to fetch content from "+src+" ("+errors.toString()+")"); } - FhirFormat fmt = checkIsResource(cnt, src); + FhirFormat fmt = checkFormat(cnt, src); if (fmt != null) { Map res = new HashMap(); res.put(Utilities.changeFileExt(src, "."+fmt.getExtension()), cnt); @@ -809,33 +862,99 @@ public class ValidationEngine implements IValidatorResourceFetcher, IPackageInst this.noInvariantChecks = value; } + private FhirFormat checkFormat(byte[] cnt, String filename) { + System.out.println(" ..Detect format for "+filename); + try { + JsonTrackingParser.parseJson(cnt); + return FhirFormat.JSON; + } catch (Exception e) { + if (debug) { + System.out.println("Not JSON: "+e.getMessage()); + } + } + try { + parseXml(cnt); + return FhirFormat.XML; + } catch (Exception e) { + if (debug) { + System.out.println("Not XML: "+e.getMessage()); + } + } + try { + new Turtle().parse(TextFile.bytesToString(cnt)); + return FhirFormat.TURTLE; + } catch (Exception e) { + if (debug) { + System.out.println("Not Turtle: "+e.getMessage()); + } + } + try { + new StructureMapUtilities(context, null, null).parse(TextFile.bytesToString(cnt), null); + return FhirFormat.TEXT; + } catch (Exception e) { + if (debug) { + System.out.println("Not Text: "+e.getMessage()); + } + } + if (debug) + System.out.println(" .. not a resource: "+filename); + return null; + } + + private FhirFormat checkIsResource(byte[] cnt, String filename) { System.out.println(" ..Detect format for "+filename); try { Manager.parse(context, new ByteArrayInputStream(cnt), FhirFormat.JSON); return FhirFormat.JSON; } catch (Exception e) { + if (debug) { + System.out.println("Not JSON: "+e.getMessage()); + } } try { - Manager.parse(context, new ByteArrayInputStream(cnt),FhirFormat.XML); + parseXml(cnt); return FhirFormat.XML; } catch (Exception e) { + if (debug) { + System.out.println("Not XML: "+e.getMessage()); + } } try { Manager.parse(context, new ByteArrayInputStream(cnt),FhirFormat.TURTLE); return FhirFormat.TURTLE; } catch (Exception e) { + if (debug) { + System.out.println("Not Turtle: "+e.getMessage()); + } } try { new StructureMapUtilities(context, null, null).parse(TextFile.bytesToString(cnt), null); return FhirFormat.TEXT; } catch (Exception e) { + if (debug) { + System.out.println("Not Text: "+e.getMessage()); + } } if (debug) System.out.println(" .. not a resource: "+filename); return null; } + private Document parseXml(byte[] cnt) throws ParserConfigurationException, SAXException, IOException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + // xxe protection + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + factory.setXIncludeAware(false); + factory.setExpandEntityReferences(false); + factory.setNamespaceAware(true); + DocumentBuilder builder = factory.newDocumentBuilder(); + return builder.parse(new ByteArrayInputStream(cnt)); + } + private FhirFormat checkIsResource(String path) throws IOException { String ext = Utilities.getFileExtension(path); if (Utilities.existsInList(ext, "xml")) @@ -1097,7 +1216,7 @@ public class ValidationEngine implements IValidatorResourceFetcher, IPackageInst public OperationOutcome validate(String source, List profiles) throws FHIRException, IOException { List l = new ArrayList(); l.add(source); - return (OperationOutcome)validate(l, profiles); + return (OperationOutcome)validate(l, profiles, null); } public List validateScan(List sources, Set guides) throws FHIRException, IOException, EOperationOutcome { @@ -1193,7 +1312,7 @@ public class ValidationEngine implements IValidatorResourceFetcher, IPackageInst } } - public Resource validate(List sources, List profiles) throws FHIRException, IOException { + public Resource validate(List sources, List profiles, List record) throws FHIRException, IOException { if (profiles.size() > 0) { System.out.println(" Profiles: "+profiles); } @@ -1207,7 +1326,7 @@ public class ValidationEngine implements IValidatorResourceFetcher, IPackageInst System.out.print(" Validate " + ref); Content cnt = loadContent(ref, "validate", false); try { - OperationOutcome outcome = validate(ref, cnt.focus, cnt.cntType, profiles); + OperationOutcome outcome = validate(ref, cnt.focus, cnt.cntType, profiles, record); ToolingExtensions.addStringExtension(outcome, ToolingExtensions.EXT_OO_FILE, ref); System.out.println(" " + context.clock().milestone()); results.addEntry().setResource(outcome); @@ -1281,7 +1400,7 @@ public class ValidationEngine implements IValidatorResourceFetcher, IPackageInst } - public OperationOutcome validate(String location, byte[] source, FhirFormat cntType, List profiles) throws FHIRException, IOException, EOperationOutcome, SAXException { + public OperationOutcome validate(String location, byte[] source, FhirFormat cntType, List profiles, List record) throws FHIRException, IOException, EOperationOutcome, SAXException { List messages = new ArrayList(); if (doNative) { SchemaValidator.validateSchema(location, cntType, messages); @@ -1291,6 +1410,9 @@ public class ValidationEngine implements IValidatorResourceFetcher, IPackageInst if (showTimes) { System.out.println(location+": "+validator.reportTimes()); } + if (record != null) { + record.add(new ValidationRecord(location, messages)); + } return messagesToOutcome(messages); } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/model/CliContext.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/model/CliContext.java index 551277f28..e729046ee 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/model/CliContext.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/model/CliContext.java @@ -41,6 +41,8 @@ public class CliContext { private String map = null; @JsonProperty("output") private String output = null; + @JsonProperty("htmlOutput") + private String htmlOutput = null; @JsonProperty("txServer") private String txServer = "http://tx.fhir.org"; @JsonProperty("sv") @@ -252,6 +254,17 @@ public class CliContext { return this; } + @JsonProperty("output") + public String getHtmlOutput() { + return htmlOutput; + } + + @JsonProperty("output") + public CliContext setHtmlOutput(String htmlOutput) { + this.htmlOutput = htmlOutput; + return this; + } + @JsonProperty("canDoNative") public boolean getCanDoNative() { return canDoNative; @@ -472,6 +485,7 @@ public class CliContext { noExtensibleBindingMessages == that.noExtensibleBindingMessages && Objects.equals(map, that.map) && Objects.equals(output, that.output) && + Objects.equals(htmlOutput, that.htmlOutput) && Objects.equals(txServer, that.txServer) && Objects.equals(sv, that.sv) && Objects.equals(txLog, that.txLog) && @@ -493,7 +507,7 @@ public class CliContext { @Override public int hashCode() { - return Objects.hash(doNative, anyExtensionsAllowed, hintAboutNonMustSupport, recursive, doDebug, assumeValidRestReferences, canDoNative, noInternalCaching, noExtensibleBindingMessages, map, output, txServer, sv, txLog, mapLog, lang, fhirpath, snomedCT, targetVer, igs, questionnaireMode, profiles, sources, mode, locale, locations, crumbTrails, showTimes); + return Objects.hash(doNative, anyExtensionsAllowed, hintAboutNonMustSupport, recursive, doDebug, assumeValidRestReferences, canDoNative, noInternalCaching, noExtensibleBindingMessages, map, output, htmlOutput, txServer, sv, txLog, mapLog, lang, fhirpath, snomedCT, targetVer, igs, questionnaireMode, profiles, sources, mode, locale, locations, crumbTrails, showTimes); } @Override @@ -510,6 +524,7 @@ public class CliContext { ", noExtensibleBindingMessages=" + noExtensibleBindingMessages + ", map='" + map + '\'' + ", output='" + output + '\'' + + ", htmlOutput='" + htmlOutput + '\'' + ", txServer='" + txServer + '\'' + ", sv='" + sv + '\'' + ", txLog='" + txLog + '\'' + diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/HTMLOutputGenerator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/HTMLOutputGenerator.java new file mode 100644 index 000000000..a13abb813 --- /dev/null +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/HTMLOutputGenerator.java @@ -0,0 +1,181 @@ +package org.hl7.fhir.validation.cli.services; + +import java.text.DateFormat; +import java.util.Date; +import java.util.List; + +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.validation.ValidationEngine.ValidationRecord; +import org.hl7.fhir.validation.cli.utils.VersionUtil; + +public class HTMLOutputGenerator { + + private List records; + + public HTMLOutputGenerator(List records) { + super(); + this.records = records; + } + + public String generate(long time) { + + StringBuilder b = new StringBuilder(); + b.append(genHeader(time)); + int i = 0; + for (ValidationRecord f : records) { + i++; + b.append(genSummaryRow(i, f)); + } + b.append("
\r\n"); + + i = 0; + int id = 0; + for (ValidationRecord f : records) { + i++; + b.append(genStart(i, f)); + if (f.getMessages().size() > 0) { + b.append( + " \r\n"+ + " \r\n"+ + " \r\n"+ + " \r\n"); + for (ValidationMessage vm : f.getMessages()) { + id++; + b.append(genDetails(vm, "m"+id)); + } + b.append("
PathSeverityMessage
\r\n"); + } else { + b.append("

No Issues detected

\r\n"); + } + } + return b.toString(); + } + + private String genHeader(long time) { + int err = 0; + int warn = 0; + int info = 0; + for (ValidationRecord f : records) { + err = err + f.getErr(); + warn = warn + f.getWarn(); + info = info + f.getInfo(); + } + + return + "\r\n"+ + "\r\n"+ + "\r\n"+ + " Validation Results\r\n"+ + " \r\n"+ + " \r\n"+ + " \r\n"+ + "\r\n"+ + "\r\n"+ + "

Validation Results

\r\n"+ + "

"+err+" "+Utilities.pluralize("error", err)+", "+warn+" "+Utilities.pluralize("warning", warn)+", "+info+" "+Utilities.pluralize("hint", info)+". Generated "+now()+" by Validator "+VersionUtil.getVersionString()+" ("+time+"ms)

\r\n"+ + " \r\n"+ + " \r\n"+ + " \r\n"+ + " \r\n"; + } + + private String now() { + return DateFormat.getDateTimeInstance().format(new Date()); + } + + private String genSummaryRow(int i, ValidationRecord rec) { + String color = colorForLevel(IssueSeverity.ERROR, false); + if (rec.getErr() == 0) { + color = "#EFFFEF"; + } + return + " \r\n"+ + " \r\n"+ + " \r\n"; + } + + private String genStart(int i, ValidationRecord f) { + String xlink = Utilities.isAbsoluteUrl(f.getLocation()) ? f.getLocation() : "file:"+f.getLocation(); + return + "
\r\n"+ + " \r\n"+ + "

"+Utilities.escapeXml(f.getLocation())+"

\r\n"; + } + + + private String genDetails(ValidationMessage vm, String id) { + String path = vm.getLocation() == null ? "" : vm.getLocation()+ lineCol(vm); + String level = vm.isSlicingHint() ? "Slicing Information" : vm.isSignpost() ? "Process Info" : vm.getLevel().toCode(); + String color = colorForLevel(vm.getLevel(), vm.isSignpost()); + String mid = vm.getMessageId(); + String msg = vm.getHtml(); + String msgdetails = vm.isSlicingHint() ? vm.getSliceHtml() : vm.getHtml(); + if (vm.isSlicingHint()) { + return + " \r\n"+ + " \r\n"+ + " \r\n"; + + } else { + return + " \r\n"+ + " \r\n"+ + " \r\n"; + } + } + + private String lineCol(ValidationMessage vm) { + return vm.getLine() > 0 ? " (l"+vm.getLine()+"/c"+vm.getCol()+")" : ""; + } + + + private String colorForLevel(IssueSeverity level, boolean signpost) { + if (signpost) { + return "#d6feff"; + } + switch (level) { + case ERROR: + return "#ffcccc"; + case FATAL: + return "#ff9999"; + case WARNING: + return "#ffebcc"; + default: // INFORMATION: + return "#ffffe6"; + } + } + + private String halfColorForLevel(IssueSeverity level, boolean signpost) { + if (signpost) { + return "#e3feff"; + } + switch (level) { + case ERROR: + return "#ffeeee"; + case FATAL: + return "#ffcccc"; + case WARNING: + return "#fff4ee"; + default: // INFORMATION: + return "#fffff2"; + } + } + + +} diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java index 6d4aa8acc..16417caa8 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java @@ -27,6 +27,7 @@ import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.VersionUtilities; import org.hl7.fhir.utilities.validation.ValidationMessage; import org.hl7.fhir.validation.ValidationEngine; +import org.hl7.fhir.validation.ValidationEngine.ValidationRecord; import org.hl7.fhir.validation.cli.model.*; import org.hl7.fhir.validation.cli.utils.EngineMode; import org.hl7.fhir.validation.cli.utils.VersionSourceInformation; @@ -69,7 +70,9 @@ public class ValidationService { } public static void validateSources(CliContext cliContext, ValidationEngine validator) throws Exception { - Resource r = validator.validate(cliContext.getSources(), cliContext.getProfiles()); + long start = System.currentTimeMillis(); + List records = new ArrayList<>(); + Resource r = validator.validate(cliContext.getSources(), cliContext.getProfiles(), records); int ec = 0; System.out.println("Done. "+validator.getContext().clock().report()); System.out.println(); @@ -92,6 +95,11 @@ public class ValidationService { x.compose(s, r); s.close(); } + if (cliContext.getHtmlOutput() != null) { + String html = new HTMLOutputGenerator(records).generate(System.currentTimeMillis()-start); + TextFile.stringToFile(html, cliContext.getHtmlOutput()); + System.out.println("HTML Summary in "+cliContext.getHtmlOutput()); + } System.exit(ec > 0 ? 1 : 0); } @@ -288,10 +296,12 @@ public class ValidationService { System.out.println("Scanning for versions (no -version parameter):"); VersionSourceInformation versions = ValidationService.scanForVersions(cliContext); for (String s : versions.getReport()) { - System.out.println(" " + s); + if (!s.equals("(nothing found)")) { + System.out.println(" " + s); + } } if (versions.isEmpty()) { - System.out.println("-> Using Default version '" + VersionUtilities.CURRENT_VERSION + "'"); + System.out.println(" No Version Info found: Using Default version '" + VersionUtilities.CURRENT_VERSION + "'"); return "current"; } if (versions.size() == 1) { diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/utils/Params.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/utils/Params.java index 3790f511f..51fdfc99d 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/utils/Params.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/utils/Params.java @@ -12,6 +12,7 @@ public class Params { public static final String VERSION = "-version"; public static final String OUTPUT = "-output"; + public static final String HTML_OUTPUT = "-html-output"; public static final String PROXY = "-proxy"; public static final String PROFILE = "-profile"; public static final String BUNDLE = "-bundle"; @@ -93,6 +94,11 @@ public class Params { throw new Error("Specified -output without indicating output file"); else cliContext.setOutput(args[++i]); + } else if (args[i].equals(HTML_OUTPUT)) { + if (i + 1 == args.length) + throw new Error("Specified -html-output without indicating output file"); + else + cliContext.setHtmlOutput(args[++i]); } else if (args[i].equals(PROXY)) { i++; // ignore next parameter } else if (args[i].equals(PROFILE)) { diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java index cc147f5c3..1727acab8 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java @@ -961,7 +961,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } else { if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, codings.size() == fixed.getCoding().size(), I18nConstants.TERMINOLOGY_TX_CODING_COUNT, Integer.toString(fixed.getCoding().size()), Integer.toString(codings.size()))) { for (int i = 0; i < codings.size(); i++) - checkFixedValue(errors, path + ".coding", codings.get(i), fixed.getCoding().get(i), fixedSource, "coding", focus); + checkFixedValue(errors, path + ".coding", codings.get(i), fixed.getCoding().get(i), fixedSource, "coding", focus, false); } } } @@ -1748,10 +1748,6 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat return b.toString(); } - private void checkFixedValue(List errors, String path, Element focus, org.hl7.fhir.r5.model.Element fixed, String fixedSource, String propName, Element parent) { - checkFixedValue(errors, path, focus, fixed, fixedSource, propName, parent, false); - } - @SuppressWarnings("rawtypes") private void checkFixedValue(List errors, String path, Element focus, org.hl7.fhir.r5.model.Element fixed, String fixedSource, String propName, Element parent, boolean pattern) { if ((fixed == null || fixed.isEmpty()) && focus == null) { @@ -1829,7 +1825,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat for (Extension e : fixed.getExtension()) { Element ex = getExtensionByUrl(extensions, e.getUrl()); if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, ex != null, I18nConstants.EXTENSION_EXT_COUNT_NOTFOUND, e.getUrl())) { - checkFixedValue(errors, path, ex.getNamedChild("extension").getNamedChild("value"), e.getValue(), fixedSource, "extension.value", ex.getNamedChild("extension")); + checkFixedValue(errors, path, ex.getNamedChild("extension").getNamedChild("value"), e.getValue(), fixedSource, "extension.value", ex.getNamedChild("extension"), false); } } } @@ -1842,25 +1838,33 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriod(), fixedSource, "period", focus, pattern); List parts = new ArrayList(); - focus.getNamedChildren("family", parts); - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() > 0 == fixed.hasFamily(), I18nConstants.FIXED_TYPE_CHECKS_DT_NAME_FAMILY, (fixed.hasFamily() ? "1" : "0"), Integer.toString(parts.size()))) { - for (int i = 0; i < parts.size(); i++) - checkFixedValue(errors, path + ".family", parts.get(i), fixed.getFamilyElement(), fixedSource, "family", focus, pattern); + if (!pattern || fixed.hasFamily()) { + focus.getNamedChildren("family", parts); + if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() > 0 == fixed.hasFamily(), I18nConstants.FIXED_TYPE_CHECKS_DT_NAME_FAMILY, (fixed.hasFamily() ? "1" : "0"), Integer.toString(parts.size()))) { + for (int i = 0; i < parts.size(); i++) + checkFixedValue(errors, path + ".family", parts.get(i), fixed.getFamilyElement(), fixedSource, "family", focus, pattern); + } } - focus.getNamedChildren("given", parts); - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getGiven().size(), I18nConstants.FIXED_TYPE_CHECKS_DT_NAME_GIVEN, Integer.toString(fixed.getGiven().size()), Integer.toString(parts.size()))) { - for (int i = 0; i < parts.size(); i++) - checkFixedValue(errors, path + ".given", parts.get(i), fixed.getGiven().get(i), fixedSource, "given", focus, pattern); + if (!pattern || fixed.hasGiven()) { + focus.getNamedChildren("given", parts); + if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getGiven().size(), I18nConstants.FIXED_TYPE_CHECKS_DT_NAME_GIVEN, Integer.toString(fixed.getGiven().size()), Integer.toString(parts.size()))) { + for (int i = 0; i < parts.size(); i++) + checkFixedValue(errors, path + ".given", parts.get(i), fixed.getGiven().get(i), fixedSource, "given", focus, pattern); + } } - focus.getNamedChildren("prefix", parts); - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getPrefix().size(), I18nConstants.FIXED_TYPE_CHECKS_DT_NAME_PREFIX, Integer.toString(fixed.getPrefix().size()), Integer.toString(parts.size()))) { - for (int i = 0; i < parts.size(); i++) - checkFixedValue(errors, path + ".prefix", parts.get(i), fixed.getPrefix().get(i), fixedSource, "prefix", focus, pattern); + if (!pattern || fixed.hasPrefix()) { + focus.getNamedChildren("prefix", parts); + if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getPrefix().size(), I18nConstants.FIXED_TYPE_CHECKS_DT_NAME_PREFIX, Integer.toString(fixed.getPrefix().size()), Integer.toString(parts.size()))) { + for (int i = 0; i < parts.size(); i++) + checkFixedValue(errors, path + ".prefix", parts.get(i), fixed.getPrefix().get(i), fixedSource, "prefix", focus, pattern); + } } - focus.getNamedChildren("suffix", parts); - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getSuffix().size(), I18nConstants.FIXED_TYPE_CHECKS_DT_NAME_SUFFIX, Integer.toString(fixed.getSuffix().size()), Integer.toString(parts.size()))) { - for (int i = 0; i < parts.size(); i++) - checkFixedValue(errors, path + ".suffix", parts.get(i), fixed.getSuffix().get(i), fixedSource, "suffix", focus, pattern); + if (!pattern || fixed.hasSuffix()) { + focus.getNamedChildren("suffix", parts); + if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getSuffix().size(), I18nConstants.FIXED_TYPE_CHECKS_DT_NAME_SUFFIX, Integer.toString(fixed.getSuffix().size()), Integer.toString(parts.size()))) { + for (int i = 0; i < parts.size(); i++) + checkFixedValue(errors, path + ".suffix", parts.get(i), fixed.getSuffix().get(i), fixedSource, "suffix", focus, pattern); + } } } @@ -3054,9 +3058,22 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat // work back through the parent list. // really, there should only be one level for this (contained resources cannot contain // contained resources), but we'll leave that to some other code to worry about + boolean wasContained = false; while (stack != null && stack.getElement() != null) { if (stack.getElement().getProperty().isResource()) { // ok, we'll try to find the contained reference + if (ref.equals("#") && stack.getElement().getSpecial() != SpecialElement.CONTAINED && wasContained) { + ResolvedReference rr = new ResolvedReference(); + rr.setResource(stack.getElement()); + rr.setFocus(stack.getElement()); + rr.setExternal(false); + rr.setStack(stack.push(stack.getElement(), -1, stack.getElement().getProperty().getDefinition(), stack.getElement().getProperty().getDefinition())); + rr.getStack().qualifyPath(".ofType("+stack.getElement().fhirType()+")"); + return rr; + } + if (stack.getElement().getSpecial() == SpecialElement.CONTAINED) { + wasContained = true; + } IndexedElement res = getContainedById(stack.getElement(), ref.substring(1)); if (res != null) { ResolvedReference rr = new ResolvedReference(); @@ -4069,8 +4086,12 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat // check type invariants checkInvariants(hostContext, errors, profile, definition, resource, element, stack, false); - if (definition.getFixed() != null) - checkFixedValue(errors, stack.getLiteralPath(), element, definition.getFixed(), profile.getUrl(), definition.getSliceName(), null); + if (definition.getFixed() != null) { + checkFixedValue(errors, stack.getLiteralPath(), element, definition.getFixed(), profile.getUrl(), definition.getSliceName(), null, false); + } + if (definition.getPattern() != null) { + checkFixedValue(errors, stack.getLiteralPath(), element, definition.getPattern(), profile.getUrl(), definition.getSliceName(), null, true); + } // get the list of direct defined children, including slices List childDefinitions = profileUtilities.getChildMap(profile, definition); @@ -4136,13 +4157,20 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat Element resource, Element element, String actualType, NodeStack stack, boolean inCodeableConcept, boolean checkDisplayInContext, ElementInfo ei, String extensionUrl) throws FHIRException, DefinitionException { + if (debug && ei.definition != null && ei.slice != null) { + System.out.println(Utilities.padLeft("", ' ', stack.depth())+ "Check "+ei.getPath()+" against both "+ei.definition.getId()+" and "+ei.slice.getId()); + } if (ei.definition != null) { - checkChildByDefinition(hostContext, errors, profile, definition, resource, element, actualType, stack, inCodeableConcept, checkDisplayInContext, ei, - extensionUrl, ei.definition, false); + if (debug) { + System.out.println(Utilities.padLeft("", ' ', stack.depth())+ "Check "+ei.getPath()+" against defn "+ei.definition.getId()); + } + checkChildByDefinition(hostContext, errors, profile, definition, resource, element, actualType, stack, inCodeableConcept, checkDisplayInContext, ei, extensionUrl, ei.definition, false); } if (ei.slice != null) { - checkChildByDefinition(hostContext, errors, profile, definition, resource, element, actualType, stack, inCodeableConcept, checkDisplayInContext, ei, - extensionUrl, ei.slice, true); + if (debug) { + System.out.println(Utilities.padLeft("", ' ', stack.depth())+ "Check "+ei.getPath()+" against slice "+ei.slice.getId()); + } + checkChildByDefinition(hostContext, errors, profile, definition, resource, element, actualType, stack, inCodeableConcept, checkDisplayInContext, ei, extensionUrl, ei.slice, true); } } @@ -4186,11 +4214,13 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } else if (checkDefn.getType().size() > 1) { String prefix = tail(checkDefn.getPath()); - assert typesAreAllReference(checkDefn.getType()) || checkDefn.hasRepresentation(PropertyRepresentation.TYPEATTR) || prefix.endsWith("[x]") : prefix; + assert typesAreAllReference(checkDefn.getType()) || checkDefn.hasRepresentation(PropertyRepresentation.TYPEATTR) || prefix.endsWith("[x]") || isResourceAndTypes(checkDefn) : "Multiple Types allowed, but name is wrong @ "+checkDefn.getPath()+": "+checkDefn.typeSummaryVB(); - if (checkDefn.hasRepresentation(PropertyRepresentation.TYPEATTR)) + if (checkDefn.hasRepresentation(PropertyRepresentation.TYPEATTR)) { type = ei.getElement().getType(); - else { + } else if (ei.getElement().isResource()) { + type = ei.getElement().fhirType(); + } else { prefix = prefix.substring(0, prefix.length() - 3); for (TypeRefComponent t : checkDefn.getType()) if ((prefix + Utilities.capitalize(t.getWorkingCode())).equals(ei.getName())) { @@ -4251,7 +4281,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat checkPrimitive(hostContext, errors, ei.getPath(), type, checkDefn, ei.getElement(), profile, stack); } else { if (checkDefn.hasFixed()) { - checkFixedValue(errors, ei.getPath(), ei.getElement(), checkDefn.getFixed(), profile.getUrl(), checkDefn.getSliceName(), null); + checkFixedValue(errors, ei.getPath(), ei.getElement(), checkDefn.getFixed(), profile.getUrl(), checkDefn.getSliceName(), null, false); } if (checkDefn.hasPattern()) { checkFixedValue(errors, ei.getPath(), ei.getElement(), checkDefn.getPattern(), profile.getUrl(), checkDefn.getSliceName(), null, true); @@ -4386,6 +4416,18 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } } + private boolean isResourceAndTypes(ElementDefinition ed) { + if (!Utilities.existsInList(ed.getBase().getPath(), "Bundle.entry.resource", "Bundle.entry.response.outcome", "DomainResource.contained", "Parameters.parameter.resource", "Parameters.parameter.part.resource")) { + return false; + } + for (TypeRefComponent tr : ed.getType()) { + if (!isResource(tr.getCode())) { + return false; + } + } + return true; + } + private boolean isResource(String type) { StructureDefinition sd = context.fetchTypeDefinition(type); return sd != null && sd.getKind().equals(StructureDefinitionKind.RESOURCE); @@ -4476,18 +4518,18 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } } } - String location = "Profile " + profile.getUrl() + ", Element '" + stack.getLiteralPath() + "." + tail(ed.getPath()) + (ed.hasSliceName() ? "[" + ed.getSliceName() + (ed.hasLabel() ? " (" + ed.getLabel() + ")" : "") + "]" : "") + "'"; if (ed.getMin() > 0) { if (problematicPaths.contains(ed.getPath())) - hint(errors, IssueType.NOTSUPPORTED, element.line(), element.col(), stack.getLiteralPath(), count >= ed.getMin(), I18nConstants.VALIDATION_VAL_PROFILE_NOCHECKMIN, location, Integer.toString(ed.getMin())); + hint(errors, IssueType.NOTSUPPORTED, element.line(), element.col(), stack.getLiteralPath(), count >= ed.getMin(), I18nConstants.VALIDATION_VAL_PROFILE_NOCHECKMIN, profile.getUrl(), ed.getPath(), ed.getId(), ed.getSliceName(),ed.getLabel(), stack.getLiteralPath(), Integer.toString(ed.getMin())); else - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), count >= ed.getMin(), I18nConstants.VALIDATION_VAL_PROFILE_MINIMUM, location, Integer.toString(ed.getMin()), Integer.toString(count)); + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), count >= ed.getMin(), I18nConstants.VALIDATION_VAL_PROFILE_MINIMUM, profile.getUrl(), ed.getPath(), ed.getId(), ed.getSliceName(),ed.getLabel(), stack.getLiteralPath(), Integer.toString(ed.getMin()), Integer.toString(count)); } if (ed.hasMax() && !ed.getMax().equals("*")) { if (problematicPaths.contains(ed.getPath())) - hint(errors, IssueType.NOTSUPPORTED, element.line(), element.col(), stack.getLiteralPath(), count <= Integer.parseInt(ed.getMax()), I18nConstants.VALIDATION_VAL_PROFILE_NOCHECKMAX, location, ed.getMax()); - else - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), count <= Integer.parseInt(ed.getMax()), I18nConstants.VALIDATION_VAL_PROFILE_MAXIMUM, location, ed.getMax(), Integer.toString(count)); + hint(errors, IssueType.NOTSUPPORTED, element.line(), element.col(), stack.getLiteralPath(), count <= Integer.parseInt(ed.getMax()), I18nConstants.VALIDATION_VAL_PROFILE_NOCHECKMAX, profile.getUrl(), ed.getPath(), ed.getId(), ed.getSliceName(),ed.getLabel(), stack.getLiteralPath(), ed.getMax()); + else if (count > Integer.parseInt(ed.getMax())) { + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), false, I18nConstants.VALIDATION_VAL_PROFILE_MAXIMUM, profile.getUrl(), ed.getPath(), ed.getId(), ed.getSliceName(),ed.getLabel(), stack.getLiteralPath(), ed.getMax(), Integer.toString(count)); + } } } } @@ -4863,7 +4905,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat private boolean valueMatchesCriteria(Element value, ElementDefinition criteria, StructureDefinition profile) throws FHIRException { if (criteria.hasFixed()) { List msgs = new ArrayList(); - checkFixedValue(msgs, "{virtual}", value, criteria.getFixed(), profile.getUrl(), "value", null); + checkFixedValue(msgs, "{virtual}", value, criteria.getFixed(), profile.getUrl(), "value", null, false); return msgs.size() == 0; } else if (criteria.hasBinding() && criteria.getBinding().getStrength() == BindingStrength.REQUIRED && criteria.getBinding().hasValueSet()) { throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SLICE_MATCHING__SLICE_MATCHING_BY_VALUE_SET_NOT_DONE)); @@ -4983,6 +5025,12 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat if ("(component.empty() and related.empty()) implies (dataAbsentReason or value)".equals(expr)) return "(component.empty() and related.empty()) implies (dataAbsentReason.exists() or value.exists())"; + if ("reference.startsWith('#').not() or (reference.substring(1).trace('url') in %rootResource.contained.id.trace('ids'))".equals(expr)) { + return "(reference = '#') or reference.startsWith('#').not() or (reference.substring(1).trace('url') in %rootResource.contained.id.trace('ids'))"; + } + if ("reference.startsWith('#').not() or (reference.substring(1).trace('url') in %resource.contained.id.trace('ids'))".equals(expr)) { + return "(reference = '#') or reference.startsWith('#').not() or (reference.substring(1).trace('url') in %resource.contained.id.trace('ids'))"; + } if ("".equals(expr)) return ""; return expr; diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/StructureDefinitionValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/StructureDefinitionValidator.java index 86b805852..0f8de3f63 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/StructureDefinitionValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/StructureDefinitionValidator.java @@ -165,11 +165,25 @@ public class StructureDefinitionValidator extends BaseValidator { if (t == null) { rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), code.equals(t), I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p); } else { - rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), code.equals(t), I18nConstants.SD_ED_TYPE_PROFILE_WRONG, p, t, code); + rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), isInstanceOf(t, code), I18nConstants.SD_ED_TYPE_PROFILE_WRONG, p, t, code); } } } + private boolean isInstanceOf(String t, String code) { + StructureDefinition sd = context.fetchTypeDefinition(t); + while (sd != null) { + if (sd.getType().equals(code)) { + return true; + } + sd = sd.hasBaseDefinition() ? context.fetchResource(StructureDefinition.class, sd.getBaseDefinition()) : null; + if (sd != null && !sd.getAbstract()) { + sd = null; + } + } + return false; + } + private String determineBaseType(StructureDefinition sd) { while (sd != null && !sd.hasType() && sd.getDerivation() == TypeDerivationRule.CONSTRAINT) { sd = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition()); diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/utils/NodeStack.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/utils/NodeStack.java index cf32d7f03..abb85ac64 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/utils/NodeStack.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/utils/NodeStack.java @@ -187,5 +187,13 @@ public class NodeStack { return literalPath; } + public int depth() { + if (parent == null) { + return 0; + } else { + return parent.depth()+1; + } + } + } \ No newline at end of file diff --git a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationEngineTests.java b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationEngineTests.java index 18be9008a..636c04747 100644 --- a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationEngineTests.java +++ b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationEngineTests.java @@ -90,7 +90,7 @@ public class ValidationEngineTests { } if (!org.hl7.fhir.validation.tests.utilities.TestUtilities.silent) System.out.println("Test102: Validate patient-example.xml in v1.0.2 version"); - ValidationEngine ve = new ValidationEngine("hl7.fhir.r2.core#1.0.2", DEF_TX, null, FhirPublication.DSTU2, "41.0.2"); + ValidationEngine ve = new ValidationEngine("hl7.fhir.r2.core#1.0.2", DEF_TX, null, FhirPublication.DSTU2, "1.0.2"); ve.setNoInvariantChecks(true); OperationOutcome op = ve.validate(FhirFormat.XML, TestingUtilities.loadTestResourceStream("validator", "patient102.xml"), null); if (!TestUtilities.silent) diff --git a/pom.xml b/pom.xml index 2956882bd..4433e7e77 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ 5.1.0 - 1.1.46 + 1.1.47-SNAPSHOT 5.6.2 3.0.0-M4 0.8.5
FilenameErrorsWarningsHints
"+Utilities.escapeXml(rec.getLocation())+""+rec.getErr()+""+rec.getWarn()+""+rec.getInfo()+"
"+path+""+level+""+msg+" Show Reasoning

 

"+msgdetails+"
"+path+""+level+""+msg+"