diff --git a/.github/workflows/license-check/license-whitelist.txt b/.github/workflows/license-check/license-whitelist.txt index 8ca2bd194..b938e0ba5 100644 --- a/.github/workflows/license-check/license-whitelist.txt +++ b/.github/workflows/license-check/license-whitelist.txt @@ -10,6 +10,7 @@ Apache-2.0 Apache 2 Apache 2.0 Apache License 2.0 +Apache License version 2.0 Eclipse Public License v2.0 BSD licence The BSD License diff --git a/org.hl7.fhir.convertors/pom.xml b/org.hl7.fhir.convertors/pom.xml index 723c5772f..929510c49 100644 --- a/org.hl7.fhir.convertors/pom.xml +++ b/org.hl7.fhir.convertors/pom.xml @@ -5,7 +5,7 @@ <parent> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>org.hl7.fhir.core</artifactId> - <version>6.3.21-SNAPSHOT</version> + <version>6.3.23-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> diff --git a/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/conv40_50/datatypes40_50/BackboneElement40_50.java b/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/conv40_50/datatypes40_50/BackboneElement40_50.java index ccd124b48..f1e573648 100644 --- a/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/conv40_50/datatypes40_50/BackboneElement40_50.java +++ b/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/conv40_50/datatypes40_50/BackboneElement40_50.java @@ -1,5 +1,7 @@ package org.hl7.fhir.convertors.conv40_50.datatypes40_50; +import java.util.Arrays; + import org.hl7.fhir.convertors.context.ConversionContext40_50; import org.hl7.fhir.convertors.conv40_50.datatypes40_50.special40_50.Extension40_50; import org.hl7.fhir.exceptions.FHIRException; @@ -8,14 +10,22 @@ public class BackboneElement40_50 { public static void copyBackboneElement(org.hl7.fhir.r4.model.BackboneElement src, org.hl7.fhir.r5.model.BackboneElement tgt, String ... extensionUrlsToIgnore) throws FHIRException { ConversionContext40_50.INSTANCE.getVersionConvertor_40_50().copyElement(src, tgt, extensionUrlsToIgnore); for (org.hl7.fhir.r4.model.Extension e : src.getModifierExtension()) { - tgt.addModifierExtension(Extension40_50.convertExtension(e)); + if (!isExemptExtension(e.getUrl(), extensionUrlsToIgnore)) { + tgt.addModifierExtension(Extension40_50.convertExtension(e)); + } } } + public static boolean isExemptExtension(String url, String[] extensionsToIgnore) { + return Arrays.asList(extensionsToIgnore).contains(url); + } + public static void copyBackboneElement(org.hl7.fhir.r5.model.BackboneElement src, org.hl7.fhir.r4.model.BackboneElement tgt, String... extensionUrlsToIgnore) throws FHIRException { ConversionContext40_50.INSTANCE.getVersionConvertor_40_50().copyElement(src, tgt, extensionUrlsToIgnore); for (org.hl7.fhir.r5.model.Extension e : src.getModifierExtension()) { - tgt.addModifierExtension(Extension40_50.convertExtension(e)); + if (!isExemptExtension(e.getUrl(), extensionUrlsToIgnore)) { + tgt.addModifierExtension(Extension40_50.convertExtension(e)); + } } } diff --git a/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/conv40_50/resources40_50/ConceptMap40_50.java b/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/conv40_50/resources40_50/ConceptMap40_50.java index 9b2ffc4cd..d413d746b 100644 --- a/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/conv40_50/resources40_50/ConceptMap40_50.java +++ b/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/conv40_50/resources40_50/ConceptMap40_50.java @@ -207,6 +207,9 @@ public class ConceptMap40_50 { for (org.hl7.fhir.r4.model.ConceptMap.TargetElementComponent t : src.getTarget()) { if (t.getEquivalence() == org.hl7.fhir.r4.model.Enumerations.ConceptMapEquivalence.UNMATCHED) { tgt.setNoMap(true); + if (t.hasComment()) { + tgt.addExtension("http://hl7.org/fhir/4.0/StructureDefinition/extension-ConceptMap.group.element.target.comment", ConversionContext40_50.INSTANCE.getVersionConvertor_40_50().convertType(t.getCommentElement())); + } } else { tgt.addTarget(convertTargetElementComponent(t, tgtMap)); } @@ -218,13 +221,18 @@ public class ConceptMap40_50 { if (src == null) return null; org.hl7.fhir.r4.model.ConceptMap.SourceElementComponent tgt = new org.hl7.fhir.r4.model.ConceptMap.SourceElementComponent(); - ConversionContext40_50.INSTANCE.getVersionConvertor_40_50().copyBackboneElement(src, tgt); + ConversionContext40_50.INSTANCE.getVersionConvertor_40_50().copyBackboneElement(src, tgt, "http://hl7.org/fhir/4.0/StructureDefinition/extension-ConceptMap.group.element.target.comment"); if (src.hasCode()) tgt.setCodeElement(Code40_50.convertCode(src.getCodeElement())); if (src.hasDisplay()) tgt.setDisplayElement(String40_50.convertString(src.getDisplayElement())); if (src.hasNoMap() && src.getNoMap() == true) { - tgt.addTarget(new org.hl7.fhir.r4.model.ConceptMap.TargetElementComponent().setEquivalence(org.hl7.fhir.r4.model.Enumerations.ConceptMapEquivalence.UNMATCHED)); + org.hl7.fhir.r4.model.ConceptMap.TargetElementComponent t = new org.hl7.fhir.r4.model.ConceptMap.TargetElementComponent(); + t.setEquivalence(org.hl7.fhir.r4.model.Enumerations.ConceptMapEquivalence.UNMATCHED); + if (src.hasExtension("http://hl7.org/fhir/4.0/StructureDefinition/extension-ConceptMap.group.element.target.comment")) { + t.setCommentElement((org.hl7.fhir.r4.model.StringType) ConversionContext40_50.INSTANCE.getVersionConvertor_40_50().convertType(src.getExtensionByUrl("http://hl7.org/fhir/4.0/StructureDefinition/extension-ConceptMap.group.element.target.comment").getValue())); + } + tgt.addTarget(t); } else { for (org.hl7.fhir.r5.model.ConceptMap.TargetElementComponent t : src.getTarget()) tgt.addTarget(convertTargetElementComponent(t, srcMap)); diff --git a/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/misc/VSACImporter.java b/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/misc/VSACImporter.java index 2b9936a9f..61e4fb04c 100644 --- a/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/misc/VSACImporter.java +++ b/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/misc/VSACImporter.java @@ -18,6 +18,7 @@ import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.r4.formats.IParser.OutputStyle; import org.hl7.fhir.r4.formats.JsonParser; import org.hl7.fhir.r4.model.CapabilityStatement; +import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.IntegerType; import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity; @@ -60,8 +61,18 @@ public class VSACImporter extends OIDBasedValueSetImporter { CapabilityStatement cs = fhirToolingClient.getCapabilitiesStatement(); JsonParser json = new JsonParser(); - json.setOutputStyle(OutputStyle.PRETTY).compose(ManagedFileAccess.outStream(Utilities.path("[tmp]", "vsac-capability-statmenet.json")), cs); + json.setOutputStyle(OutputStyle.PRETTY).compose(ManagedFileAccess.outStream(Utilities.path("[tmp]", "vsac-capability-statement.json")), cs); + System.out.println("CodeSystems"); + CodeSystem css = fhirToolingClient.fetchResource(CodeSystem.class, "CDCNHSN"); + json.setOutputStyle(OutputStyle.PRETTY).compose(ManagedFileAccess.outStream(Utilities.path(dest, "CodeSystem-CDCNHSN.json")), css); + css = fhirToolingClient.fetchResource(CodeSystem.class, "CDCREC"); + json.setOutputStyle(OutputStyle.PRETTY).compose(ManagedFileAccess.outStream(Utilities.path(dest, "CodeSystem-CDCREC.json")), css); + css = fhirToolingClient.fetchResource(CodeSystem.class, "HSLOC"); + json.setOutputStyle(OutputStyle.PRETTY).compose(ManagedFileAccess.outStream(Utilities.path(dest, "CodeSystem-HSLOC.json")), css); + css = fhirToolingClient.fetchResource(CodeSystem.class, "SOP"); + json.setOutputStyle(OutputStyle.PRETTY).compose(ManagedFileAccess.outStream(Utilities.path(dest, "CodeSystem-SOP.json")), css); + System.out.println("Loading"); List<String> oids = new ArrayList<>(); while (csv.line()) { diff --git a/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/conv40_50/ConceptMap40_50Test.java b/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/conv40_50/ConceptMap40_50Test.java new file mode 100644 index 000000000..4d131aec8 --- /dev/null +++ b/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/conv40_50/ConceptMap40_50Test.java @@ -0,0 +1,31 @@ +package org.hl7.fhir.convertors.conv40_50; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.hl7.fhir.convertors.factory.VersionConvertorFactory_40_50; +import org.hl7.fhir.r4.formats.IParser.OutputStyle; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class ConceptMap40_50Test { + + + @Test + @DisplayName("Test r5 -> r4 ConceptMap conversion.") + public void testR5_R4() throws IOException { + InputStream r4_input = this.getClass().getResourceAsStream("/cm_nomap.json"); + + org.hl7.fhir.r4.model.ConceptMap r4_actual = (org.hl7.fhir.r4.model.ConceptMap) new org.hl7.fhir.r4.formats.JsonParser().parse(r4_input); + org.hl7.fhir.r5.model.Resource r5_conv = VersionConvertorFactory_40_50.convertResource(r4_actual); + org.hl7.fhir.r4.model.Resource r4_conv = VersionConvertorFactory_40_50.convertResource(r5_conv); + + System.out.println(new org.hl7.fhir.r4.formats.JsonParser().setOutputStyle(OutputStyle.PRETTY).composeString(r4_actual)); + System.out.println(new org.hl7.fhir.r4.formats.JsonParser().setOutputStyle(OutputStyle.PRETTY).composeString(r4_conv)); + assertTrue(r4_actual.equalsDeep(r4_conv), "should be the same"); + } +} diff --git a/org.hl7.fhir.convertors/src/test/resources/cm_nomap.json b/org.hl7.fhir.convertors/src/test/resources/cm_nomap.json new file mode 100644 index 000000000..7645ac651 --- /dev/null +++ b/org.hl7.fhir.convertors/src/test/resources/cm_nomap.json @@ -0,0 +1,30 @@ +{ + "resourceType": "ConceptMap", + "id": "appointment-status-concept-map", + "url": "https://hl7.fi/fhir/finnish-scheduling/ConceptMap/appointment-status-concept-map", + "title": "FHIR Appointment status codes and Ajanvaraus - Ajanvarauksen tila", + "description": "Mapping between the Finnish logical model [*Ajanvaraus - Ajanvarauksen tila*](https://koodistopalvelu.kanta.fi/codeserver/pages/classification-view-page.xhtml?classificationKey=1943) (oid `1.2.246.537.6.881`) and FHIR Appoinment status codes, in both directions.", + "status": "draft", + "sourceUri": "https://koodistopalvelu.kanta.fi/codeserver/pages/classification-view-page.xhtml?classificationKey=1943", + "targetCanonical": "http://hl7.org/fhir/appointmentstatus", + "group": [{ + "source": "https://koodistopalvelu.kanta.fi/codeserver/pages/classification-view-page.xhtml?classificationKey=1943", + "target": "http://hl7.org/fhir/appointmentstatus", + "element": [{ + "code": "5", + "display": "Siirretty", + "target": [{ + "equivalence": "unmatched", + "comment": "There is no status for rescheduled appointments in FHIR. This code SHOULD be mapped to cancelled, if required. Note that this code is deprecated." + }] + }, { + "code" : "3", + "display" : "Varattu", + "target" : [{ + "code" : "booked", + "display" : "Booked", + "equivalence" : "equal" + }] + }] + ]} +} diff --git a/org.hl7.fhir.core.generator/src/org/hl7/fhir/core/generator/codegen/Configuration.java b/org.hl7.fhir.core.generator/src/org/hl7/fhir/core/generator/codegen/Configuration.java index 75bda20d1..f598b9750 100644 --- a/org.hl7.fhir.core.generator/src/org/hl7/fhir/core/generator/codegen/Configuration.java +++ b/org.hl7.fhir.core.generator/src/org/hl7/fhir/core/generator/codegen/Configuration.java @@ -11,6 +11,7 @@ import java.util.Map; import org.hl7.fhir.utilities.IniFile; import org.hl7.fhir.utilities.TextFile; import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.filesystem.ManagedFileAccess; public class Configuration { public static final SimpleDateFormat DATE_FORMAT() { diff --git a/org.hl7.fhir.core.generator/src/org/hl7/fhir/core/generator/codegen/extensions/JavaExtensionsGenerator.java b/org.hl7.fhir.core.generator/src/org/hl7/fhir/core/generator/codegen/extensions/JavaExtensionsGenerator.java index 6c1be9a8c..cdd93ad2b 100644 --- a/org.hl7.fhir.core.generator/src/org/hl7/fhir/core/generator/codegen/extensions/JavaExtensionsGenerator.java +++ b/org.hl7.fhir.core.generator/src/org/hl7/fhir/core/generator/codegen/extensions/JavaExtensionsGenerator.java @@ -12,6 +12,7 @@ import org.hl7.fhir.core.generator.engine.Definitions; import org.hl7.fhir.r5.conformance.profile.ProfileUtilities; import org.hl7.fhir.r5.model.StructureDefinition; import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.filesystem.ManagedFileAccess; public class JavaExtensionsGenerator { diff --git a/org.hl7.fhir.core.generator/src/org/hl7/fhir/core/generator/engine/JavaCoreGenerator.java b/org.hl7.fhir.core.generator/src/org/hl7/fhir/core/generator/engine/JavaCoreGenerator.java index 4bc849103..26cdbb2ea 100644 --- a/org.hl7.fhir.core.generator/src/org/hl7/fhir/core/generator/engine/JavaCoreGenerator.java +++ b/org.hl7.fhir.core.generator/src/org/hl7/fhir/core/generator/engine/JavaCoreGenerator.java @@ -37,6 +37,7 @@ import org.hl7.fhir.r5.model.StructureDefinition.TypeDerivationRule; import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.VersionUtilities; +import org.hl7.fhir.utilities.filesystem.ManagedFileAccess; import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager; import org.hl7.fhir.utilities.npm.NpmPackage; diff --git a/org.hl7.fhir.dstu2/pom.xml b/org.hl7.fhir.dstu2/pom.xml index 253c29c91..bbd08e95d 100644 --- a/org.hl7.fhir.dstu2/pom.xml +++ b/org.hl7.fhir.dstu2/pom.xml @@ -5,7 +5,7 @@ <parent> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>org.hl7.fhir.core</artifactId> - <version>6.3.21-SNAPSHOT</version> + <version>6.3.23-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> diff --git a/org.hl7.fhir.dstu2016may/pom.xml b/org.hl7.fhir.dstu2016may/pom.xml index 482c70381..59cf5c4ca 100644 --- a/org.hl7.fhir.dstu2016may/pom.xml +++ b/org.hl7.fhir.dstu2016may/pom.xml @@ -5,7 +5,7 @@ <parent> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>org.hl7.fhir.core</artifactId> - <version>6.3.21-SNAPSHOT</version> + <version>6.3.23-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> diff --git a/org.hl7.fhir.dstu2016may/src/main/java/org/hl7/fhir/dstu2016may/metamodel/XmlParser.java b/org.hl7.fhir.dstu2016may/src/main/java/org/hl7/fhir/dstu2016may/metamodel/XmlParser.java index d9d4746cd..fd78719fd 100644 --- a/org.hl7.fhir.dstu2016may/src/main/java/org/hl7/fhir/dstu2016may/metamodel/XmlParser.java +++ b/org.hl7.fhir.dstu2016may/src/main/java/org/hl7/fhir/dstu2016may/metamodel/XmlParser.java @@ -90,7 +90,7 @@ public class XmlParser extends ParserBase { factory.setNamespaceAware(true); if (policy == ValidationPolicy.EVERYTHING) { // use a slower parser that keeps location data - TransformerFactory transformerFactory = TransformerFactory.newInstance(); + TransformerFactory transformerFactory = XMLUtil.newXXEProtectedTransformerFactory(); Transformer nullTransformer = transformerFactory.newTransformer(); DocumentBuilder docBuilder = factory.newDocumentBuilder(); doc = docBuilder.newDocument(); diff --git a/org.hl7.fhir.dstu3/pom.xml b/org.hl7.fhir.dstu3/pom.xml index ecd7048fa..3ec2b4c55 100644 --- a/org.hl7.fhir.dstu3/pom.xml +++ b/org.hl7.fhir.dstu3/pom.xml @@ -5,7 +5,7 @@ <parent> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>org.hl7.fhir.core</artifactId> - <version>6.3.21-SNAPSHOT</version> + <version>6.3.23-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> diff --git a/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/elementmodel/XmlParser.java b/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/elementmodel/XmlParser.java index 805db40c1..3e160f815 100644 --- a/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/elementmodel/XmlParser.java +++ b/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/elementmodel/XmlParser.java @@ -1,33 +1,33 @@ package org.hl7.fhir.dstu3.elementmodel; -/* - Copyright (c) 2011+, HL7, Inc. - All rights reserved. - - Redistribution and use in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name of HL7 nor the names of its contributors may be used to - endorse or promote products derived from this software without specific - prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - POSSIBILITY OF SUCH DAMAGE. - - */ +/* + Copyright (c) 2011+, HL7, Inc. + All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of HL7 nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + */ @@ -109,7 +109,7 @@ public class XmlParser extends ParserBase { factory.setNamespaceAware(true); if (policy == ValidationPolicy.EVERYTHING) { // use a slower parser that keeps location data - TransformerFactory transformerFactory = TransformerFactory.newInstance(); + TransformerFactory transformerFactory = XMLUtil.newXXEProtectedTransformerFactory(); Transformer nullTransformer = transformerFactory.newTransformer(); DocumentBuilder docBuilder = factory.newDocumentBuilder(); doc = docBuilder.newDocument(); diff --git a/org.hl7.fhir.r4/pom.xml b/org.hl7.fhir.r4/pom.xml index 6bf991ee8..759f80fb5 100644 --- a/org.hl7.fhir.r4/pom.xml +++ b/org.hl7.fhir.r4/pom.xml @@ -5,7 +5,7 @@ <parent> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>org.hl7.fhir.core</artifactId> - <version>6.3.21-SNAPSHOT</version> + <version>6.3.23-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> diff --git a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/elementmodel/XmlParser.java b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/elementmodel/XmlParser.java index 8c49d9dff..c6675bc87 100644 --- a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/elementmodel/XmlParser.java +++ b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/elementmodel/XmlParser.java @@ -108,7 +108,7 @@ public class XmlParser extends ParserBase { factory.setNamespaceAware(true); if (policy == ValidationPolicy.EVERYTHING) { // use a slower parser that keeps location data - TransformerFactory transformerFactory = TransformerFactory.newInstance(); + TransformerFactory transformerFactory = XMLUtil.newXXEProtectedTransformerFactory(); Transformer nullTransformer = transformerFactory.newTransformer(); DocumentBuilder docBuilder = factory.newDocumentBuilder(); doc = docBuilder.newDocument(); diff --git a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/fhirpath/FHIRPathEngine.java b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/fhirpath/FHIRPathEngine.java index 1ce4f9546..3ca6de5b9 100644 --- a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/fhirpath/FHIRPathEngine.java +++ b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/fhirpath/FHIRPathEngine.java @@ -3658,23 +3658,22 @@ public class FHIRPathEngine { case LowBoundary: case HighBoundary: { - checkContextContinuous(focus, exp.getFunction().toCode(), exp); + checkContextContinuous(focus, exp.getFunction().toCode(), exp, true); if (paramTypes.size() > 0) { checkParamTypes(exp, exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer)); } - if (focus.hasType("decimal") - && (focus.hasType("date") || focus.hasType("datetime") || focus.hasType("instant"))) { - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Decimal, TypeDetails.FP_DateTime); - } else if (focus.hasType("decimal")) { - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Decimal); + if ((focus.hasType("date") || focus.hasType("datetime") || focus.hasType("instant"))) { + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Decimal, TypeDetails.FP_DateTime); + } else if (focus.hasType("decimal") || focus.hasType("integer")) { + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Decimal); } else { - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_DateTime); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_DateTime); } } case Precision: { - checkContextContinuous(focus, exp.getFunction().toCode(), exp); - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer); + checkContextContinuous(focus, exp.getFunction().toCode(), exp, false); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer); } case Custom: { @@ -3778,9 +3777,8 @@ public class FHIRPathEngine { } } - private void checkContextContinuous(TypeDetails focus, String name, ExpressionNode expr) throws PathEngineException { - if (!focus.hasNoTypes() && !focus.hasType("decimal") && !focus.hasType("date") && !focus.hasType("dateTime") - && !focus.hasType("time") && !focus.hasType("Quantity")) { + private void checkContextContinuous(TypeDetails focus, String name, ExpressionNode expr, boolean allowInteger) throws PathEngineException { + if (!focus.hasNoTypes() && !focus.hasType("decimal") && !focus.hasType("date") && !focus.hasType("dateTime") && !focus.hasType("time") && !focus.hasType("Quantity") && !(allowInteger && focus.hasType("integer"))) { throw makeException(expr, I18nConstants.FHIRPATH_CONTINUOUS_ONLY, name, focus.describe()); } } @@ -4223,42 +4221,43 @@ public class FHIRPathEngine { if (focus.size() > 1) { throw makeExceptionPlural(focus.size(), expr, I18nConstants.FHIRPATH_FOCUS, "lowBoundary", focus.size()); } - int precision = 0; + Integer precision = null; if (expr.getParameters().size() > 0) { List<Base> n1 = execute(context, focus, expr.getParameters().get(0), true); if (n1.size() != 1) { - throw makeException(expr, I18nConstants.FHIRPATH_WRONG_PARAM_TYPE, "lowBoundary", "0", "Multiple Values", - "integer"); + throw makeException(expr, I18nConstants.FHIRPATH_WRONG_PARAM_TYPE, "lowBoundary", "0", "Multiple Values", "integer"); } precision = Integer.parseInt(n1.get(0).primitiveValue()); } - + Base base = focus.get(0); List<Base> result = new ArrayList<Base>(); - + if (base.hasType("decimal")) { - result - .add(new DecimalType(Utilities.lowBoundaryForDecimal(base.primitiveValue(), precision == 0 ? 8 : precision))); + if (precision == null || (precision >= 0 && precision < 17)) { + result.add(new DecimalType(Utilities.lowBoundaryForDecimal(base.primitiveValue(), precision == null ? 8 : precision))); + } + } else if (base.hasType("integer")) { + if (precision == null || (precision >= 0 && precision < 17)) { + result.add(new DecimalType(Utilities.lowBoundaryForDecimal(base.primitiveValue(), precision == null ? 8 : precision))); + } } else if (base.hasType("date")) { - result - .add(new DateTimeType(Utilities.lowBoundaryForDate(base.primitiveValue(), precision == 0 ? 10 : precision))); + result.add(new DateTimeType(Utilities.lowBoundaryForDate(base.primitiveValue(), precision == null ? 10 : precision))); } else if (base.hasType("dateTime")) { - result - .add(new DateTimeType(Utilities.lowBoundaryForDate(base.primitiveValue(), precision == 0 ? 17 : precision))); + result.add(new DateTimeType(Utilities.lowBoundaryForDate(base.primitiveValue(), precision == null ? 17 : precision))); } else if (base.hasType("time")) { - result.add(new TimeType(Utilities.lowBoundaryForTime(base.primitiveValue(), precision == 0 ? 9 : precision))); + result.add(new TimeType(Utilities.lowBoundaryForTime(base.primitiveValue(), precision == null ? 9 : precision))); } else if (base.hasType("Quantity")) { String value = getNamedValue(base, "value"); Base v = base.copy(); - v.setProperty("value", new DecimalType(Utilities.lowBoundaryForDecimal(value, precision == 0 ? 8 : precision))); + v.setProperty("value", new DecimalType(Utilities.lowBoundaryForDecimal(value, precision == null ? 8 : precision))); result.add(v); } else { - makeException(expr, I18nConstants.FHIRPATH_WRONG_PARAM_TYPE, "sqrt", "(focus)", base.fhirType(), - "decimal or date"); + makeException(expr, I18nConstants.FHIRPATH_WRONG_PARAM_TYPE, "sqrt", "(focus)", base.fhirType(), "decimal or date"); } return result; } - + private List<Base> funcHighBoundary(ExecutionContext context, List<Base> focus, ExpressionNode expr) { if (focus.size() == 0) { return makeNull(); @@ -4266,41 +4265,43 @@ public class FHIRPathEngine { if (focus.size() > 1) { throw makeExceptionPlural(focus.size(), expr, I18nConstants.FHIRPATH_FOCUS, "highBoundary", focus.size()); } - int precision = 0; + Integer precision = null; if (expr.getParameters().size() > 0) { List<Base> n1 = execute(context, focus, expr.getParameters().get(0), true); if (n1.size() != 1) { - throw makeException(expr, I18nConstants.FHIRPATH_WRONG_PARAM_TYPE, "lowBoundary", "0", "Multiple Values", - "integer"); + throw makeException(expr, I18nConstants.FHIRPATH_WRONG_PARAM_TYPE, "lowBoundary", "0", "Multiple Values", "integer"); } precision = Integer.parseInt(n1.get(0).primitiveValue()); } - + + Base base = focus.get(0); List<Base> result = new ArrayList<Base>(); if (base.hasType("decimal")) { - result.add( - new DecimalType(Utilities.highBoundaryForDecimal(base.primitiveValue(), precision == 0 ? 8 : precision))); + if (precision == null || (precision >= 0 && precision < 17)) { + result.add(new DecimalType(Utilities.highBoundaryForDecimal(base.primitiveValue(), precision == null ? 8 : precision))); + } + } else if (base.hasType("integer")) { + if (precision == null || (precision >= 0 && precision < 17)) { + result.add(new DecimalType(Utilities.highBoundaryForDecimal(base.primitiveValue(), precision == null ? 8 : precision))); + } } else if (base.hasType("date")) { - result - .add(new DateTimeType(Utilities.highBoundaryForDate(base.primitiveValue(), precision == 0 ? 10 : precision))); + result.add(new DateTimeType(Utilities.highBoundaryForDate(base.primitiveValue(), precision == null ? 10 : precision))); } else if (base.hasType("dateTime")) { - result - .add(new DateTimeType(Utilities.highBoundaryForDate(base.primitiveValue(), precision == 0 ? 17 : precision))); + result.add(new DateTimeType(Utilities.highBoundaryForDate(base.primitiveValue(), precision == null ? 17 : precision))); } else if (base.hasType("time")) { - result.add(new TimeType(Utilities.highBoundaryForTime(base.primitiveValue(), precision == 0 ? 9 : precision))); + result.add(new TimeType(Utilities.highBoundaryForTime(base.primitiveValue(), precision == null ? 9 : precision))); } else if (base.hasType("Quantity")) { String value = getNamedValue(base, "value"); Base v = base.copy(); - v.setProperty("value", new DecimalType(Utilities.highBoundaryForDecimal(value, precision == 0 ? 8 : precision))); + v.setProperty("value", new DecimalType(Utilities.highBoundaryForDecimal(value, precision == null ? 8 : precision))); result.add(v); } else { - makeException(expr, I18nConstants.FHIRPATH_WRONG_PARAM_TYPE, "sqrt", "(focus)", base.fhirType(), - "decimal or date"); + makeException(expr, I18nConstants.FHIRPATH_WRONG_PARAM_TYPE, "sqrt", "(focus)", base.fhirType(), "decimal or date"); } return result; } - + private List<Base> funcPrecision(ExecutionContext context, List<Base> focus, ExpressionNode expr) { if (focus.size() != 1) { throw makeExceptionPlural(focus.size(), expr, I18nConstants.FHIRPATH_FOCUS, "highBoundary", focus.size()); diff --git a/org.hl7.fhir.r4b/pom.xml b/org.hl7.fhir.r4b/pom.xml index d2c800dfc..422404744 100644 --- a/org.hl7.fhir.r4b/pom.xml +++ b/org.hl7.fhir.r4b/pom.xml @@ -5,7 +5,7 @@ <parent> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>org.hl7.fhir.core</artifactId> - <version>6.3.21-SNAPSHOT</version> + <version>6.3.23-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> diff --git a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/elementmodel/XmlParser.java b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/elementmodel/XmlParser.java index c36fb2ba6..0dacab064 100644 --- a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/elementmodel/XmlParser.java +++ b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/elementmodel/XmlParser.java @@ -131,7 +131,7 @@ public class XmlParser extends ParserBase { stream.reset(); } // use a slower parser that keeps location data - TransformerFactory transformerFactory = TransformerFactory.newInstance(); + TransformerFactory transformerFactory = XMLUtil.newXXEProtectedTransformerFactory(); Transformer nullTransformer = transformerFactory.newTransformer(); DocumentBuilder docBuilder = factory.newDocumentBuilder(); doc = docBuilder.newDocument(); @@ -233,6 +233,8 @@ public class XmlParser extends ParserBase { return "sdtc:"; if (ns.equals("urn:ihe:pharm")) return "pharm:"; + if (ns.equals("http://ns.electronichealth.net.au/Ci/Cda/Extensions/3.0")) + return "ext:"; return "?:"; } diff --git a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/fhirpath/FHIRPathEngine.java b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/fhirpath/FHIRPathEngine.java index 3c883d44a..60d32662d 100644 --- a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/fhirpath/FHIRPathEngine.java +++ b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/fhirpath/FHIRPathEngine.java @@ -3660,23 +3660,22 @@ public class FHIRPathEngine { case LowBoundary: case HighBoundary: { - checkContextContinuous(focus, exp.getFunction().toCode(), exp); + checkContextContinuous(focus, exp.getFunction().toCode(), exp, true); if (paramTypes.size() > 0) { checkParamTypes(exp, exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer)); } - if (focus.hasType("decimal") - && (focus.hasType("date") || focus.hasType("datetime") || focus.hasType("instant"))) { - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Decimal, TypeDetails.FP_DateTime); - } else if (focus.hasType("decimal")) { - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Decimal); + if ((focus.hasType("date") || focus.hasType("datetime") || focus.hasType("instant"))) { + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Decimal, TypeDetails.FP_DateTime); + } else if (focus.hasType("decimal") || focus.hasType("integer")) { + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Decimal); } else { - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_DateTime); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_DateTime); } } case Precision: { - checkContextContinuous(focus, exp.getFunction().toCode(), exp); - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer); + checkContextContinuous(focus, exp.getFunction().toCode(), exp, false); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer); } case Custom: { @@ -3780,9 +3779,8 @@ public class FHIRPathEngine { } } - private void checkContextContinuous(TypeDetails focus, String name, ExpressionNode expr) throws PathEngineException { - if (!focus.hasNoTypes() && !focus.hasType("decimal") && !focus.hasType("date") && !focus.hasType("dateTime") - && !focus.hasType("time") && !focus.hasType("Quantity")) { + private void checkContextContinuous(TypeDetails focus, String name, ExpressionNode expr, boolean allowInteger) throws PathEngineException { + if (!focus.hasNoTypes() && !focus.hasType("decimal") && !focus.hasType("date") && !focus.hasType("dateTime") && !focus.hasType("time") && !focus.hasType("Quantity") && !(allowInteger && focus.hasType("integer"))) { throw makeException(expr, I18nConstants.FHIRPATH_CONTINUOUS_ONLY, name, focus.describe()); } } @@ -4233,42 +4231,43 @@ public class FHIRPathEngine { if (focus.size() > 1) { throw makeExceptionPlural(focus.size(), expr, I18nConstants.FHIRPATH_FOCUS, "lowBoundary", focus.size()); } - int precision = 0; + Integer precision = null; if (expr.getParameters().size() > 0) { List<Base> n1 = execute(context, focus, expr.getParameters().get(0), true); if (n1.size() != 1) { - throw makeException(expr, I18nConstants.FHIRPATH_WRONG_PARAM_TYPE, "lowBoundary", "0", "Multiple Values", - "integer"); + throw makeException(expr, I18nConstants.FHIRPATH_WRONG_PARAM_TYPE, "lowBoundary", "0", "Multiple Values", "integer"); } precision = Integer.parseInt(n1.get(0).primitiveValue()); } - + Base base = focus.get(0); List<Base> result = new ArrayList<Base>(); - + if (base.hasType("decimal")) { - result - .add(new DecimalType(Utilities.lowBoundaryForDecimal(base.primitiveValue(), precision == 0 ? 8 : precision))); + if (precision == null || (precision >= 0 && precision < 17)) { + result.add(new DecimalType(Utilities.lowBoundaryForDecimal(base.primitiveValue(), precision == null ? 8 : precision))); + } + } else if (base.hasType("integer")) { + if (precision == null || (precision >= 0 && precision < 17)) { + result.add(new DecimalType(Utilities.lowBoundaryForDecimal(base.primitiveValue(), precision == null ? 8 : precision))); + } } else if (base.hasType("date")) { - result - .add(new DateTimeType(Utilities.lowBoundaryForDate(base.primitiveValue(), precision == 0 ? 10 : precision))); + result.add(new DateTimeType(Utilities.lowBoundaryForDate(base.primitiveValue(), precision == null ? 10 : precision))); } else if (base.hasType("dateTime")) { - result - .add(new DateTimeType(Utilities.lowBoundaryForDate(base.primitiveValue(), precision == 0 ? 17 : precision))); + result.add(new DateTimeType(Utilities.lowBoundaryForDate(base.primitiveValue(), precision == null ? 17 : precision))); } else if (base.hasType("time")) { - result.add(new TimeType(Utilities.lowBoundaryForTime(base.primitiveValue(), precision == 0 ? 9 : precision))); + result.add(new TimeType(Utilities.lowBoundaryForTime(base.primitiveValue(), precision == null ? 9 : precision))); } else if (base.hasType("Quantity")) { String value = getNamedValue(base, "value"); Base v = base.copy(); - v.setProperty("value", new DecimalType(Utilities.lowBoundaryForDecimal(value, precision == 0 ? 8 : precision))); + v.setProperty("value", new DecimalType(Utilities.lowBoundaryForDecimal(value, precision == null ? 8 : precision))); result.add(v); } else { - makeException(expr, I18nConstants.FHIRPATH_WRONG_PARAM_TYPE, "sqrt", "(focus)", base.fhirType(), - "decimal or date"); + makeException(expr, I18nConstants.FHIRPATH_WRONG_PARAM_TYPE, "sqrt", "(focus)", base.fhirType(), "decimal or date"); } return result; } - + private List<Base> funcHighBoundary(ExecutionContext context, List<Base> focus, ExpressionNode expr) { if (focus.size() == 0) { return makeNull(); @@ -4276,41 +4275,43 @@ public class FHIRPathEngine { if (focus.size() > 1) { throw makeExceptionPlural(focus.size(), expr, I18nConstants.FHIRPATH_FOCUS, "highBoundary", focus.size()); } - int precision = 0; + Integer precision = null; if (expr.getParameters().size() > 0) { List<Base> n1 = execute(context, focus, expr.getParameters().get(0), true); if (n1.size() != 1) { - throw makeException(expr, I18nConstants.FHIRPATH_WRONG_PARAM_TYPE, "lowBoundary", "0", "Multiple Values", - "integer"); + throw makeException(expr, I18nConstants.FHIRPATH_WRONG_PARAM_TYPE, "lowBoundary", "0", "Multiple Values", "integer"); } precision = Integer.parseInt(n1.get(0).primitiveValue()); } - + + Base base = focus.get(0); List<Base> result = new ArrayList<Base>(); if (base.hasType("decimal")) { - result.add( - new DecimalType(Utilities.highBoundaryForDecimal(base.primitiveValue(), precision == 0 ? 8 : precision))); + if (precision == null || (precision >= 0 && precision < 17)) { + result.add(new DecimalType(Utilities.highBoundaryForDecimal(base.primitiveValue(), precision == null ? 8 : precision))); + } + } else if (base.hasType("integer")) { + if (precision == null || (precision >= 0 && precision < 17)) { + result.add(new DecimalType(Utilities.highBoundaryForDecimal(base.primitiveValue(), precision == null ? 8 : precision))); + } } else if (base.hasType("date")) { - result - .add(new DateTimeType(Utilities.highBoundaryForDate(base.primitiveValue(), precision == 0 ? 10 : precision))); + result.add(new DateTimeType(Utilities.highBoundaryForDate(base.primitiveValue(), precision == null ? 10 : precision))); } else if (base.hasType("dateTime")) { - result - .add(new DateTimeType(Utilities.highBoundaryForDate(base.primitiveValue(), precision == 0 ? 17 : precision))); + result.add(new DateTimeType(Utilities.highBoundaryForDate(base.primitiveValue(), precision == null ? 17 : precision))); } else if (base.hasType("time")) { - result.add(new TimeType(Utilities.highBoundaryForTime(base.primitiveValue(), precision == 0 ? 9 : precision))); + result.add(new TimeType(Utilities.highBoundaryForTime(base.primitiveValue(), precision == null ? 9 : precision))); } else if (base.hasType("Quantity")) { String value = getNamedValue(base, "value"); Base v = base.copy(); - v.setProperty("value", new DecimalType(Utilities.highBoundaryForDecimal(value, precision == 0 ? 8 : precision))); + v.setProperty("value", new DecimalType(Utilities.highBoundaryForDecimal(value, precision == null ? 8 : precision))); result.add(v); } else { - makeException(expr, I18nConstants.FHIRPATH_WRONG_PARAM_TYPE, "sqrt", "(focus)", base.fhirType(), - "decimal or date"); + makeException(expr, I18nConstants.FHIRPATH_WRONG_PARAM_TYPE, "sqrt", "(focus)", base.fhirType(), "decimal or date"); } return result; } - + private List<Base> funcPrecision(ExecutionContext context, List<Base> focus, ExpressionNode expr) { if (focus.size() != 1) { throw makeExceptionPlural(focus.size(), expr, I18nConstants.FHIRPATH_FOCUS, "highBoundary", focus.size()); diff --git a/org.hl7.fhir.r5/pom.xml b/org.hl7.fhir.r5/pom.xml index e6064cc73..77de81010 100644 --- a/org.hl7.fhir.r5/pom.xml +++ b/org.hl7.fhir.r5/pom.xml @@ -5,7 +5,7 @@ <parent> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>org.hl7.fhir.core</artifactId> - <version>6.3.21-SNAPSHOT</version> + <version>6.3.23-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/StructureDefinitionHacker.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/StructureDefinitionHacker.java index 470b90653..85eb906ab 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/StructureDefinitionHacker.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/StructureDefinitionHacker.java @@ -62,6 +62,18 @@ public class StructureDefinitionHacker { } } } + if (VersionUtilities.isR4Ver(version) && "http://hl7.org/fhir/StructureDefinition/ExplanationOfBenefit".equals(sd.getUrl())) { + for (ElementDefinition ed : sd.getSnapshot().getElement()) { + if (ed.hasBinding() && "http://terminology.hl7.org/CodeSystem/processpriority".equals(ed.getBinding().getValueSet())) { + ed.getBinding().setValueSet("http://hl7.org/fhir/ValueSet/process-priority"); + } + } + for (ElementDefinition ed : sd.getDifferential().getElement()) { + if (ed.hasBinding() && "http://terminology.hl7.org/CodeSystem/processpriority".equals(ed.getBinding().getValueSet())) { + ed.getBinding().setValueSet("http://hl7.org/fhir/ValueSet/process-priority"); + } + } + } if (sd.getUrl().startsWith("http://hl7.org/fhir/uv/subscriptions-backport")) { for (ElementDefinition ed : sd.getDifferential().getElement()) { fixMarkdownR4BURLs(ed); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/profile/MappingAssistant.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/profile/MappingAssistant.java new file mode 100644 index 000000000..42daed659 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/profile/MappingAssistant.java @@ -0,0 +1,227 @@ +package org.hl7.fhir.r5.conformance.profile; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.hl7.fhir.exceptions.DefinitionException; +import org.hl7.fhir.r5.model.Element; +import org.hl7.fhir.r5.model.ElementDefinition; +import org.hl7.fhir.r5.model.StructureDefinition; +import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionMappingComponent; +import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionMappingComponent; +import org.hl7.fhir.r5.utils.ToolingExtensions; +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.VersionUtilities; +import org.hl7.fhir.utilities.i18n.I18nConstants; + +public class MappingAssistant { + + + public enum MappingMergeModeOption { + DUPLICATE, // if there's more than one mapping for the same URI, just keep them all + IGNORE, // if there's more than one, keep the first + OVERWRITE, // if there's opre than, keep the last + APPEND, // if there's more than one, append them with ';' + } + + private MappingMergeModeOption mappingMergeMode = MappingMergeModeOption.APPEND; + private StructureDefinition base; + private StructureDefinition derived; + + private List<StructureDefinitionMappingComponent> masterList= new ArrayList<StructureDefinition.StructureDefinitionMappingComponent>(); + private Map<String, String> renames = new HashMap<>(); + private String version; + + public MappingAssistant(MappingMergeModeOption mappingMergeMode, StructureDefinition base, StructureDefinition derived, String version) { + this.mappingMergeMode = mappingMergeMode; + this.base = base; + this.derived = derived; + this.version = version; + + // figure out where we're going to be: + // mappings declared in derived get priority; we do not change them either + for (StructureDefinitionMappingComponent m : derived.getMapping()) { + masterList.add(m); + if (!isSuppressed(m)) { + m.setUserData("private-marked-as-derived", true); + } + } + + // now, look at the base profile. If mappings in there match one in the derived, then we use that, otherwise, we add it to the list + for (StructureDefinitionMappingComponent m : base.getMapping()) { + StructureDefinitionMappingComponent md = findMatchInDerived(m); + if (md == null) { + if (nameExists(m.getIdentity())) { + int i = 1; + String n = m.getIdentity() + i; + while (nameExists(n)) { + i++; + n = m.getIdentity() + i; + } + renames.put(m.getIdentity(), n); + masterList.add(m.copy().setName(n)); + } else { + masterList.add(m.copy()); + } + } else { + if (!md.hasName() && m.hasName()) { + md.setName(m.getName()); + } + if (!md.hasUri() && m.hasUri()) { + md.setUri(m.getUri()); + } + if (!md.hasComment() && m.hasComment()) { + md.setComment(m.getComment()); + } + if (!m.getIdentity().equals(md.getIdentity())) { + renames.put(m.getIdentity(), md.getIdentity()); + } + } + } + } + + private boolean nameExists(String n) { + for (StructureDefinitionMappingComponent md : masterList) { + if (n.equals(md.getIdentity())) { + return true; + } + } + return false; + } + + private StructureDefinitionMappingComponent findMatchInDerived(StructureDefinitionMappingComponent m) { + for (StructureDefinitionMappingComponent md : derived.getMapping()) { + // if the URIs match, they match, irregardless of anything else + if (md.hasUri() && m.hasUri() && md.getUri().equals(m.getUri())) { + return md; + } + // if the codes match + if (md.hasIdentity() && m.hasIdentity() && md.getIdentity().equals(m.getIdentity())) { + // the names have to match if present + if (!md.hasName() || !m.hasName() || md.getName().equals(m.getName())) { + return md; + } + } + + } + return null; + } + + public void update() { + + Set<StructureDefinitionMappingComponent> usedList= new HashSet<StructureDefinition.StructureDefinitionMappingComponent>(); + for (ElementDefinition ed : derived.getSnapshot().getElement()) { + for (ElementDefinitionMappingComponent m : ed.getMapping()) { + StructureDefinitionMappingComponent def = findDefinition(m.getIdentity()); + if (def != null) { + usedList.add(def); + } else { + // not sure what to do? + } + } + } + + derived.getMapping().clear(); + for (StructureDefinitionMappingComponent t : masterList) { + if (usedList.contains(t) || t.hasUserData("private-marked-as-derived")) { + derived.getMapping().add(t); + } + } + } + + public void merge(ElementDefinition base, ElementDefinition derived) { + List<ElementDefinitionMappingComponent> list = new ArrayList<>(); + addMappings(list, base.getMapping(), renames); + if (derived.hasMapping()) { + addMappings(list, derived.getMapping(), null); + } + derived.setMapping(list); + + // trim anything + for (ElementDefinitionMappingComponent m : base.getMapping()) { + if (m.hasMap()) { + m.setMap(m.getMap().trim()); + } + } + + } + + private void addMappings(List<ElementDefinitionMappingComponent> destination, List<ElementDefinitionMappingComponent> source, Map<String, String> renames2) { + for (ElementDefinitionMappingComponent s : source) { + if (!isSuppressed(s)) { + String name = s.getIdentity(); + if (!isSuppressed(name)) { + if (renames2 != null && renames2.containsKey(name)) { + name = renames2.get(name); + } + + boolean found = false; + for (ElementDefinitionMappingComponent d : destination) { + if (compareMaps(name, s, d)) { + found = true; + d.setUserData(ProfileUtilities.UD_DERIVATION_EQUALS, true); + break; + } + } + if (!found) { + destination.add(s.setIdentity(name)); + } + } + } + } + } + + private boolean isSuppressed(String name) { + StructureDefinitionMappingComponent m = findDefinition(name); + return m != null && isSuppressed(m); + } + + private boolean isSuppressed(Element s) { + return ToolingExtensions.readBoolExtension(s, ToolingExtensions.EXT_SUPPRESSED); + } + + private StructureDefinitionMappingComponent findDefinition(String name) { + for (StructureDefinitionMappingComponent t : masterList) { + if (t.getIdentity().equals(name)) { + return t; + } + } + return null; + } + + private boolean compareMaps(String name, ElementDefinitionMappingComponent s, ElementDefinitionMappingComponent d) { + + if (d.getIdentity().equals(name) && d.getMap().equals(s.getMap())) { + return true; + } + if (VersionUtilities.isR5Plus(version)) { + if (d.getIdentity().equals(name)) { + switch (mappingMergeMode) { + case APPEND: + if (!Utilities.splitStrings(d.getMap(), "\\,").contains(s.getMap())) { + d.setMap(d.getMap()+","+s.getMap()); + } + return true; + case DUPLICATE: + return false; + case IGNORE: + d.setMap(s.getMap()); + return true; + case OVERWRITE: + return true; + default: + return false; + } + } else { + return false; + } + } else { + return false; + } + } + +} diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/profile/ProfilePathProcessor.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/profile/ProfilePathProcessor.java index 9eef03c70..6a939f95a 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/profile/ProfilePathProcessor.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/profile/ProfilePathProcessor.java @@ -128,7 +128,7 @@ public class ProfilePathProcessor { } - protected static void processPaths(ProfileUtilities profileUtilities, StructureDefinition base, StructureDefinition derived, String url, String webUrl, StructureDefinition.StructureDefinitionDifferentialComponent differential, StructureDefinition.StructureDefinitionSnapshotComponent baseSnapshot) { + protected static void processPaths(ProfileUtilities profileUtilities, StructureDefinition base, StructureDefinition derived, String url, String webUrl, StructureDefinition.StructureDefinitionDifferentialComponent differential, StructureDefinition.StructureDefinitionSnapshotComponent baseSnapshot, MappingAssistant mapHelper) { ProfilePathProcessorState cursors = new ProfilePathProcessorState( baseSnapshot, @@ -152,16 +152,17 @@ public class ProfilePathProcessor { .withRedirector(new ArrayList<ElementRedirection>()) .withSourceStructureDefinition(base) .withDerived(derived) - .withSlicing(new PathSlicingParams()).processPaths(cursors); + .withSlicing(new PathSlicingParams()).processPaths(cursors, mapHelper); } /** * @param cursors + * @param mapHelper * @throws DefinitionException, FHIRException * @throws Exception */ - private ElementDefinition processPaths(final ProfilePathProcessorState cursors) throws FHIRException { + private ElementDefinition processPaths(final ProfilePathProcessorState cursors, MappingAssistant mapHelper) throws FHIRException { debugProcessPathsEntry(cursors); ElementDefinition res = null; List<TypeSlice> typeList = new ArrayList<>(); @@ -177,13 +178,13 @@ public class ProfilePathProcessor { // in the simple case, source is not sliced. if (!currentBase.hasSlicing() || currentBasePath.equals(getSlicing().getPath())) { - ElementDefinition currentRes = processSimplePath(currentBase, currentBasePath, diffMatches, typeList, cursors); + ElementDefinition currentRes = processSimplePath(currentBase, currentBasePath, diffMatches, typeList, cursors, mapHelper); if (res == null) { res = currentRes; } } else { - processPathWithSlicedBase(currentBase, currentBasePath, diffMatches, typeList, cursors); + processPathWithSlicedBase(currentBase, currentBasePath, diffMatches, typeList, cursors, mapHelper); } } @@ -238,26 +239,26 @@ public class ProfilePathProcessor { final String currentBasePath, final List<ElementDefinition> diffMatches, final List<TypeSlice> typeList, - final ProfilePathProcessorState cursors) throws FHIRException { + final ProfilePathProcessorState cursors, MappingAssistant mapHelper) throws FHIRException { ElementDefinition res = null; // the differential doesn't say anything about this item // so we just copy it in if (diffMatches.isEmpty()) - processSimplePathWithEmptyDiffMatches(currentBase, currentBasePath, diffMatches, cursors); + processSimplePathWithEmptyDiffMatches(currentBase, currentBasePath, diffMatches, cursors, mapHelper); // one matching element in the differential else if (oneMatchingElementInDifferential(getSlicing().isDone(), currentBasePath, diffMatches)) - res = processSimplePathWithOneMatchingElementInDifferential(currentBase, currentBasePath, diffMatches, cursors); + res = processSimplePathWithOneMatchingElementInDifferential(currentBase, currentBasePath, diffMatches, cursors, mapHelper); else if (profileUtilities.diffsConstrainTypes(diffMatches, currentBasePath, typeList)) - processSimplePathWhereDiffsConstrainTypes(currentBasePath, diffMatches, typeList, cursors); + processSimplePathWhereDiffsConstrainTypes(currentBasePath, diffMatches, typeList, cursors, mapHelper); else - processSimplePathDefault(currentBase, currentBasePath, diffMatches, cursors); + processSimplePathDefault(currentBase, currentBasePath, diffMatches, cursors, mapHelper); return res; } - private void processSimplePathDefault(ElementDefinition currentBase, String currentBasePath, List<ElementDefinition> diffMatches, ProfilePathProcessorState cursors) { + private void processSimplePathDefault(ElementDefinition currentBase, String currentBasePath, List<ElementDefinition> diffMatches, ProfilePathProcessorState cursors, MappingAssistant mapHelper) { // ok, the differential slices the item. Let's check our pre-conditions to ensure that this is correct if (!profileUtilities.unbounded(currentBase) && !profileUtilities.isSlicedToOneOnly(diffMatches.get(0))) // you can only slice an element that doesn't repeat if the sum total of your slices is limited to 1 @@ -280,7 +281,7 @@ public class ProfilePathProcessor { .withBaseLimit(newBaseLimit) .withDiffLimit(newDiffLimit) .withProfileName(getProfileName() + profileUtilities.pathTail(diffMatches, 0)).withSlicing(new PathSlicingParams(true, null, null)) - .processPaths(new ProfilePathProcessorState(cursors.base, cursors.baseCursor, newDiffCursor, cursors.contextName, cursors.resultPathBase)); + .processPaths(new ProfilePathProcessorState(cursors.base, cursors.baseCursor, newDiffCursor, cursors.contextName, cursors.resultPathBase), mapHelper); if (e == null) throw new FHIRException(profileUtilities.getContext().formatMessage(I18nConstants.DID_NOT_FIND_SINGLE_SLICE_, diffMatches.get(0).getPath())); e.setSlicing(diffMatches.get(0).getSlicing()); @@ -322,7 +323,7 @@ public class ProfilePathProcessor { // differential - if the first one in the list has a name, we'll process it. Else we'll treat it as the base definition of the slice. if (!diffMatches.get(0).hasSliceName()) { - profileUtilities.updateFromDefinition(outcome, diffMatches.get(0), getProfileName(), isTrimDifferential(), getUrl(),getSourceStructureDefinition(), getDerived(), diffPath(diffMatches.get(0))); + profileUtilities.updateFromDefinition(outcome, diffMatches.get(0), getProfileName(), isTrimDifferential(), getUrl(),getSourceStructureDefinition(), getDerived(), diffPath(diffMatches.get(0)), mapHelper); profileUtilities.removeStatusExtensions(outcome); if (!outcome.hasContentReference() && !outcome.hasType() && outcome.getPath().contains(".")) { throw new DefinitionException(profileUtilities.getContext().formatMessage(I18nConstants.NOT_DONE_YET)); @@ -350,7 +351,7 @@ public class ProfilePathProcessor { .withContextPathSource(currentBasePath) .withContextPathTarget(outcome.getPath()).withSlicing(new PathSlicingParams()) /* starting again on the data type, but skip the root */ . processPaths(new ProfilePathProcessorState(dt.getSnapshot(), 1 /* starting again on the data type, but skip the root */, start, - cursors.contextName, cursors.resultPathBase)); + cursors.contextName, cursors.resultPathBase), mapHelper); } } start++; @@ -375,7 +376,7 @@ public class ProfilePathProcessor { .withDiffLimit(newDiffLimit) .withProfileName(getProfileName() + profileUtilities.pathTail(diffMatches, i)) .withSlicing(new PathSlicingParams(true, slicerElement, null)) - .processPaths(new ProfilePathProcessorState(cursors.base, cursors.baseCursor, newDiffCursor, cursors.contextName, cursors.resultPathBase)); + .processPaths(new ProfilePathProcessorState(cursors.base, cursors.baseCursor, newDiffCursor, cursors.contextName, cursors.resultPathBase), mapHelper); } // ok, done with that - next in the base list cursors.baseCursor = newBaseLimit + 1; @@ -400,7 +401,7 @@ public class ProfilePathProcessor { return Base.compareDeep(s1.getDiscriminator(), s2.getDiscriminator(), false); } - private void processSimplePathWhereDiffsConstrainTypes(String currentBasePath, List<ElementDefinition> diffMatches, List<TypeSlice> typeList, ProfilePathProcessorState cursors) { + private void processSimplePathWhereDiffsConstrainTypes(String currentBasePath, List<ElementDefinition> diffMatches, List<TypeSlice> typeList, ProfilePathProcessorState cursors, MappingAssistant mapHelper) { int start = 0; int newBaseLimit = profileUtilities.findEndOfElement(cursors.base, cursors.baseCursor); int newDiffCursor = getDifferential().getElement().indexOf(diffMatches.get(0)); @@ -498,7 +499,7 @@ public class ProfilePathProcessor { .withProfileName(getProfileName() + profileUtilities.pathTail(diffMatches, 0)) .withSlicing(new PathSlicingParams(true, null, null)) .processPaths(new ProfilePathProcessorState(cursors.base, cursors.baseCursor, newDiffCursor, - cursors.contextName, cursors.resultPathBase)); + cursors.contextName, cursors.resultPathBase), mapHelper); if (elementDefinition == null) throw new FHIRException(profileUtilities.getContext().formatMessage(I18nConstants.DID_NOT_FIND_TYPE_ROOT_, path)); // now set up slicing on the e (cause it was wiped by what we called. @@ -531,7 +532,7 @@ public class ProfilePathProcessor { .withDiffLimit(newDiffLimit) .withProfileName(getProfileName() + profileUtilities.pathTail(diffMatches, i)) .withSlicing(new PathSlicingParams(true, elementDefinition, null)) - .processPaths(new ProfilePathProcessorState(cursors.base, cursors.baseCursor, newDiffCursor, cursors.contextName, cursors.resultPathBase)); + .processPaths(new ProfilePathProcessorState(cursors.base, cursors.baseCursor, newDiffCursor, cursors.contextName, cursors.resultPathBase), mapHelper); if (typeList.size() > start + 1) { typeSliceElement.setMin(0); } @@ -576,7 +577,7 @@ public class ProfilePathProcessor { cursors.diffCursor = newDiffLimit + 1; } - private ElementDefinition processSimplePathWithOneMatchingElementInDifferential(ElementDefinition currentBase, String currentBasePath, List<ElementDefinition> diffMatches, ProfilePathProcessorState cursors) { + private ElementDefinition processSimplePathWithOneMatchingElementInDifferential(ElementDefinition currentBase, String currentBasePath, List<ElementDefinition> diffMatches, ProfilePathProcessorState cursors, MappingAssistant mapHelper) { ElementDefinition res; ElementDefinition template = null; if (diffMatches.get(0).hasType() && "Reference".equals(diffMatches.get(0).getType().get(0).getWorkingCode()) && !profileUtilities.isValidType(diffMatches.get(0).getType().get(0), currentBase)) { @@ -684,7 +685,7 @@ public class ProfilePathProcessor { } } } - profileUtilities.updateFromDefinition(outcome, diffMatches.get(0), getProfileName(), isTrimDifferential(), getUrl(), getSourceStructureDefinition(), getDerived(), diffPath(diffMatches.get(0))); + profileUtilities.updateFromDefinition(outcome, diffMatches.get(0), getProfileName(), isTrimDifferential(), getUrl(), getSourceStructureDefinition(), getDerived(), diffPath(diffMatches.get(0)), mapHelper); profileUtilities.removeStatusExtensions(outcome); // if (outcome.getPath().endsWith("[x]") && outcome.getType().size() == 1 && !outcome.getType().get(0).getCode().equals("*") && !diffMatches.get(0).hasSlicing()) // if the base profile allows multiple types, but the profile only allows one, rename it // outcome.setPath(outcome.getPath().substring(0, outcome.getPath().length()-3)+Utilities.capitalize(outcome.getType().get(0).getCode())); @@ -746,7 +747,7 @@ public class ProfilePathProcessor { .withContextPathSource(target.getElement().getPath()) .withContextPathTarget(diffMatches.get(0).getPath()).withRedirector(profileUtilities.redirectorStack(getRedirector(), outcome, currentBasePath)) .withSourceStructureDefinition(target.getSource()) - .withSlicing(new PathSlicingParams()).processPaths(new ProfilePathProcessorState(cursors.base, newBaseCursor, start - 1, cursors.contextName, cursors.resultPathBase)); + .withSlicing(new PathSlicingParams()).processPaths(new ProfilePathProcessorState(cursors.base, newBaseCursor, start - 1, cursors.contextName, cursors.resultPathBase), mapHelper); } else { final int newBaseCursor = cursors.base.getElement().indexOf(target.getElement()) + 1; int newBaseLimit = newBaseCursor; @@ -761,7 +762,7 @@ public class ProfilePathProcessor { .withContextPathTarget(diffMatches.get(0).getPath()) .withRedirector(profileUtilities.redirectorStack(getRedirector(), outcome, currentBasePath)) .withSlicing(new PathSlicingParams()).processPaths( - new ProfilePathProcessorState(cursors.base, newBaseCursor, start - 1, cursors.contextName, cursors.resultPathBase)); + new ProfilePathProcessorState(cursors.base, newBaseCursor, start - 1, cursors.contextName, cursors.resultPathBase), mapHelper); } } else { StructureDefinition dt = outcome.getType().size() == 1 ? profileUtilities.getProfileForDataType(outcome.getType().get(0), getWebUrl(), getDerived()) : profileUtilities.getProfileForDataType("Element"); @@ -778,7 +779,7 @@ public class ProfilePathProcessor { .withContextPathSource(diffMatches.get(0).getPath()).withContextPathTarget(outcome.getPath()).withRedirector(new ArrayList<ElementRedirection>()) .withSlicing(new PathSlicingParams()). /* starting again on the data type, but skip the root */ processPaths(new ProfilePathProcessorState(dt.getSnapshot(), 1 /* starting again on the data type, but skip the root */, start, - cursors.contextName, cursors.resultPathBase)); + cursors.contextName, cursors.resultPathBase), mapHelper); } } } @@ -864,7 +865,7 @@ public class ProfilePathProcessor { } - private void processSimplePathWithEmptyDiffMatches(ElementDefinition currentBase, String currentBasePath, List<ElementDefinition> diffMatches, ProfilePathProcessorState cursors) { + private void processSimplePathWithEmptyDiffMatches(ElementDefinition currentBase, String currentBasePath, List<ElementDefinition> diffMatches, ProfilePathProcessorState cursors, MappingAssistant mapHelper) { ElementDefinition outcome = profileUtilities.updateURLs(getUrl(), getWebUrl(), currentBase.copy()); outcome.setPath(profileUtilities.fixedPathDest(getContextPathTarget(), outcome.getPath(), getRedirector(), getContextPathSource())); profileUtilities.updateFromBase(outcome, currentBase, getSourceStructureDefinition().getUrl()); @@ -884,7 +885,7 @@ public class ProfilePathProcessor { // did we implicitly step into a new type? if (baseHasChildren(cursors.base, currentBase)) { // not a new type here - this.incrementDebugIndent().withSlicing(new PathSlicingParams()). processPaths( new ProfilePathProcessorState(cursors.base, cursors.baseCursor + 1, cursors.diffCursor, cursors.contextName, cursors.resultPathBase)); + this.incrementDebugIndent().withSlicing(new PathSlicingParams()). processPaths( new ProfilePathProcessorState(cursors.base, cursors.baseCursor + 1, cursors.diffCursor, cursors.contextName, cursors.resultPathBase), mapHelper); cursors.baseCursor = indexOfFirstNonChild(cursors.base, currentBase, cursors.baseCursor + 1, getBaseLimit()); } else { @@ -933,7 +934,7 @@ public class ProfilePathProcessor { .withRedirector(profileUtilities.redirectorStack(getRedirector(), outcome, currentBasePath)) .withSourceStructureDefinition(tgt.getSource()) .withSlicing(new PathSlicingParams()).processPaths( - new ProfilePathProcessorState(cursors.base, newBaseCursor, start - 1, cursors.contextName, cursors.resultPathBase)); + new ProfilePathProcessorState(cursors.base, newBaseCursor, start - 1, cursors.contextName, cursors.resultPathBase), mapHelper); } else { int newBaseCursor = cursors.base.getElement().indexOf(tgt.getElement()) + 1; int newBaseLimit = newBaseCursor; @@ -948,7 +949,7 @@ public class ProfilePathProcessor { .withContextPathSource(tgt.getElement().getPath()) .withContextPathTarget(outcome.getPath()) .withRedirector(profileUtilities.redirectorStack(getRedirector(), outcome, currentBasePath)).withSlicing(new PathSlicingParams()).processPaths( - new ProfilePathProcessorState(cursors.base, newBaseCursor, start, cursors.contextName, cursors.resultPathBase)); + new ProfilePathProcessorState(cursors.base, newBaseCursor, start, cursors.contextName, cursors.resultPathBase), mapHelper); } } else { StructureDefinition dt = outcome.getType().size() > 1 ? profileUtilities.getContext().fetchTypeDefinition("Element") : profileUtilities.getProfileForDataType(outcome.getType().get(0), getWebUrl(), getDerived()); @@ -967,7 +968,7 @@ public class ProfilePathProcessor { .withContextPathTarget(outcome.getPath()) .withSlicing(new PathSlicingParams()).processPaths( /* starting again on the data type, but skip the root */ new ProfilePathProcessorState(dt.getSnapshot(), 1 /* starting again on the data type, but skip the root */, start, - cursors.contextName, cursors.resultPathBase)); + cursors.contextName, cursors.resultPathBase), mapHelper); } else { this @@ -979,7 +980,7 @@ public class ProfilePathProcessor { .withContextPathTarget( outcome.getPath()) .withRedirector(profileUtilities.redirectorStack(getRedirector(), currentBase, currentBasePath)).withSlicing(new PathSlicingParams()).processPaths( /* starting again on the data type, but skip the root */ new ProfilePathProcessorState(dt.getSnapshot(), 1 /* starting again on the data type, but skip the root */, start, - cursors.contextName, cursors.resultPathBase)); + cursors.contextName, cursors.resultPathBase), mapHelper); } } } @@ -991,7 +992,7 @@ public class ProfilePathProcessor { ElementDefinition currentBase, String currentBasePath, List<ElementDefinition> diffMatches, List<TypeSlice> typeList, - final ProfilePathProcessorState cursors + final ProfilePathProcessorState cursors, MappingAssistant mapHelper ) { // the item is already sliced in the base profile. // here's the rules @@ -1004,19 +1005,19 @@ public class ProfilePathProcessor { String path = currentBase.getPath(); if (diffMatches.isEmpty()) { - processPathWithSlicedBaseAndEmptyDiffMatches(currentBase, currentBasePath, diffMatches, cursors, path); + processPathWithSlicedBaseAndEmptyDiffMatches(currentBase, currentBasePath, diffMatches, cursors, path, mapHelper); } else if (profileUtilities.diffsConstrainTypes(diffMatches, currentBasePath, typeList)) { - processPathWithSlicedBaseWhereDiffsConstrainTypes(currentBasePath, diffMatches, typeList, cursors); + processPathWithSlicedBaseWhereDiffsConstrainTypes(currentBasePath, diffMatches, typeList, cursors, mapHelper); } else { - processPathWithSlicedBaseDefault(currentBase, currentBasePath, diffMatches, cursors, path); + processPathWithSlicedBaseDefault(currentBase, currentBasePath, diffMatches, cursors, path, mapHelper); } } - private void processPathWithSlicedBaseDefault(ElementDefinition currentBase, String currentBasePath, List<ElementDefinition> diffMatches, ProfilePathProcessorState cursors, String path) { + private void processPathWithSlicedBaseDefault(ElementDefinition currentBase, String currentBasePath, List<ElementDefinition> diffMatches, ProfilePathProcessorState cursors, String path, MappingAssistant mapHelper) { // first - check that the slicing is ok boolean closed = currentBase.getSlicing().getRules() == ElementDefinition.SlicingRules.CLOSED; int diffpos = 0; @@ -1037,7 +1038,7 @@ public class ProfilePathProcessor { profileUtilities.updateFromBase(outcome, currentBase, getSourceStructureDefinition().getUrl()); if (diffMatches.get(0).hasSlicing() || !diffMatches.get(0).hasSliceName()) { profileUtilities.updateFromSlicing(outcome.getSlicing(), diffMatches.get(0).getSlicing()); - profileUtilities.updateFromDefinition(outcome, diffMatches.get(0), getProfileName(), closed, getUrl(), getSourceStructureDefinition(), getDerived(), diffPath(diffMatches.get(0))); // if there's no slice, we don't want to update the unsliced description + profileUtilities.updateFromDefinition(outcome, diffMatches.get(0), getProfileName(), closed, getUrl(), getSourceStructureDefinition(), getDerived(), diffPath(diffMatches.get(0)), mapHelper); // if there's no slice, we don't want to update the unsliced description profileUtilities.removeStatusExtensions(outcome); } else if (!diffMatches.get(0).hasSliceName()) { diffMatches.get(0).setUserData(profileUtilities.UD_GENERATED_IN_SNAPSHOT, outcome); // because of updateFromDefinition isn't called @@ -1076,7 +1077,7 @@ public class ProfilePathProcessor { .withContextPathSource(currentBasePath).withContextPathTarget(outcome.getPath()) .withSlicing(new PathSlicingParams()).processPaths( new ProfilePathProcessorState(dt.getSnapshot(), 1, newDiffCursor, - cursors.contextName, cursors.resultPathBase)); + cursors.contextName, cursors.resultPathBase), mapHelper); } else { this @@ -1086,7 +1087,7 @@ public class ProfilePathProcessor { .withProfileName(getProfileName() + profileUtilities.pathTail(diffMatches, 0)) .withRedirector(null).withSlicing(new PathSlicingParams()).processPaths( new ProfilePathProcessorState(cursors.base, cursors.baseCursor + 1, newDiffCursor, - cursors.contextName, cursors.resultPathBase)); + cursors.contextName, cursors.resultPathBase), mapHelper); } // throw new Error("Not done yet"); // } else if (currentBase.getType().get(0).getCode().equals("BackboneElement") && diffMatches.size() > 0 && diffMatches.get(0).hasSliceName()) { @@ -1127,7 +1128,7 @@ public class ProfilePathProcessor { .withProfileName(getProfileName() + profileUtilities.pathTail(diffMatches, diffpos)) .withTrimDifferential(closed) .withSlicing(new PathSlicingParams(true, null, null)).processPaths( - new ProfilePathProcessorState(cursors.base, cursors.baseCursor, newDiffCursor, cursors.contextName, cursors.resultPathBase)); + new ProfilePathProcessorState(cursors.base, cursors.baseCursor, newDiffCursor, cursors.contextName, cursors.resultPathBase), mapHelper); // ok, done with that - now set the cursors for if this is the end cursors.baseCursor = newBaseLimit; cursors.diffCursor = newDiffLimit + 1; @@ -1175,7 +1176,7 @@ public class ProfilePathProcessor { throw new DefinitionException(profileUtilities.getContext().formatMessage(I18nConstants.ADDING_WRONG_PATH)); debugCheck(outcome); getResult().getElement().add(outcome); - profileUtilities.updateFromDefinition(outcome, diffItem, getProfileName(), isTrimDifferential(), getUrl(), getSourceStructureDefinition(), getDerived(), diffPath(diffItem)); + profileUtilities.updateFromDefinition(outcome, diffItem, getProfileName(), isTrimDifferential(), getUrl(), getSourceStructureDefinition(), getDerived(), diffPath(diffItem), mapHelper); profileUtilities.removeStatusExtensions(outcome); // --- LM Added this cursors.diffCursor = getDifferential().getElement().indexOf(diffItem) + 1; @@ -1204,7 +1205,7 @@ public class ProfilePathProcessor { .withContextPathTarget(cursors.base.getElement().get(0).getPath()) .withSlicing(new PathSlicingParams()).processPaths( new ProfilePathProcessorState(cursors.base, baseStart, start - 1, - cursors.contextName, cursors.resultPathBase)); + cursors.contextName, cursors.resultPathBase), mapHelper); } else { StructureDefinition dt = profileUtilities.getProfileForDataType(outcome.getType().get(0), getWebUrl(), getDerived()); // if (t.getCode().equals("Extension") && t.hasProfile() && !t.getProfile().contains(":")) { @@ -1225,7 +1226,7 @@ public class ProfilePathProcessor { .withProfileName(getProfileName() + profileUtilities.pathTail(diffMatches, 0)) .withContextPathSource(diffMatches.get(0).getPath()).withContextPathTarget(outcome.getPath()).withSlicing(new PathSlicingParams()).processPaths( /* starting again on the data type, but skip the root */ new ProfilePathProcessorState(dt.getSnapshot(), 1 /* starting again on the data type, but skip the root */, start - 1, - cursors.contextName, cursors.resultPathBase)); + cursors.contextName, cursors.resultPathBase), mapHelper); } } } @@ -1243,7 +1244,7 @@ public class ProfilePathProcessor { } } - private void processPathWithSlicedBaseWhereDiffsConstrainTypes(String currentBasePath, List<ElementDefinition> diffMatches, List<TypeSlice> typeList, ProfilePathProcessorState cursors) { + private void processPathWithSlicedBaseWhereDiffsConstrainTypes(String currentBasePath, List<ElementDefinition> diffMatches, List<TypeSlice> typeList, ProfilePathProcessorState cursors, MappingAssistant mapHelper) { int start = 0; int newBaseLimit = profileUtilities.findEndOfElement(cursors.base, cursors.baseCursor); int newDiffCursor = getDifferential().getElement().indexOf(diffMatches.get(0)); @@ -1329,7 +1330,7 @@ public class ProfilePathProcessor { .withProfileName(getProfileName() + profileUtilities.pathTail(diffMatches,0)) .withSlicing(new PathSlicingParams(true, null, currentBasePath)).processPaths( new ProfilePathProcessorState(cursors.base, cursors.baseCursor, newDiffCursor, - cursors.contextName, cursors.resultPathBase)); + cursors.contextName, cursors.resultPathBase), mapHelper); if (e == null) throw new FHIRException(profileUtilities.getContext().formatMessage(I18nConstants.DID_NOT_FIND_TYPE_ROOT_, diffMatches.get(0).getPath())); // now set up slicing on the e (cause it was wiped by what we called. @@ -1369,7 +1370,7 @@ public class ProfilePathProcessor { .withDiffLimit(newDiffLimit) .withProfileName(getProfileName() + profileUtilities.pathTail(diffMatches, i)) .withSlicing(new PathSlicingParams(true, e, currentBasePath)).processPaths( - new ProfilePathProcessorState(cursors.base, sStart, newDiffCursor, cursors.contextName, cursors.resultPathBase)); + new ProfilePathProcessorState(cursors.base, sStart, newDiffCursor, cursors.contextName, cursors.resultPathBase), mapHelper); } if (elementToRemove != null) { getDifferential().getElement().remove(elementToRemove); @@ -1395,7 +1396,7 @@ public class ProfilePathProcessor { .withBaseLimit(bs.getEnd()) .withDiffLimit(0) .withProfileName(getProfileName() + profileUtilities.tail(bs.getDefn().getPath())).withSlicing(new PathSlicingParams(true, e, currentBasePath)).processPaths( - new ProfilePathProcessorState(cursors.base, bs.getStart(), 0, cursors.contextName, cursors.resultPathBase)); + new ProfilePathProcessorState(cursors.base, bs.getStart(), 0, cursors.contextName, cursors.resultPathBase), mapHelper); } } @@ -1405,7 +1406,7 @@ public class ProfilePathProcessor { //throw new Error("not done yet - slicing / types @ "+cpath); } - private void processPathWithSlicedBaseAndEmptyDiffMatches(ElementDefinition currentBase, String currentBasePath, List<ElementDefinition> diffMatches, ProfilePathProcessorState cursors, String path) { + private void processPathWithSlicedBaseAndEmptyDiffMatches(ElementDefinition currentBase, String currentBasePath, List<ElementDefinition> diffMatches, ProfilePathProcessorState cursors, String path, MappingAssistant mapHelper) { if (profileUtilities.hasInnerDiffMatches(getDifferential(), path, cursors.diffCursor, getDiffLimit(), cursors.base.getElement(), true)) { // so we just copy it in ElementDefinition outcome = profileUtilities.updateURLs(getUrl(), getWebUrl(), currentBase.copy()); @@ -1425,7 +1426,7 @@ public class ProfilePathProcessor { this .incrementDebugIndent() .withSlicing(new PathSlicingParams()).processPaths( - new ProfilePathProcessorState(cursors.base, cursors.baseCursor + 1, cursors.diffCursor, cursors.contextName, cursors.resultPathBase)); + new ProfilePathProcessorState(cursors.base, cursors.baseCursor + 1, cursors.diffCursor, cursors.contextName, cursors.resultPathBase), mapHelper); cursors.baseCursor = indexOfFirstNonChild(cursors.base, currentBase, cursors.baseCursor, getBaseLimit()); } else { StructureDefinition dt = profileUtilities.getTypeForElement(getDifferential(), cursors.diffCursor, getProfileName(), diffMatches, outcome, getWebUrl(), getDerived()); @@ -1447,7 +1448,7 @@ public class ProfilePathProcessor { .withContextPathSource(currentBasePath) .withContextPathTarget(outcome.getPath()).withSlicing(new PathSlicingParams()).processPaths( /* starting again on the data type, but skip the root */ new ProfilePathProcessorState(dt.getSnapshot(), 1 /* starting again on the data type, but skip the root */, start, - cursors.contextName, cursors.resultPathBase)); + cursors.contextName, cursors.resultPathBase), mapHelper); } } cursors.baseCursor++; diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/profile/ProfileUtilities.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/profile/ProfileUtilities.java index cab010b6c..468430552 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/profile/ProfileUtilities.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/profile/ProfileUtilities.java @@ -48,6 +48,7 @@ import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRFormatError; import org.hl7.fhir.r5.conformance.ElementRedirection; +import org.hl7.fhir.r5.conformance.profile.MappingAssistant.MappingMergeModeOption; import org.hl7.fhir.r5.conformance.profile.ProfileUtilities.AllowUnknownProfile; import org.hl7.fhir.r5.conformance.profile.ProfileUtilities.ElementDefinitionCounter; import org.hl7.fhir.r5.context.IWorkerContext; @@ -205,13 +206,6 @@ public class ProfileUtilities { } - public enum MappingMergeModeOption { - DUPLICATE, // if there's more than one mapping for the same URI, just keep them all - IGNORE, // if there's more than one, keep the first - OVERWRITE, // if there's opre than, keep the last - APPEND, // if there's more than one, append them with ';' - } - public enum AllowUnknownProfile { NONE, // exception if there's any unknown profiles (the default) NON_EXTNEIONS, // don't raise an exception except on Extension (because more is going on there @@ -640,26 +634,6 @@ public class ProfileUtilities { } } - private void updateMaps(StructureDefinition base, StructureDefinition derived) throws DefinitionException { - if (base == null) - throw new DefinitionException(context.formatMessage(I18nConstants.NO_BASE_PROFILE_PROVIDED)); - if (derived == null) - throw new DefinitionException(context.formatMessage(I18nConstants.NO_DERIVED_STRUCTURE_PROVIDED)); - - for (StructureDefinitionMappingComponent baseMap : base.getMapping()) { - boolean found = false; - for (StructureDefinitionMappingComponent derivedMap : derived.getMapping()) { - if (derivedMap.getUri() != null && derivedMap.getUri().equals(baseMap.getUri())) { - found = true; - break; - } - } - if (!found) { - derived.getMapping().add(baseMap); - } - } - } - /** * Given a base (snapshot) profile structure, and a differential profile, generate a new snapshot profile * @@ -752,7 +726,9 @@ public class ProfileUtilities { // debug = true; // } - ProfilePathProcessor.processPaths(this, base, derived, url, webUrl, diff, baseSnapshot); + MappingAssistant mappingDetails = new MappingAssistant(mappingMergeMode, base, derived, context.getVersion()); + + ProfilePathProcessor.processPaths(this, base, derived, url, webUrl, diff, baseSnapshot, mappingDetails); checkGroupConstraints(derived); if (derived.getDerivation() == TypeDerivationRule.SPECIALIZATION) { @@ -761,7 +737,7 @@ public class ProfileUtilities { if (!e.hasUserData(UD_GENERATED_IN_SNAPSHOT) && e.getPath().contains(".")) { ElementDefinition existing = getElementInCurrentContext(e.getPath(), derived.getSnapshot().getElement()); if (existing != null) { - updateFromDefinition(existing, e, profileName, false, url, base, derived, "StructureDefinition.differential.element["+i+"]"); + updateFromDefinition(existing, e, profileName, false, url, base, derived, "StructureDefinition.differential.element["+i+"]", mappingDetails); } else { ElementDefinition outcome = updateURLs(url, webUrl, e.copy()); e.setUserData(UD_GENERATED_IN_SNAPSHOT, outcome); @@ -781,7 +757,7 @@ public class ProfileUtilities { if (derived.getKind() != StructureDefinitionKind.LOGICAL && !derived.getSnapshot().getElementFirstRep().getType().isEmpty()) throw new Error(context.formatMessage(I18nConstants.TYPE_ON_FIRST_SNAPSHOT_ELEMENT_FOR__IN__FROM_, derived.getSnapshot().getElementFirstRep().getPath(), derived.getUrl(), base.getUrl())); - updateMaps(base, derived); + mappingDetails.update(); setIds(derived, false); if (debug) { @@ -2374,7 +2350,7 @@ public class ProfileUtilities { } - protected void updateFromDefinition(ElementDefinition dest, ElementDefinition source, String pn, boolean trimDifferential, String purl, StructureDefinition srcSD, StructureDefinition derivedSrc, String path) throws DefinitionException, FHIRException { + protected void updateFromDefinition(ElementDefinition dest, ElementDefinition source, String pn, boolean trimDifferential, String purl, StructureDefinition srcSD, StructureDefinition derivedSrc, String path, MappingAssistant mappings) throws DefinitionException, FHIRException { source.setUserData(UD_GENERATED_IN_SNAPSHOT, dest); // we start with a clone of the base profile ('dest') and we copy from the profile ('source') // over the top for anything the source has @@ -2845,18 +2821,7 @@ public class ProfileUtilities { t.setUserData(UD_DERIVATION_EQUALS, true); } - List<ElementDefinitionMappingComponent> list = new ArrayList<>(); - list.addAll(base.getMapping()); - base.getMapping().clear(); - addMappings(base.getMapping(), list); - if (derived.hasMapping()) { - addMappings(base.getMapping(), derived.getMapping()); - } - for (ElementDefinitionMappingComponent m : base.getMapping()) { - if (m.hasMap()) { - m.setMap(m.getMap().trim()); - } - } + mappings.merge(derived, base); // note reversal of names to be correct in .merge() // todo: constraints are cumulative. there is no replacing for (ElementDefinitionConstraintComponent s : base.getConstraint()) { @@ -2960,52 +2925,6 @@ public class ProfileUtilities { tgt.getExtension().addAll(src.getExtension()); } - private void addMappings(List<ElementDefinitionMappingComponent> destination, List<ElementDefinitionMappingComponent> source) { - for (ElementDefinitionMappingComponent s : source) { - boolean found = false; - for (ElementDefinitionMappingComponent d : destination) { - if (compareMaps(s, d)) { - found = true; - d.setUserData(UD_DERIVATION_EQUALS, true); - break; - } - } - if (!found) { - destination.add(s); - } - } - } - - private boolean compareMaps(ElementDefinitionMappingComponent s, ElementDefinitionMappingComponent d) { - if (d.getIdentity().equals(s.getIdentity()) && d.getMap().equals(s.getMap())) { - return true; - } - if (VersionUtilities.isR5Plus(context.getVersion())) { - if (d.getIdentity().equals(s.getIdentity())) { - switch (mappingMergeMode) { - case APPEND: - if (!Utilities.splitStrings(d.getMap(), "\\,").contains(s.getMap())) { - d.setMap(d.getMap()+","+s.getMap()); - } - return true; - case DUPLICATE: - return false; - case IGNORE: - d.setMap(s.getMap()); - return true; - case OVERWRITE: - return true; - default: - return false; - } - } else { - return false; - } - } else { - return false; - } - } - private void checkTypeDerivation(String purl, StructureDefinition srcSD, ElementDefinition base, ElementDefinition derived, TypeRefComponent ts, String path) { boolean ok = false; CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/Element.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/Element.java index 4856585d0..c6254b682 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/Element.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/Element.java @@ -1108,6 +1108,21 @@ public class Element extends Base implements NamedItem { return null; } + public List<Element> getExtensions(String url) { + List<Element> list = new ArrayList<>(); + if (children != null) { + for (Element child : children) { + if (extensionList.contains(child.getName())) { + String u = child.getChildValue("url"); + if (url.equals(u)) { + list.add(child); + } + } + } + } + return list; + } + public Base getExtensionValue(String url) { if (children != null) { for (Element child : children) { diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/XmlParser.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/XmlParser.java index 58a799d6a..adf06a789 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/XmlParser.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/XmlParser.java @@ -144,7 +144,7 @@ public class XmlParser extends ParserBase { stream.reset(); // use a slower parser that keeps location data - TransformerFactory transformerFactory = TransformerFactory.newInstance(); + TransformerFactory transformerFactory = XMLUtil.newXXEProtectedTransformerFactory(); Transformer nullTransformer = transformerFactory.newTransformer(); DocumentBuilder docBuilder = factory.newDocumentBuilder(); doc = docBuilder.newDocument(); @@ -255,6 +255,8 @@ public class XmlParser extends ParserBase { return "sdtc:"; if (ns.equals("urn:ihe:pharm")) return "pharm:"; + if (ns.equals("http://ns.electronichealth.net.au/Ci/Cda/Extensions/3.0")) + return "ext:"; return "?:"; } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/fhirpath/FHIRPathEngine.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/fhirpath/FHIRPathEngine.java index cc72e3e89..5d1efdcec 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/fhirpath/FHIRPathEngine.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/fhirpath/FHIRPathEngine.java @@ -3735,20 +3735,20 @@ public class FHIRPathEngine { case LowBoundary: case HighBoundary: { - checkContextContinuous(focus, exp.getFunction().toCode(), exp); + checkContextContinuous(focus, exp.getFunction().toCode(), exp, true); if (paramTypes.size() > 0) { checkParamTypes(exp, exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer)); } - if (focus.hasType("decimal") && (focus.hasType("date") || focus.hasType("datetime") || focus.hasType("instant"))) { + if ((focus.hasType("date") || focus.hasType("datetime") || focus.hasType("instant"))) { return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Decimal, TypeDetails.FP_DateTime); - } else if (focus.hasType("decimal")) { + } else if (focus.hasType("decimal") || focus.hasType("integer")) { return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Decimal); } else { return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_DateTime); } } case Precision: { - checkContextContinuous(focus, exp.getFunction().toCode(), exp); + checkContextContinuous(focus, exp.getFunction().toCode(), exp, false); return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer); } case hasTemplateIdOf: { @@ -3897,8 +3897,8 @@ public class FHIRPathEngine { } } - private void checkContextContinuous(TypeDetails focus, String name, ExpressionNode expr) throws PathEngineException { - if (!focus.hasNoTypes() && !focus.hasType("decimal") && !focus.hasType("date") && !focus.hasType("dateTime") && !focus.hasType("time") && !focus.hasType("Quantity")) { + private void checkContextContinuous(TypeDetails focus, String name, ExpressionNode expr, boolean allowInteger) throws PathEngineException { + if (!focus.hasNoTypes() && !focus.hasType("decimal") && !focus.hasType("date") && !focus.hasType("dateTime") && !focus.hasType("time") && !focus.hasType("Quantity") && !(allowInteger && focus.hasType("integer"))) { throw makeException(expr, I18nConstants.FHIRPATH_CONTINUOUS_ONLY, name, focus.describe()); } } @@ -4295,7 +4295,7 @@ public class FHIRPathEngine { if (focus.size() > 1) { throw makeExceptionPlural(focus.size(), expr, I18nConstants.FHIRPATH_FOCUS, "lowBoundary", focus.size()); } - int precision = 0; + Integer precision = null; if (expr.getParameters().size() > 0) { List<Base> n1 = execute(context, focus, expr.getParameters().get(0), true); if (n1.size() != 1) { @@ -4308,17 +4308,23 @@ public class FHIRPathEngine { List<Base> result = new ArrayList<Base>(); if (base.hasType("decimal")) { - result.add(new DecimalType(Utilities.lowBoundaryForDecimal(base.primitiveValue(), precision == 0 ? 8 : precision))); + if (precision == null || (precision >= 0 && precision < 17)) { + result.add(new DecimalType(Utilities.lowBoundaryForDecimal(base.primitiveValue(), precision == null ? 8 : precision))); + } + } else if (base.hasType("integer")) { + if (precision == null || (precision >= 0 && precision < 17)) { + result.add(new DecimalType(Utilities.lowBoundaryForDecimal(base.primitiveValue(), precision == null ? 8 : precision))); + } } else if (base.hasType("date")) { - result.add(new DateTimeType(Utilities.lowBoundaryForDate(base.primitiveValue(), precision == 0 ? 10 : precision))); + result.add(new DateTimeType(Utilities.lowBoundaryForDate(base.primitiveValue(), precision == null ? 10 : precision))); } else if (base.hasType("dateTime")) { - result.add(new DateTimeType(Utilities.lowBoundaryForDate(base.primitiveValue(), precision == 0 ? 17 : precision))); + result.add(new DateTimeType(Utilities.lowBoundaryForDate(base.primitiveValue(), precision == null ? 17 : precision))); } else if (base.hasType("time")) { - result.add(new TimeType(Utilities.lowBoundaryForTime(base.primitiveValue(), precision == 0 ? 9 : precision))); + result.add(new TimeType(Utilities.lowBoundaryForTime(base.primitiveValue(), precision == null ? 9 : precision))); } else if (base.hasType("Quantity")) { String value = getNamedValue(base, "value"); Base v = base.copy(); - v.setProperty("value", new DecimalType(Utilities.lowBoundaryForDecimal(value, precision == 0 ? 8 : precision))); + v.setProperty("value", new DecimalType(Utilities.lowBoundaryForDecimal(value, precision == null ? 8 : precision))); result.add(v); } else { makeException(expr, I18nConstants.FHIRPATH_WRONG_PARAM_TYPE, "sqrt", "(focus)", base.fhirType(), "decimal or date"); @@ -4333,7 +4339,7 @@ public class FHIRPathEngine { if (focus.size() > 1) { throw makeExceptionPlural(focus.size(), expr, I18nConstants.FHIRPATH_FOCUS, "highBoundary", focus.size()); } - int precision = 0; + Integer precision = null; if (expr.getParameters().size() > 0) { List<Base> n1 = execute(context, focus, expr.getParameters().get(0), true); if (n1.size() != 1) { @@ -4346,17 +4352,23 @@ public class FHIRPathEngine { Base base = focus.get(0); List<Base> result = new ArrayList<Base>(); if (base.hasType("decimal")) { - result.add(new DecimalType(Utilities.highBoundaryForDecimal(base.primitiveValue(), precision == 0 ? 8 : precision))); + if (precision == null || (precision >= 0 && precision < 17)) { + result.add(new DecimalType(Utilities.highBoundaryForDecimal(base.primitiveValue(), precision == null ? 8 : precision))); + } + } else if (base.hasType("integer")) { + if (precision == null || (precision >= 0 && precision < 17)) { + result.add(new DecimalType(Utilities.highBoundaryForDecimal(base.primitiveValue(), precision == null ? 8 : precision))); + } } else if (base.hasType("date")) { - result.add(new DateTimeType(Utilities.highBoundaryForDate(base.primitiveValue(), precision == 0 ? 10 : precision))); + result.add(new DateTimeType(Utilities.highBoundaryForDate(base.primitiveValue(), precision == null ? 10 : precision))); } else if (base.hasType("dateTime")) { - result.add(new DateTimeType(Utilities.highBoundaryForDate(base.primitiveValue(), precision == 0 ? 17 : precision))); + result.add(new DateTimeType(Utilities.highBoundaryForDate(base.primitiveValue(), precision == null ? 17 : precision))); } else if (base.hasType("time")) { - result.add(new TimeType(Utilities.highBoundaryForTime(base.primitiveValue(), precision == 0 ? 9 : precision))); + result.add(new TimeType(Utilities.highBoundaryForTime(base.primitiveValue(), precision == null ? 9 : precision))); } else if (base.hasType("Quantity")) { String value = getNamedValue(base, "value"); Base v = base.copy(); - v.setProperty("value", new DecimalType(Utilities.highBoundaryForDecimal(value, precision == 0 ? 8 : precision))); + v.setProperty("value", new DecimalType(Utilities.highBoundaryForDecimal(value, precision == null ? 8 : precision))); result.add(v); } else { makeException(expr, I18nConstants.FHIRPATH_WRONG_PARAM_TYPE, "sqrt", "(focus)", base.fhirType(), "decimal or date"); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/ElementDefinition.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/ElementDefinition.java index a2ea355d4..e81d72850 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/ElementDefinition.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/ElementDefinition.java @@ -5625,6 +5625,11 @@ public boolean hasTarget() { @Block() public static class ElementDefinitionMappingComponent extends Element implements IBaseDatatypeElement { + @Override + public String toString() { + return identity+"=" + map; + } + /** * An internal reference to the definition of a mapping. */ diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/StructureDefinition.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/StructureDefinition.java index 7b5589721..e8876fa31 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/StructureDefinition.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/StructureDefinition.java @@ -397,6 +397,11 @@ public class StructureDefinition extends CanonicalResource { @Block() public static class StructureDefinitionMappingComponent extends BackboneElement implements IBaseBackboneElement { + @Override + public String toString() { + return identity + "=" + uri + " (\""+name+"\")"; + } + /** * An Internal id that is used to identify this mapping set when specific mappings are made. */ diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/CapabilityStatementRenderer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/CapabilityStatementRenderer.java index 7193ad60b..68148c913 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/CapabilityStatementRenderer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/CapabilityStatementRenderer.java @@ -18,6 +18,10 @@ import org.hl7.fhir.r5.model.CapabilityStatement.CapabilityStatementRestComponen import org.hl7.fhir.r5.model.CapabilityStatement.CapabilityStatementRestResourceComponent; import org.hl7.fhir.r5.model.CapabilityStatement.CapabilityStatementRestResourceOperationComponent; import org.hl7.fhir.r5.model.CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent; +import org.hl7.fhir.r5.model.CapabilityStatement.CapabilityStatementDocumentComponent; +import org.hl7.fhir.r5.model.CapabilityStatement.CapabilityStatementMessagingComponent; +import org.hl7.fhir.r5.model.CapabilityStatement.CapabilityStatementMessagingSupportedMessageComponent; +import org.hl7.fhir.r5.model.CapabilityStatement.CapabilityStatementMessagingEndpointComponent; import org.hl7.fhir.r5.model.CapabilityStatement.ReferenceHandlingPolicy; import org.hl7.fhir.r5.model.CapabilityStatement.ResourceInteractionComponent; import org.hl7.fhir.r5.model.CapabilityStatement.SystemInteractionComponent; @@ -281,6 +285,30 @@ public class CapabilityStatementRenderer extends ResourceRenderer { } + private class ResourceInteraction { + private String codeString; + private String documentation; + public ResourceInteraction(String code, String markdown) { + codeString = code; + if (!Utilities.noString(markdown)) { + documentation = markdown; + } + else { + documentation = null; + } + } + + public String getDocumentation() { + return documentation; + } + + public String getInteraction() { + return codeString; + } + } + + + public void render(RenderingStatus status, XhtmlNode x, CapabilityStatement conf, ResourceWrapper res) throws FHIRFormatError, DefinitionException, IOException { status.setExtensions(true); boolean igRenderingMode = (context.getRules() == GenerationRules.IG_PUBLISHER); @@ -346,16 +374,42 @@ public class CapabilityStatementRenderer extends ResourceRenderer { //Third time for individual resources int resCount = 1; for (CapabilityStatementRestResourceComponent r : rest.getResource()) { - addResourceConfigPanel(x, r, nextLevel+1, count, resCount, igRenderingMode); + addResourceConfigPanel(status, res, x, r, nextLevel+1, count, resCount, igRenderingMode); resCount++; } } + if (rest.getOperation().size() > 0) { + //TODO Figure out what should come out of this + x.h(nextLevel,"operationsCap" + Integer.toString(count)).addText(context.formatPhrase(RenderingContext.CAPABILITY_OP)); + x.h(nextLevel+1,"operationsSummary" + Integer.toString(count)).addText(context.formatPhrase(RenderingContext.OP_DEF_USE)); + } count++; } } + int messagingNum = conf.getMessaging().size(); + nextLevel = 3; + if (messagingNum > 0) { + x.h(2,"messaging").addText((context.formatPhrase(RenderingContext.CAPABILITY_MESSAGING_CAPS))); + int count=1; + for (CapabilityStatementMessagingComponent msg : conf.getMessaging()) + { + addMessagingPanel(status, res, x, msg, nextLevel, count, messagingNum); + count++; + } + + } + + int documentNum = conf.getDocument().size(); + nextLevel = 3; + if (documentNum > 0) { + x.h(2,"document").addText((context.formatPhrase(RenderingContext.CAPABILITY_DOCUMENT_CAPS))); + addDocumentTable(status, res, x, conf, nextLevel); + } + + if (multExpectationsPresent) { - addWarningPanel(x,"⹋⹋ - this mark indicates that there are more than one expectation extensions present"); + addWarningPanel(x,"⹋⹋ - " + context.formatPhrase(RenderingContext.CAPABILITY_MULT_EXT)); } } @@ -484,7 +538,7 @@ public class CapabilityStatementRenderer extends ResourceRenderer { } } if (igMays.size() > 0) { - x.h(3,"shouldIGs").addText(context.formatPhrase(RenderingContext.CAPABILITY_SHOULD_SUPP)); + x.h(3,"mayIGs").addText(context.formatPhrase(RenderingContext.CAPABILITY_MAY_SUPP)); ul = x.ul(); for (String url : igMays) { addResourceLink(ul.li(), url, url); @@ -522,7 +576,7 @@ public class CapabilityStatementRenderer extends ResourceRenderer { capExpectation = getExtValueCode(c.getExtensionByUrl(EXPECTATION)); if (!Utilities.noString(capExpectation)) { lItem.addTag("strong").addText(capExpectation); - lItem.addText(context.formatPhrase(RenderingContext.CAPABILITY_SUPP) + " "); + lItem.addText(" " + context.formatPhrase(RenderingContext.CAPABILITY_SUPP) + " "); } lItem.code().addText(c.getCode()); first = false; @@ -564,6 +618,107 @@ public class CapabilityStatementRenderer extends ResourceRenderer { } + private void addMessagingPanel(RenderingStatus status, ResourceWrapper res, XhtmlNode x, CapabilityStatementMessagingComponent msg, int nextLevel, int index, int total) throws FHIRFormatError, DefinitionException, IOException { + XhtmlNode panel= null; + XhtmlNode body = null; + XhtmlNode row = null; + XhtmlNode heading = null; + + XhtmlNode table; + XhtmlNode tbody; + XhtmlNode tr; + + panel = x.div().attribute("class", "panel panel-default"); + heading = panel.div().attribute("class", "panel-heading").h(nextLevel,"messaging_" + Integer.toString(index)).attribute("class", "panel-title"); + if(total == 1) + { + heading.addText(context.formatPhrase(RenderingContext.CAPABILITY_MESSAGING_CAP)); + } + else + { + heading.addText(context.formatPhrase(RenderingContext.CAPABILITY_MESSAGING_CAP) + " " + String.valueOf(index)); + } + + body = panel.div().attribute("class", "panel-body"); + + if(msg.hasReliableCache()) + { + addLead(body, "Reliable Cache Length"); + body.br(); + body.addText(String.valueOf(msg.getReliableCache()) + " Minute(s)"); + body.br(); + } + + if(msg.hasEndpoint()) + { + body.h(nextLevel+1,"msg_end_"+Integer.toString(index)).addText(context.formatPhrase(RenderingContext.CAPABILITY_ENDPOINTS)); + table = body.table("table table-condensed table-hover"); + tr = table.addTag("thead").tr(); + tr.th().addText("Protocol"); + tr.th().addText("Address"); + + tbody = table.addTag("tbody"); + for (CapabilityStatementMessagingEndpointComponent end : msg.getEndpoint()) + { + tr = tbody.tr(); + renderDataType(status, tr.td(), wrapNC(end.getProtocol())); + renderUri(status, tr.td(), wrapNC(end.getAddressElement())); + } + body.br(); + } + + if(msg.hasSupportedMessage()) + { + body.h(nextLevel+1,"msg_end_"+Integer.toString(index)).addText(context.formatPhrase(RenderingContext.CAPABILITY_SUPP_MSGS)); + table = body.table("table table-condensed table-hover"); + tr = table.addTag("thead").tr(); + tr.th().addText("Mode"); + tr.th().addText(context.formatPhrase(RenderingContext.GENERAL_DEFINITION)); + + tbody = table.addTag("tbody"); + for (CapabilityStatementMessagingSupportedMessageComponent sup : msg.getSupportedMessage()) + { + tr = tbody.tr(); + tr.td().addText(sup.getMode().toCode()); + renderCanonical(status, res, tr.td(), StructureDefinition.class, sup.getDefinitionElement()); + } + if(msg.hasDocumentation()) + { + addLead(body, context.formatPhrase(RenderingContext.GENERAL_DOCUMENTATION)); + addMarkdown(body.blockquote(), msg.getDocumentation()); + } + body.br(); + } + } + + + private void addDocumentTable(RenderingStatus status, ResourceWrapper res, XhtmlNode x, CapabilityStatement conf, int nextLevel) throws FHIRFormatError, DefinitionException, IOException { + XhtmlNode table; + XhtmlNode tbody; + XhtmlNode tr; + + table = x.table("table table-condensed table-hover"); + tr = table.addTag("thead").tr(); + tr.th().addText("Mode"); + tr.th().addText(context.formatPhrase(RenderingContext.CAPABILITY_PROF_RES_DOC)); + tr.th().addText(context.formatPhrase(RenderingContext.GENERAL_DOCUMENTATION)); + + tbody = table.addTag("tbody"); + for (CapabilityStatementDocumentComponent document : conf.getDocument()) { + tr = tbody.tr(); + tr.td().addText(document.getMode().toCode()); + renderCanonical(status, res, tr.td(), StructureDefinition.class, document.getProfileElement()); + if(document.hasDocumentation()) + { + addMarkdown(tr.td(), document.getDocumentation()); + } + else + { + tr.td().nbsp(); + } + } + } + private String getCorsText(boolean on) { if (on) { return context.formatPhrase(RenderingContext.CAPABILITY_CORS_YES); @@ -639,10 +794,10 @@ public class CapabilityStatementRenderer extends ResourceRenderer { for (Map<String,String> interactionMap : interactions) { item = uList.li(); if (Utilities.noString(verb)) { - item.addText(context.formatPhrase(RenderingContext.CAPABILITY_SUPP_THE) + " "); + item.addText(context.formatPhrase(RenderingContext.CAPABILITY_SUPPS_THE) + " "); } else { - item.addTag("strong").addText(verb); + item.addTag("strong").addText(verb + " "); item.addText(context.formatPhrase(RenderingContext.CAPABILITY_SUPP_THE) + " "); } interaction = interactionMap.keySet().toArray()[0].toString(); @@ -669,7 +824,7 @@ public class CapabilityStatementRenderer extends ResourceRenderer { } } - private void addInteractionSummaryList(XhtmlNode uList, String verb, List<String> interactions) { + private void addInteractionSummaryList(XhtmlNode uList, String verb, List<ResourceInteraction> interactions) { if (interactions.size() == 0) return; XhtmlNode item = uList.li(); if (Utilities.noString(verb)) { @@ -677,10 +832,10 @@ public class CapabilityStatementRenderer extends ResourceRenderer { } else { item.addTag("strong").addText(verb); - item.addText(context.formatPhrase(RenderingContext.CAPABILITY_SUPP) + " "); + item.addText(" " + context.formatPhrase(RenderingContext.CAPABILITY_SUPP) + " "); } - addSeparatedListOfCodes(item, interactions, ","); - item.addText("."); + + applyInteractionsList(item, interactions); } private void addSummaryIntro(XhtmlNode x) { @@ -779,7 +934,7 @@ public class CapabilityStatementRenderer extends ResourceRenderer { renderSupportedProfiles(status, res, profCell, r); } //Show capabilities - tr.td().addText(showOp(r, TypeRestfulInteraction.READ)); + tr.td().attribute("class", "text-center").addText(showOp(r, TypeRestfulInteraction.READ)); if (hasVRead) tr.td().attribute("class", "text-center").addText(showOp(r, TypeRestfulInteraction.VREAD)); tr.td().attribute("class", "text-center").addText(showOp(r, TypeRestfulInteraction.SEARCHTYPE)); @@ -934,11 +1089,45 @@ public class CapabilityStatementRenderer extends ResourceRenderer { result.add("$"+op.getName()); } } + else { + result.add("$"+op.getName()); + } } return result; } - private void addResourceConfigPanel(XhtmlNode x, CapabilityStatementRestResourceComponent r, int nextLevel, int count, int resCount, boolean igRenderingMode) throws FHIRFormatError, DefinitionException, IOException { + private void applyInteractionsList(XhtmlNode item, List<ResourceInteraction> list) { + List<String> noDocList = new ArrayList<String>(); + List<ResourceInteraction> docList = new ArrayList<ResourceInteraction>(); + for (ResourceInteraction inter : list) { + if (Utilities.noString(inter.getDocumentation())) { + noDocList.add(inter.getInteraction()); + } + else { + docList.add(inter); + } + } + if (noDocList.size() > 0) { + addSeparatedListOfCodes(item,noDocList, ","); + } + if (docList.size() > 0) { + item.br(); + for (ResourceInteraction inter : docList) { + item.code().addText(inter.getInteraction()); + try { + addMarkdown(item, inter.getDocumentation()); + } + catch(IOException e) { + e.printStackTrace(); + } + } + } + else { + item.addText("."); + } + } + + private void addResourceConfigPanel(RenderingStatus status, ResourceWrapper res, XhtmlNode x, CapabilityStatementRestResourceComponent r, int nextLevel, int count, int resCount, boolean igRenderingMode) throws FHIRFormatError, DefinitionException, IOException { XhtmlNode panel= null; XhtmlNode body = null; XhtmlNode panelHead = null; @@ -973,7 +1162,7 @@ public class CapabilityStatementRenderer extends ResourceRenderer { cell = row.div().attribute("class", "col-lg-6"); addLead(cell,context.formatPhrase(RenderingContext.CAPABILITY_BASE_SYS)); cell.br(); - addResourceLink(cell, text, text); + renderCanonical(status, res, cell, StructureDefinition.class, r.getProfileElement()); cell=row.div().attribute("class", "col-lg-3"); addLead(cell, context.formatPhrase(RenderingContext.CAPABILITY_PROF_CONF)); cell.br(); @@ -1008,7 +1197,7 @@ public class CapabilityStatementRenderer extends ResourceRenderer { para.br(); } first=false; - addResourceLink(para, c.asStringValue(), c.asStringValue()); + renderCanonical(status, res, para, StructureDefinition.class, c); //para.ah(c.asStringValue()).addText(c.asStringValue()); } } @@ -1085,28 +1274,32 @@ public class CapabilityStatementRenderer extends ResourceRenderer { private void addInteractions(XhtmlNode row, CapabilityStatementRestResourceComponent r, int width) { String capExpectation; String widthString = "col-lg-" + Integer.toString(width); - List<String> shalls = new ArrayList<String>(); - List<String> shoulds = new ArrayList<String>(); - List<String> mays = new ArrayList<String>(); - List<String> shouldnots = new ArrayList<String>(); - List<String> supporteds = new ArrayList<String>(); + //Need to build a different structure + List<ResourceInteraction> shalls = new ArrayList<ResourceInteraction>(); + List<ResourceInteraction> shoulds = new ArrayList<ResourceInteraction>(); + List<ResourceInteraction> mays = new ArrayList<ResourceInteraction>(); + List<ResourceInteraction> shouldnots = new ArrayList<ResourceInteraction>(); + List<ResourceInteraction> supporteds = new ArrayList<ResourceInteraction>(); + + ResourceInteraction tempInteraction = null; for (ResourceInteractionComponent op : r.getInteraction()) { capExpectation = expectationForDisplay(op,EXPECTATION); + tempInteraction = new ResourceInteraction(op.getCode().toCode(), op.getDocumentation()); if (!Utilities.noString(capExpectation)) { switch(capExpectation) { - case "SHALL" : shalls.add(op.getCode().toCode()); + case "SHALL" : shalls.add(tempInteraction); break; - case "SHOULD" : shoulds.add(op.getCode().toCode()); + case "SHOULD" : shoulds.add(tempInteraction); break; - case "MAY" : mays.add(op.getCode().toCode()); + case "MAY" : mays.add(tempInteraction); break; - case "SHOULD-NOT" : shouldnots.add(op.getCode().toCode()); + case "SHOULD-NOT" : shouldnots.add(tempInteraction); break; } } else { - supporteds.add(op.getCode().toCode()); + supporteds.add(tempInteraction); } } XhtmlNode cell = row.div().attribute("class", widthString); @@ -1494,7 +1687,7 @@ public class CapabilityStatementRenderer extends ResourceRenderer { private void addWarningPanel(XhtmlNode node, String text) { XhtmlNode panel = node.addTag("div").attribute("class","panel panel-danger").addTag("div").attribute("class","panel-body"); - panel.addTag("span").attribute("class","label label-danger").addText("Error detected"); + panel.addTag("span").attribute("class","label label-danger").addText(context.formatPhrase(RenderingContext.CAPABILITY_ERR_DET)); panel.addText(" " + text); } } \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ConceptMapRenderer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ConceptMapRenderer.java index 5cecde33a..7faafba35 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ConceptMapRenderer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ConceptMapRenderer.java @@ -579,8 +579,8 @@ public class ConceptMapRenderer extends TerminologyRenderer { if (!ccm.hasRelationship()) tr.td(); else { - if (ccm.getRelationshipElement().hasExtension(ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE)) { - String code = ToolingExtensions.readStringExtension(ccm.getRelationshipElement(), ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE); + if (ccm.hasExtension(ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE)) { + String code = ToolingExtensions.readStringExtension(ccm, ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE); tr.td().ah(context.prefixLocalHref(eqpath+"#"+code), code).tx(presentEquivalenceCode(code)); } else { tr.td().ah(context.prefixLocalHref(eqpath+"#"+ccm.getRelationship().toCode()), ccm.getRelationship().toCode()).tx(presentRelationshipCode(ccm.getRelationship().toCode())); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/OperationDefinitionRenderer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/OperationDefinitionRenderer.java index e7cce387d..6d7a1563c 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/OperationDefinitionRenderer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/OperationDefinitionRenderer.java @@ -86,8 +86,10 @@ public class OperationDefinitionRenderer extends TerminologyRenderer { } else { p.ah(context.prefixLocalHref(sd.getWebPath())).tx(sd.present()); } - } - x.para().tx(context.formatPhrase(RenderingContext.GENERAL_PARS)); + } + + x.h3().tx(context.formatPhrase(RenderingContext.GENERAL_PARS)); + //x.para().tx(context.formatPhrase(RenderingContext.GENERAL_PARS)); XhtmlNode tbl = x.table( "grid"); XhtmlNode tr = tbl.tr(); tr.td().b().tx(context.formatPhrase(RenderingContext.OP_DEF_USE)); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ProfileDrivenRenderer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ProfileDrivenRenderer.java index 60ad608cd..0e0227efb 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ProfileDrivenRenderer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ProfileDrivenRenderer.java @@ -332,13 +332,14 @@ public class ProfileDrivenRenderer extends ResourceRenderer { } } } else if (!round2 && !exemptFromRendering(child)) { - if (isExtension(p)) { + boolean isExt = isExtension(p); + if (isExt) { status.setExtensions(true); } List<ElementDefinition> grandChildren = getChildrenForPath(profile, allElements, path+"."+p.getName()); filterGrandChildren(grandChildren, path+"."+p.getName(), p); if (p.getValues().size() > 0) { - if (isSimple(child)) { + if (isSimple(child) && !isExt) { XhtmlNode para = x.isPara() ? para = x : x.para(); String name = p.getName(); if (name.endsWith("[x]")) @@ -383,22 +384,40 @@ public class ProfileDrivenRenderer extends ResourceRenderer { x.add(tbl); } } else if (isExtension(p)) { + StructureDefinition sd = context.getContext().fetchResource(StructureDefinition.class, p.getUrl()); for (ResourceWrapper v : p.getValues()) { if (v != null) { ResourceWrapper vp = v.child("value"); List<ResourceWrapper> ev = v.children("extension"); if (vp != null) { XhtmlNode para = x.para(); - para.b().addText(labelforExtension(p.getName())); + para.b().addText(labelforExtension(sd, p.getUrl())); para.tx(": "); renderLeaf(status, res, vp, profile, child, x, para, false, showCodeDetails, displayHints, path, indent); } else if (!ev.isEmpty()) { - XhtmlNode bq = x.addTag("blockquote"); - bq.para().b().addText(labelforExtension(p.getName())); + XhtmlNode bq = x.addTag("blockquote"); + bq.para().b().addText(labelforExtension(sd, p.getUrl())); + // what happens now depends. If all the children are simple extensions, they'll be rendered as properties + boolean allSimple = true; for (ResourceWrapper vv : ev) { - StructureDefinition ex = context.getWorker().fetchTypeDefinition("Extension"); - List<ElementDefinition> children = getChildrenForPath(profile, ex.getSnapshot().getElement(), "Extension"); - generateByProfile(status, res, ex, vv, allElements, child, children, bq, "Extension", showCodeDetails, indent+1); + if (!vv.has("value")) { + allSimple = false; + } + } + if (allSimple) { + XhtmlNode ul = bq.ul(); + for (ResourceWrapper vv : ev) { + XhtmlNode li = ul.li(); + li.tx(labelForSubExtension(vv.primitiveValue("url"), sd)); + li.tx(": "); + renderLeaf(status, res, vv.child("value"), sd, child, x, li, isExt, showCodeDetails, displayHints, path, indent); + } + } else { + for (ResourceWrapper vv : ev) { + StructureDefinition ex = context.getWorker().fetchTypeDefinition("Extension"); + List<ElementDefinition> children = getChildrenForPath(profile, ex.getSnapshot().getElement(), "Extension"); + generateByProfile(status, res, ex, vv, allElements, child, children, bq, "Extension", showCodeDetails, indent+1); + } } } } @@ -417,8 +436,11 @@ public class ProfileDrivenRenderer extends ResourceRenderer { } - private String labelforExtension(String url) { - StructureDefinition sd = context.getContext().fetchResource(StructureDefinition.class, url); + private String labelForSubExtension(String url, StructureDefinition sd) { + return url; + } + + private String labelforExtension(StructureDefinition sd, String url) { if (sd == null) { return tail(url); } else { @@ -467,7 +489,7 @@ public class ProfileDrivenRenderer extends ResourceRenderer { } public boolean isExtension(NamedResourceWrapperList p) { - return p.getName().contains("extension["); + return p.getUrl() != null; } @@ -571,12 +593,12 @@ public class ProfileDrivenRenderer extends ResourceRenderer { // 2. Park it NamedResourceWrapperList nl = null; for (NamedResourceWrapperList t : results) { - if (t.getName().equals(url)) { + if (t.getUrl() != null && t.getUrl().equals(url)) { nl = t; } } if (nl == null) { - nl = new NamedResourceWrapperList(url); + nl = new NamedResourceWrapperList(p.getName(), url); results.add(nl); } nl.getValues().add(v); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/QuestionnaireRenderer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/QuestionnaireRenderer.java index 9d601c685..20d3166f0 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/QuestionnaireRenderer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/QuestionnaireRenderer.java @@ -152,6 +152,12 @@ public class QuestionnaireRenderer extends TerminologyRenderer { text = v.primitiveValue("code"); } } + if (value == null) { + value = "??"; + } + if (text == null) { + text = "??"; + } boolean selected = "true".equals(opt.primitiveValue("initialSelected")); x.option(value, text, selected); } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ResourceRenderer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ResourceRenderer.java index b899fd8f1..ace526204 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ResourceRenderer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ResourceRenderer.java @@ -578,14 +578,16 @@ public abstract class ResourceRenderer extends DataRenderer { protected void renderUri(RenderingStatus status, XhtmlNode x, ResourceWrapper uri) throws FHIRFormatError, DefinitionException, IOException { if (!renderPrimitiveWithNoValue(status, x, uri)) { String v = uri.primitiveValue(); + boolean local = false; if (context.getContextUtilities().isResource(v)) { v = "http://hl7.org/fhir/StructureDefinition/"+v; + local = true; } if (v.startsWith("mailto:")) { x.ah(v).addText(v.substring(7)); } else { - ResourceWithReference rr = resolveReference(uri); + ResourceWithReference rr = local ? resolveReference(uri.resource(), v, true) : resolveReference(uri); if (rr != null) { if (rr.getResource() == null) { x.ah(context.prefixLocalHref(rr.getWebPath())).addText(rr.getUrlReference()); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/utils/ResourceWrapper.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/utils/ResourceWrapper.java index 4ca925231..5bcf1bc2f 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/utils/ResourceWrapper.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/utils/ResourceWrapper.java @@ -31,16 +31,28 @@ public abstract class ResourceWrapper { public static class NamedResourceWrapperList { private String name; + private String url; // for extension definitions private List<ResourceWrapper> values = new ArrayList<ResourceWrapper>(); - + public NamedResourceWrapperList(String name) { super(); this.name = name; } + + public NamedResourceWrapperList(String name, String url) { + super(); + this.name = name; + this.url = url; + } public String getName() { return name; } + + public String getUrl() { + return url; + } + public List<ResourceWrapper> getValues() { return values; } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/ToolingExtensions.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/ToolingExtensions.java index d4c845bcf..8f38aa2f4 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/ToolingExtensions.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/ToolingExtensions.java @@ -275,6 +275,7 @@ public class ToolingExtensions { public static final String EXT_VS_CS_SUPPL_NEEDED = "http://hl7.org/fhir/StructureDefinition/valueset-supplement"; public static final String EXT_TYPE_PARAMETER = "http://hl7.org/fhir/tools/StructureDefinition/type-parameter"; public static final String EXT_ALTERNATE_CANONICAL = "http://hl7.org/fhir/StructureDefinition/alternate-canonical"; + public static final String EXT_SUPPRESSED = "http://hl7.org/fhir/StructureDefinition/elementdefinition-suppress"; // specific extension helpers diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/SnapShotGenerationTests.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/SnapShotGenerationTests.java index 0b3ce1596..0ad0f7615 100644 --- a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/SnapShotGenerationTests.java +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/SnapShotGenerationTests.java @@ -31,6 +31,7 @@ import org.hl7.fhir.r5.formats.JsonParser; import org.hl7.fhir.r5.formats.XmlParser; import org.hl7.fhir.r5.model.Base; import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionBindingComponent; +import org.hl7.fhir.r5.model.Narrative; import org.hl7.fhir.r5.model.Resource; import org.hl7.fhir.r5.model.StructureDefinition; import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind; @@ -582,6 +583,9 @@ public class SnapShotGenerationTests { rc.setProfileUtilities(new ProfileUtilities(TestingUtilities.getSharedWorkerContext(), null, new TestPKP())); RendererFactory.factory(output, rc).renderResource(ResourceWrapper.forResource(rc.getContextUtilities(), output)); } + // we just generated it - but we don't care what it is here, just that there's no exceptions (though we need it for the rules) + Narrative txt = output.getText(); + output.setText(null); if (!fail) { test.output = output; TestingUtilities.getSharedWorkerContext().cacheResource(output); @@ -603,6 +607,7 @@ public class SnapShotGenerationTests { } Assertions.assertTrue(structureDefinitionEquality, "Output does not match expected"); } + output.setText(txt); } private StructureDefinition getSD(String url, SnapShotGenerationTestsContext context) throws DefinitionException, FHIRException, IOException { diff --git a/org.hl7.fhir.report/pom.xml b/org.hl7.fhir.report/pom.xml index 8396f3fee..5d3a1baf0 100644 --- a/org.hl7.fhir.report/pom.xml +++ b/org.hl7.fhir.report/pom.xml @@ -5,7 +5,7 @@ <parent> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>org.hl7.fhir.core</artifactId> - <version>6.3.21-SNAPSHOT</version> + <version>6.3.23-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> diff --git a/org.hl7.fhir.utilities/pom.xml b/org.hl7.fhir.utilities/pom.xml index a4fdde285..02b2f0f7a 100644 --- a/org.hl7.fhir.utilities/pom.xml +++ b/org.hl7.fhir.utilities/pom.xml @@ -5,7 +5,7 @@ <parent> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>org.hl7.fhir.core</artifactId> - <version>6.3.21-SNAPSHOT</version> + <version>6.3.23-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/MyURIResolver.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/MyURIResolver.java index cc11943a1..eecdb942f 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/MyURIResolver.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/MyURIResolver.java @@ -94,7 +94,7 @@ public class MyURIResolver implements URIResolver { if (s != null) return s; } - return TransformerFactory.newInstance().getURIResolver().resolve(href, base); + return org.hl7.fhir.utilities.xml.XMLUtil.newXXEProtectedTransformerFactory().getURIResolver().resolve(href, base); } else return new StreamSource(ManagedFileAccess.inStream(href.contains(File.separator) ? href : Utilities.path(path, href))); } catch (FileNotFoundException e) { diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/Utilities.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/Utilities.java index c9075c87b..c56787430 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/Utilities.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/Utilities.java @@ -1032,6 +1032,10 @@ public class Utilities { public static String escapeJson(String value) { + return escapeJson(value, true); + } + + public static String escapeJson(String value, boolean escapeUnicodeWhitespace) { if (value == null) return ""; @@ -1049,7 +1053,7 @@ public class Utilities { b.append("\\\\"); else if (c == ' ') b.append(" "); - else if (isWhitespace(c)) { + else if ((c == '\r' || c == '\n') || (isWhitespace(c) && escapeUnicodeWhitespace)) { b.append("\\u"+Utilities.padLeft(Integer.toHexString(c), '0', 4)); } else if (((int) c) < 32) b.append("\\u" + Utilities.padLeft(Integer.toHexString(c), '0', 4)); @@ -1691,31 +1695,47 @@ public class Utilities { value = value.substring(0, value.indexOf("e")); } if (isZero(value)) { - return applyPrecision("-0.5000000000000000000000000", precision); + return applyPrecision("-0.5000000000000000000000000", precision, true); } else if (value.startsWith("-")) { return "-"+highBoundaryForDecimal(value.substring(1), precision)+(e == null ? "" : e); } else { if (value.contains(".")) { - return applyPrecision(minusOne(value)+"50000000000000000000000000000", precision)+(e == null ? "" : e); + return applyPrecision(minusOne(value)+"50000000000000000000000000000", precision, true)+(e == null ? "" : e); } else { - return applyPrecision(minusOne(value)+".50000000000000000000000000000", precision)+(e == null ? "" : e); + return applyPrecision(minusOne(value)+".50000000000000000000000000000", precision, true)+(e == null ? "" : e); } } } - private static String applyPrecision(String v, int p) { + private static String applyPrecision(String v, int p, boolean down) { + String nv = v; + int dp = -1; + if (nv.contains(".")) { + dp = nv.indexOf("."); + nv = nv.substring(0, dp)+nv.substring(dp+1); + } + String s = null; int d = p - getDecimalPrecision(v); if (d == 0) { - return v; + s = nv; } else if (d > 0) { - return v + padLeft("", '0', d); + s = nv + padLeft("", '0', d); } else { - if (v.charAt(v.length()+d) >= '6') { - return v.substring(0, v.length()+d-1)+((char) (v.charAt(v.length()+d)+1)); + int l = v.length(); + int ld = l+d; + if (dp > -1) { + ld--; + } + if (nv.charAt(ld) >= '5' && !down) { + s = nv.substring(0, ld-1)+((char) (nv.charAt(ld-1)+1)); } else { - return v.substring(0, v.length()+d); + s = nv.substring(0, ld); } } + if (s.endsWith(".")) { + s = s.substring(0, s.length()-1); + } + return dp == -1 || dp >= s.length() ? s : s.substring(0, dp)+"."+s.substring(dp); } private static String minusOne(String value) { @@ -1827,14 +1847,14 @@ public class Utilities { value = value.substring(0, value.indexOf("e")); } if (isZero(value)) { - return applyPrecision("0.50000000000000000000000000000", precision); + return applyPrecision("0.50000000000000000000000000000", precision, false); } else if (value.startsWith("-")) { return "-"+lowBoundaryForDecimal(value.substring(1), precision)+(e == null ? "" : e); } else { if (value.contains(".")) { - return applyPrecision(value+"50000000000000000000000000000", precision)+(e == null ? "" : e); + return applyPrecision(value+"50000000000000000000000000000", precision, false)+(e == null ? "" : e); } else { - return applyPrecision(value+".50000000000000000000000000000", precision)+(e == null ? "" : e); + return applyPrecision(value+".50000000000000000000000000000", precision, false)+(e == null ? "" : e); } } } diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/XsltUtilities.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/XsltUtilities.java index 4507fc6c9..5a7e9b9fb 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/XsltUtilities.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/XsltUtilities.java @@ -73,7 +73,7 @@ public class XsltUtilities { } public static byte[] transform(Map<String, byte[]> files, byte[] source, byte[] xslt) throws TransformerException { - TransformerFactory f = TransformerFactory.newInstance(); + TransformerFactory f = org.hl7.fhir.utilities.xml.XMLUtil.newXXEProtectedTransformerFactory(); f.setAttribute("http://saxon.sf.net/feature/version-warning", Boolean.FALSE); StreamSource xsrc = new StreamSource(new ByteArrayInputStream(xslt)); f.setURIResolver(new ZipURIResolver(files)); @@ -129,7 +129,7 @@ public class XsltUtilities { public static void transform(String xsltDir, String source, String xslt, String dest, URIResolver alt) throws TransformerException, IOException { - TransformerFactory f = TransformerFactory.newInstance(); + TransformerFactory f = org.hl7.fhir.utilities.xml.XMLUtil.newXXEProtectedTransformerFactory(); StreamSource xsrc = new StreamSource(ManagedFileAccess.inStream(xslt)); f.setURIResolver(new MyURIResolver(xsltDir, alt)); Transformer t = f.newTransformer(xsrc); diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/SimpleHTTPClient.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/SimpleHTTPClient.java index ad3f5c7d5..52ce02bd6 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/SimpleHTTPClient.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/SimpleHTTPClient.java @@ -91,16 +91,18 @@ public class SimpleHTTPClient { c.setInstanceFollowRedirects(false); switch (c.getResponseCode()) { - case HttpURLConnection.HTTP_MOVED_PERM: - case HttpURLConnection.HTTP_MOVED_TEMP: - String location = c.getHeaderField("Location"); - location = URLDecoder.decode(location, "UTF-8"); - URL base = new URL(url); - URL next = new URL(base, location); // Deal with relative URLs - url = next.toExternalForm(); - continue; - default: - done = true; + case HttpURLConnection.HTTP_MOVED_PERM: + case HttpURLConnection.HTTP_MOVED_TEMP: + case 307: + case 308: // Same as HTTP_MOVED_PERM, but does not allow changing the request method from POST to GET + String location = c.getHeaderField("Location"); + location = URLDecoder.decode(location, "UTF-8"); + URL base = new URL(url); + URL next = new URL(base, location); // Deal with relative URLs + url = next.toExternalForm(); + continue; + default: + done = true; } } 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 94b1e770f..db81c97c3 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 @@ -1107,4 +1107,5 @@ public class I18nConstants { public static final String TYPE_SPECIFIC_CHECKS_DT_XHTML_LITERAL_HREF = "TYPE_SPECIFIC_CHECKS_DT_XHTML_LITERAL_HREF"; public static final String SM_TARGET_TYPE_UNKNOWN = "SM_TARGET_TYPE_UNKNOWN"; public static final String XHTML_XHTML_ATTRIBUTE_XML_SPACE = "XHTML_XHTML_ATTRIBUTE_XML_SPACE"; + public static final String VALIDATION_HL7_PUBLISHER_MULTIPLE_WGS = "VALIDATION_HL7_PUBLISHER_MULTIPLE_WGS"; } diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/RenderingI18nContext.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/RenderingI18nContext.java index b7bbb386b..c3b1be7da 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/RenderingI18nContext.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/RenderingI18nContext.java @@ -78,6 +78,7 @@ public class RenderingI18nContext extends I18nBase { public static final String CAPABILITY_CREATE_INT = "CAPABILITY_CREATE_INT"; public static final String GENERAL_CRIT = "GENERAL_CRIT"; public static final String CAPABILITY_DELETE_INT = "CAPABILITY_DELETE_INT"; + public static final String CAPABILITY_ERR_DET = "CAPABILITY_ERR_DET"; public static final String CAPABILITY_EXT_OP = "CAPABILITY_EXT_OP"; public static final String CAPABILITY_FHIR = "CAPABILITY_FHIR"; public static final String CAPABILITY_FHIR_VER = "CAPABILITY_FHIR_VER"; @@ -88,6 +89,8 @@ public class RenderingI18nContext extends I18nBase { public static final String CAPABILITY_INTER_SUPP = "CAPABILITY_INTER_SUPP"; public static final String CAPABILITY_INT_DESC = "CAPABILITY_INT_DESC"; public static final String CAPABILITY_INT_SUMM = "CAPABILITY_INT_SUMM"; + public static final String CAPABILITY_MAY_SUPP = "CAPABILITY_MAY_SUPP"; + public static final String CAPABILITY_MULT_EXT = "CAPABILITY_MULT_EXT"; public static final String CAPABILITY_NOTE_CAP = "CAPABILITY_NOTE_CAP"; public static final String CAPABILITY_OP = "CAPABILITY_OP"; public static final String CAPABILITY_OPER = "CAPABILITY_OPER"; @@ -99,6 +102,7 @@ public class RenderingI18nContext extends I18nBase { public static final String CAPABILITY_PATCH_INT = "CAPABILITY_PATCH_INT"; public static final String GENERAL_PROF = "GENERAL_PROF"; public static final String CAPABILITY_PROF_CONF = "CAPABILITY_PROF_CONF"; + public static final String CAPABILITY_PROF_RES_DOC = "CAPABILITY_PROF_RES_DOC"; public static final String CAPABILITY_PROF_MAP = "CAPABILITY_PROF_MAP"; public static final String CAPABILITY_PUB_BY = "CAPABILITY_PUB_BY"; public static final String CAPABILITY_PUB_ON = "CAPABILITY_PUB_ON"; @@ -106,6 +110,9 @@ public class RenderingI18nContext extends I18nBase { public static final String CAPABILITY_REF_PROF = "CAPABILITY_REF_PROF"; public static final String CAPABILITY_REQ_RECOM = "CAPABILITY_REQ_RECOM"; public static final String CAPABILITY_REST_CAPS = "CAPABILITY_REST_CAPS"; + public static final String CAPABILITY_DOCUMENT_CAPS = "CAPABILITY_DOCUMENT_CAPS"; + public static final String CAPABILITY_MESSAGING_CAPS = "CAPABILITY_MESSAGING_CAPS"; + public static final String CAPABILITY_MESSAGING_CAP = "CAPABILITY_MESSAGING_CAP"; public static final String CAPABILITY_REST_CONFIG = "CAPABILITY_REST_CONFIG"; public static final String CAPABILITY_RES_CONF = "CAPABILITY_RES_CONF"; public static final String CAPABILITY_RES_ENB = "CAPABILITY_RES_ENB"; @@ -126,7 +133,10 @@ public class RenderingI18nContext extends I18nBase { public static final String CAPABILITY_SUPP_FORM = "CAPABILITY_SUPP_FORM"; public static final String CAPABILITY_SUPP_PATCH_FORM = "CAPABILITY_SUPP_PATCH_FORM"; public static final String CAPABILITY_SUPP_PROFS = "CAPABILITY_SUPP_PROFS"; + public static final String CAPABILITY_SUPP_MSGS = "CAPABILITY_SUPP_MSGS"; + public static final String CAPABILITY_ENDPOINTS = "CAPABILITY_ENDPOINTS"; public static final String CAPABILITY_SUPP_THE = "CAPABILITY_SUPP_THE"; + public static final String CAPABILITY_SUPPS_THE = "CAPABILITY_SUPPS_THE"; public static final String GENERAL_TYPE = "GENERAL_TYPE"; public static final String CAPABILITY_TYPS = "CAPABILITY_TYPS"; public static final String CAPABILITY_TYP_PRES = "CAPABILITY_TYP_PRES"; diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/json/parser/JsonParser.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/json/parser/JsonParser.java index 9729cb4ce..df6efedc3 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/json/parser/JsonParser.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/json/parser/JsonParser.java @@ -682,7 +682,7 @@ public class JsonParser { break; case STRING: b.append("\""); - b.append(Utilities.escapeJson(((JsonString) e).getValue())); + b.append(Utilities.escapeJson(((JsonString) e).getValue(), false)); b.append("\""); break; default: diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/BasePackageCacheManager.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/BasePackageCacheManager.java index 7ac1ca647..13a387eee 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/BasePackageCacheManager.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/BasePackageCacheManager.java @@ -18,7 +18,7 @@ import org.slf4j.LoggerFactory; public abstract class BasePackageCacheManager implements IPackageCacheManager { private static final Logger ourLog = LoggerFactory.getLogger(BasePackageCacheManager.class); - protected List<PackageServer> myPackageServers = new ArrayList<>(); + protected final List<PackageServer> myPackageServers; private Function<PackageServer, PackageClient> myClientFactory = server -> new PackageClient(server); protected boolean silent; @@ -27,8 +27,12 @@ public abstract class BasePackageCacheManager implements IPackageCacheManager { */ public BasePackageCacheManager() { super(); + myPackageServers = new ArrayList<>(); } + protected BasePackageCacheManager(@Nonnull List<PackageServer> thePackageServers) { + myPackageServers = thePackageServers; + } /** * Provide a new client factory implementation */ diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/FilesystemPackageCacheLock.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/FilesystemPackageCacheLock.java deleted file mode 100644 index 33718015a..000000000 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/FilesystemPackageCacheLock.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.hl7.fhir.utilities.npm; - -import org.hl7.fhir.utilities.TextFile; -import org.hl7.fhir.utilities.Utilities; -import org.hl7.fhir.utilities.filesystem.ManagedFileAccess; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.channels.FileChannel; -import java.nio.channels.FileLock; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; - -public class FilesystemPackageCacheLock { - - private static final ConcurrentHashMap<File, ReadWriteLock> locks = new ConcurrentHashMap<>(); - - private final File lockFile; - - public FilesystemPackageCacheLock(File cacheFolder, String name) throws IOException { - this.lockFile = ManagedFileAccess.file(Utilities.path(cacheFolder.getAbsolutePath(), name + ".lock")); - if (!lockFile.isFile()) { - TextFile.stringToFile("", lockFile); - } - } - - public <T> T doWriteWithLock(FilesystemPackageCacheManager.CacheLockFunction<T> f) throws IOException { - - try (FileChannel channel = new RandomAccessFile(lockFile, "rw").getChannel()) { - locks.putIfAbsent(lockFile, new ReentrantReadWriteLock()); - ReadWriteLock lock = locks.get(lockFile); - lock.writeLock().lock(); - final FileLock fileLock = channel.lock(); - T result = null; - try { - result = f.get(); - } finally { - fileLock.release(); - lock.writeLock().unlock(); - } - if (!lockFile.delete()) { - lockFile.deleteOnExit(); - } - - return result; - } - } -} diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/FilesystemPackageCacheManager.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/FilesystemPackageCacheManager.java index c3ad924f2..df192cc19 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/FilesystemPackageCacheManager.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/FilesystemPackageCacheManager.java @@ -1,28 +1,16 @@ package org.hl7.fhir.utilities.npm; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; +import java.io.*; import java.text.ParseException; import java.text.SimpleDateFormat; -import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.UUID; +import java.util.*; import javax.annotation.Nonnull; import lombok.Getter; +import lombok.Setter; import lombok.With; import org.apache.commons.io.FileUtils; import org.hl7.fhir.exceptions.FHIRException; @@ -87,28 +75,33 @@ import org.slf4j.LoggerFactory; */ public class FilesystemPackageCacheManager extends BasePackageCacheManager implements IPackageCacheManager { - public static final String INI_TIMESTAMP_FORMAT = "yyyyMMddHHmmss"; + private final FilesystemPackageCacheManagerLocks locks; // When running in testing mode, some packages are provided from the test case repository rather than by the normal means // the PackageProvider is responsible for this. if no package provider is defined, or it declines to handle the package, // then the normal means will be used public interface IPackageProvider { boolean handlesPackage(String id, String version); + InputStreamWithSrc provide(String id, String version) throws IOException; } + private static IPackageProvider packageProvider; public static final String PACKAGE_REGEX = "^[a-zA-Z][A-Za-z0-9\\_\\-]*(\\.[A-Za-z0-9\\_\\-]+)+$"; public static final String PACKAGE_VERSION_REGEX = "^[A-Za-z][A-Za-z0-9\\_\\-]*(\\.[A-Za-z0-9\\_\\-]+)+\\#[A-Za-z0-9\\-\\_\\$]+(\\.[A-Za-z0-9\\-\\_\\$]+)*$"; public static final String PACKAGE_VERSION_REGEX_OPT = "^[A-Za-z][A-Za-z0-9\\_\\-]*(\\.[A-Za-z0-9\\_\\-]+)+(\\#[A-Za-z0-9\\-\\_]+(\\.[A-Za-z0-9\\-\\_]+)*)?$"; private static final Logger ourLog = LoggerFactory.getLogger(FilesystemPackageCacheManager.class); private static final String CACHE_VERSION = "3"; // second version - see wiki page - private File cacheFolder; - private boolean progress = true; - private List<NpmPackage> temporaryPackages = new ArrayList<>(); + @Nonnull + private final File cacheFolder; + + private final List<NpmPackage> temporaryPackages = new ArrayList<>(); private boolean buildLoaded = false; - private Map<String, String> ciList = new HashMap<String, String>(); + private final Map<String, String> ciList = new HashMap<>(); private JsonArray buildInfo; private boolean suppressErrors; + @Setter + @Getter private boolean minimalMemory; public static class Builder { @@ -116,7 +109,8 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple @Getter private final File cacheFolder; - @With @Getter + @With + @Getter private final List<PackageServer> packageServers; public Builder() throws IOException { @@ -127,6 +121,7 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple private File getUserCacheFolder() throws IOException { return ManagedFileAccess.file(Utilities.path(System.getProperty("user.home"), ".fhir", "packages")); } + private List<PackageServer> getPackageServersFromFHIRSettings() { List<PackageServer> packageServers = new ArrayList<>(getConfiguredServers()); if (!isIgnoreDefaultPackageServers()) { @@ -147,15 +142,16 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple protected List<PackageServer> getConfiguredServers() { return PackageServer.getConfiguredServers(); } + private Builder(File cacheFolder, List<PackageServer> packageServers) { this.cacheFolder = cacheFolder; this.packageServers = packageServers; } - public Builder withCacheFolder (String cacheFolderPath) throws IOException { + public Builder withCacheFolder(String cacheFolderPath) throws IOException { File cacheFolder = ManagedFileAccess.file(cacheFolderPath); if (!cacheFolder.exists()) { - throw new FHIRException("The folder '"+cacheFolder+"' could not be found"); + throw new FHIRException("The folder '" + cacheFolder + "' could not be found"); } return new Builder(cacheFolder, this.packageServers); } @@ -163,7 +159,7 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple public Builder withSystemCacheFolder() throws IOException { final File systemCacheFolder; if (Utilities.isWindows()) { - systemCacheFolder = ManagedFileAccess.file(Utilities.path(System.getenv("ProgramData"), ".fhir", "packages")); + systemCacheFolder = ManagedFileAccess.file(Utilities.path(System.getenv("ProgramData"), ".fhir", "packages")); } else { systemCacheFolder = ManagedFileAccess.file(Utilities.path("/var", "lib", ".fhir", "packages")); } @@ -179,22 +175,65 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple } } - private FilesystemPackageCacheManager(File cacheFolder, List<PackageServer> packageServers) throws IOException { + private FilesystemPackageCacheManager(@Nonnull File cacheFolder, @Nonnull List<PackageServer> packageServers) throws IOException { + super(packageServers); this.cacheFolder = cacheFolder; - this.myPackageServers = packageServers; - initCacheFolder(); + + try { + this.locks = FilesystemPackageCacheManagerLocks.getFilesystemPackageCacheManagerLocks(cacheFolder); + } catch (RuntimeException e) { + if (e.getCause() instanceof IOException) { + throw (IOException) e.getCause(); + } else { + throw e; + } + } + + prepareCacheFolder(); } - protected void initCacheFolder() throws IOException { - if (!(cacheFolder.exists())) - Utilities.createDirectory(cacheFolder.getAbsolutePath()); - String packagesIniPath = Utilities.path(cacheFolder, "packages.ini"); - File packagesIniFile = ManagedFileAccess.file(packagesIniPath); - if (!(packagesIniFile.exists())) - packagesIniFile.createNewFile(); - TextFile.stringToFile("[cache]\r\nversion=" + CACHE_VERSION + "\r\n\r\n[urls]\r\n\r\n[local]\r\n\r\n", packagesIniPath); - createIniFile(); - for (File f : cacheFolder.listFiles()) { + /** + * Check if the cache folder exists and is valid. + * <p> + * If it doesn't exist, create it. + * <p> + * If it does exist and isn't valid, delete it and create a new one. + * <p> + * If it does exist and is valid, just do some cleanup (delete temp download directories, etc.) + * + * @throws IOException if the cache folder can't be created + */ + protected void prepareCacheFolder() throws IOException { + locks.getCacheLock().doWriteWithLock(() -> { + + if (!(cacheFolder.exists())) { + Utilities.createDirectory(cacheFolder.getAbsolutePath()); + createIniFile(); + } else { + if (!isCacheFolderValid()) { + clearCache(); + createIniFile(); + } else { + deleteOldTempDirectories(); + } + } + return null; + }); + } + + private boolean isCacheFolderValid() throws IOException { + String iniPath = getPackagesIniPath(); + File iniFile = ManagedFileAccess.file(iniPath); + if (!(iniFile.exists())) { + return false; + } + IniFile ini = new IniFile(iniPath); + String v = ini.getStringProperty("cache", "version"); + return CACHE_VERSION.equals(v); + } + + private void deleteOldTempDirectories() throws IOException { + for (File f : Objects.requireNonNull(cacheFolder.listFiles())) { if (f.isDirectory() && Utilities.isValidUUID(f.getName())) { Utilities.clearDirectory(f.getAbsolutePath()); f.delete(); @@ -202,6 +241,7 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple } } + private void initPackageServers() { myPackageServers.addAll(getConfiguredServers()); if (!isIgnoreDefaultPackageServers()) { @@ -222,84 +262,43 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple return PackageServer.getConfiguredServers(); } - public boolean isMinimalMemory() { - return minimalMemory; - } - - public void setMinimalMemory(boolean minimalMemory) { - this.minimalMemory = minimalMemory; - } - - /** - * do not use this in minimal memory mode - * @param packagesFolder - * @throws IOException - */ - public void loadFromFolder(String packagesFolder) throws IOException { - assert !minimalMemory; - - File[] files = ManagedFileAccess.file(packagesFolder).listFiles(); - if (files != null) { - for (File f : files) { - if (f.getName().endsWith(".tgz")) { - FileInputStream fs = ManagedFileAccess.inStream(f); - try { - temporaryPackages.add(NpmPackage.fromPackage(fs)); - } finally { - fs.close(); - } - } - } - } - } - public String getFolder() { return cacheFolder.getAbsolutePath(); } private NpmPackage loadPackageInfo(String path) throws IOException { - File f = ManagedFileAccess.file(Utilities.path(path, "usage.ini")); - JsonObject j = f.exists() ? JsonParser.parseObject(f) : new JsonObject(); - j.set("date", new SimpleDateFormat("yyyy-MM-dd").format(new Date())); - JsonParser.compose(j, f, true); - - NpmPackage pi = minimalMemory ? NpmPackage.fromFolderMinimal(path) : NpmPackage.fromFolder(path); - return pi; + return minimalMemory ? NpmPackage.fromFolderMinimal(path, false) : NpmPackage.fromFolder(path, false); } private void clearCache() throws IOException { - for (File f : cacheFolder.listFiles()) { + for (File f : Objects.requireNonNull(cacheFolder.listFiles())) { if (f.isDirectory()) { - new FilesystemPackageCacheLock(cacheFolder, f.getName()).doWriteWithLock(() -> { - Utilities.clearDirectory(f.getAbsolutePath()); + Utilities.clearDirectory(f.getAbsolutePath()); + try { + FileUtils.deleteDirectory(f); + } catch (Exception e1) { try { FileUtils.deleteDirectory(f); - } catch (Exception e1) { - try { - FileUtils.deleteDirectory(f); - } catch (Exception e2) { - // just give up - } + } catch (Exception e2) { + // just give up } - return null; // must return something - }); - } else if (!f.getName().equals("packages.ini")) + } + + } else if (!f.getName().equals("packages.ini")) { FileUtils.forceDelete(f); + } + } - IniFile ini = new IniFile(Utilities.path(cacheFolder, "packages.ini")); - ini.removeSection("packages"); - ini.save(); } private void createIniFile() throws IOException { - IniFile ini = new IniFile(Utilities.path(cacheFolder, "packages.ini")); - boolean save = false; - String v = ini.getStringProperty("cache", "version"); - if (!CACHE_VERSION.equals(v)) { - clearCache(); - ini.setStringProperty("cache", "version", CACHE_VERSION, null); - ini.save(); - } + IniFile ini = new IniFile(getPackagesIniPath()); + ini.setStringProperty("cache", "version", CACHE_VERSION, null); + ini.save(); + } + + private String getPackagesIniPath() throws IOException { + return Utilities.path(cacheFolder, "packages.ini"); } private void checkValidVersionString(String version, String id) { @@ -316,23 +315,13 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple } } - private void listSpecs(Map<String, String> specList, PackageServer server) throws IOException { - PackageClient pc = new PackageClient(server); - List<PackageInfo> matches = pc.search(null, null, null, false); - for (PackageInfo m : matches) { - if (!specList.containsKey(m.getId())) { - specList.put(m.getId(), m.getUrl()); - } - } - } - protected InputStreamWithSrc loadFromPackageServer(String id, String version) { InputStreamWithSrc retVal = super.loadFromPackageServer(id, version); if (retVal != null) { return retVal; } - retVal = super.loadFromPackageServer(id, VersionUtilities.getMajMin(version)+".x"); + retVal = super.loadFromPackageServer(id, VersionUtilities.getMajMin(version) + ".x"); if (retVal != null) { return retVal; } @@ -344,7 +333,7 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple public String getLatestVersion(String id) throws IOException { for (PackageServer nextPackageServer : getPackageServers()) { // special case: - if (!(Utilities.existsInList(id,CommonPackages.ID_PUBPACK, "hl7.terminology.r5") && PackageServer.PRIMARY_SERVER.equals(nextPackageServer.getUrl()))) { + if (!(Utilities.existsInList(id, CommonPackages.ID_PUBPACK, "hl7.terminology.r5") && PackageServer.PRIMARY_SERVER.equals(nextPackageServer.getUrl()))) { PackageClient pc = new PackageClient(nextPackageServer); try { return pc.getLatestVersion(id); @@ -356,7 +345,7 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple try { return fetchVersionTheOldWay(id); } catch (Exception e) { - ourLog.info("Failed to determine latest version of package {} from server: {}", id, "build.fhir.org"); + ourLog.info("Failed to determine latest version of package {} from server: {}", id, "build.fhir.org"); } // still here? use the latest version we previously found or at least, is in the cache @@ -364,7 +353,7 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple if (version != null) { return version; } - throw new FHIRException("Unable to find the last version for package "+id+": no local copy, and no network access"); + throw new FHIRException("Unable to find the last version for package " + id + ": no local copy, and no network access"); } public String getLatestVersionFromCache(String id) throws IOException { @@ -372,8 +361,8 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple File cf = ManagedFileAccess.file(Utilities.path(cacheFolder, f)); if (cf.isDirectory()) { if (f.startsWith(id + "#")) { - String ver = f.substring(f.indexOf("#")+1); - ourLog.info("Latest version of package {} found locally is {} - using that", id, ver); + String ver = f.substring(f.indexOf("#") + 1); + ourLog.info("Latest version of package {} found locally is {} - using that", id, ver); return ver; } } @@ -399,10 +388,13 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple /** * Clear the cache * - * @throws IOException + * @throws IOException If the cache cannot be cleared */ public void clear() throws IOException { - clearCache(); + this.locks.getCacheLock().doWriteWithLock(() -> { + clearCache(); + return null; + }); } // ========================= Utilities ============================================================================ @@ -410,38 +402,39 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple /** * Remove a particular package from the cache * - * @param id - * @param ver - * @throws IOException + * @param id The id of the package to remove + * @param version The literal version of the package to remove. Values such as 'current' and 'dev' are not allowed. + * @throws IOException If the package cannot be removed */ - public void removePackage(String id, String ver) throws IOException { - new FilesystemPackageCacheLock(cacheFolder, id + "#" + ver).doWriteWithLock(() -> { - String f = Utilities.path(cacheFolder, id + "#" + ver); + public void removePackage(String id, String version) throws IOException { + locks.getPackageLock(id + "#" + version).doWriteWithLock(() -> { + + String f = Utilities.path(cacheFolder, id + "#" + version); File ff = ManagedFileAccess.file(f); if (ff.exists()) { Utilities.clearDirectory(f); - IniFile ini = new IniFile(Utilities.path(cacheFolder, "packages.ini")); - ini.removeProperty("packages", id + "#" + ver); - ini.save(); ff.delete(); } + return null; }); } /** * Load the identified package from the cache - if it exists - * <p> + * <p/> * This is for special purpose only (testing, control over speed of loading). + * <p/> * Generally, use the loadPackage method * - * @param id - * @param version - * @return - * @throws IOException + * @param id The id of the package to load + * @param version The version of the package to load. Values such as 'current' and 'dev' are allowed. + * @return The package, or null if it is not found + * @throws IOException If the package cannot be loaded */ @Override public NpmPackage loadPackageFromCacheOnly(String id, String version) throws IOException { + if (!Utilities.noString(version) && version.startsWith("file:")) { return loadPackageFromFile(id, version.substring(5)); } @@ -454,110 +447,144 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple return p; } } - String foundPackage = null; - String foundVersion = null; - for (String f : Utilities.reverseSorted(cacheFolder.list())) { - File cf = ManagedFileAccess.file(Utilities.path(cacheFolder, f)); - if (cf.isDirectory()) { - if (f.equals(id + "#" + version) || (Utilities.noString(version) && f.startsWith(id + "#"))) { - return loadPackageInfo(Utilities.path(cacheFolder, f)); + + String foundPackageFolder = findPackageFolder(id, version); + if (foundPackageFolder != null) { + NpmPackage foundPackage = locks.getPackageLock(foundPackageFolder).doReadWithLock(() -> { + String path = Utilities.path(cacheFolder, foundPackageFolder); + File directory = ManagedFileAccess.file(path); + + /* Check if the directory still exists now that we have a read lock. findPackageFolder does no locking in order + to avoid locking every potential package directory, so it's possible that a package deletion has occurred. + * */ + if (!directory.exists()) { + return null; } - if (version != null && !version.equals("current") && (version.endsWith(".x") || Utilities.charCount(version, '.') < 2) && f.contains("#")) { - String[] parts = f.split("#"); - if (parts[0].equals(id) && VersionUtilities.isMajMinOrLaterPatch((foundVersion!=null ? foundVersion : version),parts[1])) { - foundVersion = parts[1]; - foundPackage = f; + return loadPackageInfo(path); + }); + if (foundPackage != null) { + if (foundPackage.isIndexed()){ + return foundPackage; + } else { + return locks.getPackageLock(foundPackageFolder).doWriteWithLock(() -> { + File directory = ManagedFileAccess.file(foundPackage.getPath()); + + /* Check if the directory still exists now that we have a write lock. findPackageFolder does no locking in order + to avoid locking every potential package directory, so it's possible that a package deletion has occurred. + * */ + if (!directory.exists()) { + return null; + } + + // Since another thread may have already indexed the package since our read, we need to check again + NpmPackage output = loadPackageInfo(foundPackage.getPath()); + if (output.isIndexed()) { + return output; + } + String path = Utilities.path(cacheFolder, foundPackageFolder); + output.checkIndexed(path); + return output; + }); } - } } } - if (foundPackage!=null) { - return loadPackageInfo(Utilities.path(cacheFolder, foundPackage)); - } if ("dev".equals(version)) return loadPackageFromCacheOnly(id, "current"); else return null; } + private String findPackageFolder(String id, String version) throws IOException { + String foundPackageFolder = null; + String foundVersion = null; + for (String currentPackageFolder : Utilities.reverseSorted(cacheFolder.list())) { + File cf = ManagedFileAccess.file(Utilities.path(cacheFolder, currentPackageFolder)); + if (cf.isDirectory()) { + if (currentPackageFolder.equals(id + "#" + version) || (Utilities.noString(version) && currentPackageFolder.startsWith(id + "#"))) { + return currentPackageFolder; + } + if (version != null && !version.equals("current") && (version.endsWith(".x") || Utilities.charCount(version, '.') < 2) && currentPackageFolder.contains("#")) { + String[] parts = currentPackageFolder.split("#"); + if (parts[0].equals(id) && VersionUtilities.isMajMinOrLaterPatch((foundVersion != null ? foundVersion : version), parts[1])) { + foundVersion = parts[1]; + foundPackageFolder = currentPackageFolder; + } + } + } + } + return foundPackageFolder; + } + /** * Add an already fetched package to the cache */ @Override - public NpmPackage addPackageToCache(String id, String version, InputStream packageTgzInputStream, String sourceDesc) throws IOException { + public NpmPackage addPackageToCache(final String id, final String version, final InputStream packageTgzInputStream, final String sourceDesc) throws IOException { checkValidVersionString(version, id); - - String uuid = UUID.randomUUID().toString().toLowerCase(); - String tempDir = Utilities.path(cacheFolder, uuid); - NpmPackage npm = NpmPackage.extractFromTgz(packageTgzInputStream, sourceDesc, tempDir, minimalMemory); + return locks.getPackageLock(id + "#" + version).doWriteWithLock(() -> { + String uuid = UUID.randomUUID().toString().toLowerCase(); + String tempDir = Utilities.path(cacheFolder, uuid); + + NpmPackage npm = NpmPackage.extractFromTgz(packageTgzInputStream, sourceDesc, tempDir, minimalMemory); - if (progress) { log(""); - logn("Installing "+id+"#"+version); - } - - if ((npm.name() != null && id != null && !id.equalsIgnoreCase(npm.name()))) { - if (!suppressErrors && (!id.equals("hl7.fhir.r5.core") && !id.equals("hl7.fhir.us.immds"))) {// temporary work around - throw new IOException("Attempt to import a mis-identified package. Expected " + id + ", got " + npm.name()); - } - } - if (version == null) { - version = npm.version(); - } + log("Installing " + id + "#" + version); - String v = version; - return new FilesystemPackageCacheLock(cacheFolder, id + "#" + version).doWriteWithLock(() -> { - NpmPackage pck = null; - String packRoot = Utilities.path(cacheFolder, id + "#" + v); + if ((npm.name() != null && id != null && !id.equalsIgnoreCase(npm.name()))) { + if (!suppressErrors && (!id.equals("hl7.fhir.r5.core") && !id.equals("hl7.fhir.us.immds"))) {// temporary work around + throw new IOException("Attempt to import a mis-identified package. Expected " + id + ", got " + npm.name()); + } + } + + + NpmPackage npmPackage = null; + String packageRoot = Utilities.path(cacheFolder, id + "#" + version); try { // ok, now we have a lock on it... check if something created it while we were waiting - if (!ManagedFileAccess.file(packRoot).exists() || Utilities.existsInList(v, "current", "dev")) { - Utilities.createDirectory(packRoot); + if (!ManagedFileAccess.file(packageRoot).exists() || Utilities.existsInList(version, "current", "dev")) { + Utilities.createDirectory(packageRoot); try { - Utilities.clearDirectory(packRoot); + Utilities.clearDirectory(packageRoot); } catch (Throwable t) { - log("Unable to clear directory: "+packRoot+": "+t.getMessage()+" - this may cause problems later"); + log("Unable to clear directory: " + packageRoot + ": " + t.getMessage() + " - this may cause problems later"); } - Utilities.renameDirectory(tempDir, packRoot); + Utilities.renameDirectory(tempDir, packageRoot); - IniFile ini = new IniFile(Utilities.path(cacheFolder, "packages.ini")); - ini.setTimeStampFormat(INI_TIMESTAMP_FORMAT); - ini.setTimestampProperty("packages", id + "#" + v, ZonedDateTime.now(), null); - ini.setIntegerProperty("package-sizes", id + "#" + v, npm.getSize(), null); - ini.save(); - if (progress) - log(" done."); + log(" done."); } else { Utilities.clearDirectory(tempDir); ManagedFileAccess.file(tempDir).delete(); } - if (!id.equals(npm.getNpm().asString("name")) || !v.equals(npm.getNpm().asString("version"))) { + if (!id.equals(npm.getNpm().asString("name")) || !version.equals(npm.getNpm().asString("version"))) { if (!id.equals(npm.getNpm().asString("name"))) { npm.getNpm().add("original-name", npm.getNpm().asString("name")); npm.getNpm().remove("name"); npm.getNpm().add("name", id); } - if (!v.equals(npm.getNpm().asString("version"))) { + if (!version.equals(npm.getNpm().asString("version"))) { npm.getNpm().add("original-version", npm.getNpm().asString("version")); npm.getNpm().remove("version"); - npm.getNpm().add("version", v); + npm.getNpm().add("version", version); } - TextFile.stringToFile(JsonParser.compose(npm.getNpm(), true), Utilities.path(cacheFolder, id + "#" + v, "package", "package.json")); + TextFile.stringToFile(JsonParser.compose(npm.getNpm(), true), Utilities.path(cacheFolder, id + "#" + version, "package", "package.json")); + } + npmPackage = loadPackageInfo(packageRoot); + if (npmPackage != null && !npmPackage.isIndexed()) { + npmPackage.checkIndexed(packageRoot); } - pck = loadPackageInfo(packRoot); } catch (Exception e) { try { // don't leave a half extracted package behind - log("Clean up package " + packRoot + " because installation failed: " + e.getMessage()); + log("Clean up package " + packageRoot + " because installation failed: " + e.getMessage()); e.printStackTrace(); - Utilities.clearDirectory(packRoot); - ManagedFileAccess.file(packRoot).delete(); - } catch (Exception ei) { + Utilities.clearDirectory(packageRoot); + ManagedFileAccess.file(packageRoot).delete(); + } catch (Exception ignored) { // nothing } throw e; } - return pck; + return npmPackage; }); } @@ -567,12 +594,6 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple } } - private void logn(String s) { - if (!silent) { - System.out.print(s); - } - } - @Override public String getPackageUrl(String packageId) throws IOException { String result = super.getPackageUrl(packageId); @@ -583,14 +604,27 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple return result; } - public void listAllIds(Map<String, String> specList) throws IOException { - for (NpmPackage p : temporaryPackages) { - specList.put(p.name(), p.canonical()); + /** + * do not use this in minimal memory mode + * @param packagesFolder + * @throws IOException + */ + public void loadFromFolder(String packagesFolder) throws IOException { + assert !minimalMemory; + + File[] files = ManagedFileAccess.file(packagesFolder).listFiles(); + if (files != null) { + for (File f : files) { + if (f.getName().endsWith(".tgz")) { + FileInputStream fs = ManagedFileAccess.inStream(f); + try { + temporaryPackages.add(NpmPackage.fromPackage(fs)); + } finally { + fs.close(); + } + } + } } - for (PackageServer next : getPackageServers()) { - listSpecs(specList, next); - } - addCIBuildSpecs(specList); } @Override @@ -601,7 +635,7 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple } if (version == null && id.contains("#")) { - version = id.substring(id.indexOf("#")+1); + version = id.substring(id.indexOf("#") + 1); id = id.substring(0, id.indexOf("#")); } @@ -629,10 +663,8 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple version = "current"; } - if (progress) { - log("Installing " + id + "#" + (version == null ? "?" : version) + " to the package cache"); - log(" Fetching:"); - } + log("Installing " + id + "#" + (version == null ? "?" : version) + " to the package cache"); + log(" Fetching:"); // nup, don't have it locally (or it's expired) FilesystemPackageCacheManager.InputStreamWithSrc source; @@ -640,14 +672,14 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple source = packageProvider.provide(id, version); } else if (Utilities.isAbsoluteUrl(version)) { source = fetchSourceFromUrlSpecific(version); - } else if ("current".equals(version) || (version!= null && version.startsWith("current$"))) { + } else if ("current".equals(version) || (version != null && version.startsWith("current$"))) { // special case - fetch from ci-build server source = loadFromCIBuild(id, version.startsWith("current$") ? version.substring(8) : null); } else { source = loadFromPackageServer(id, version); } if (source == null) { - throw new FHIRException("Unable to find package "+id+"#"+version); + throw new FHIRException("Unable to find package " + id + "#" + version); } return addPackageToCache(id, source.version, source.stream, source.url); } @@ -665,7 +697,7 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple if (optional) return null; else - throw new FHIRException("Unable to fetch: "+e.getMessage(), e); + throw new FHIRException("Unable to fetch: " + e.getMessage(), e); } } @@ -677,18 +709,18 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple try { stream = fetchFromUrlSpecific(Utilities.pathURL(ciList.get(id), "package.tgz"), false); } catch (Exception e) { - stream = fetchFromUrlSpecific(Utilities.pathURL(ciList.get(id), "branches", "main", "package.tgz"), false); + stream = fetchFromUrlSpecific(Utilities.pathURL(ciList.get(id), "branches", "main", "package.tgz"), false); } return new InputStreamWithSrc(stream, Utilities.pathURL(ciList.get(id), "package.tgz"), "current"); } else { InputStream stream = fetchFromUrlSpecific(Utilities.pathURL(ciList.get(id), "branches", branch, "package.tgz"), false); - return new InputStreamWithSrc(stream, Utilities.pathURL(ciList.get(id), "branches", branch, "package.tgz"), "current$"+branch); + return new InputStreamWithSrc(stream, Utilities.pathURL(ciList.get(id), "branches", branch, "package.tgz"), "current$" + branch); } } else if (id.startsWith("hl7.fhir.r6")) { - InputStream stream = fetchFromUrlSpecific(Utilities.pathURL("http://build.fhir.org", id + ".tgz"), false); - return new InputStreamWithSrc(stream, Utilities.pathURL("http://build.fhir.org", id + ".tgz"), "current"); + InputStream stream = fetchFromUrlSpecific(Utilities.pathURL("https://build.fhir.org", id + ".tgz"), false); + return new InputStreamWithSrc(stream, Utilities.pathURL("https://build.fhir.org", id + ".tgz"), "current"); } else { - throw new FHIRException("The package '" + id + "' has no entry on the current build server ("+ciList.toString()+")"); + throw new FHIRException("The package '" + id + "' has no entry on the current build server (" + ciList + ")"); } } @@ -702,24 +734,14 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple return null; } - private void addCIBuildSpecs(Map<String, String> specList) throws IOException { - checkBuildLoaded(); - for (JsonElement n : buildInfo) { - JsonObject o = (JsonObject) n; - if (!specList.containsKey(o.asString("package-id"))) { - specList.put(o.asString("package-id"), o.asString("url")); - } - } - } - @Override public String getPackageId(String canonicalUrl) throws IOException { String retVal = findCanonicalInLocalCache(canonicalUrl); - - if(retVal == null) { + + if (retVal == null) { retVal = super.getPackageId(canonicalUrl); } - + if (retVal == null) { retVal = getPackageIdFromBuildList(canonicalUrl); } @@ -745,7 +767,7 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple // ========================= Package Mgmt API ======================================================================= - private String getPackageIdFromBuildList(String canonical) throws IOException { + private String getPackageIdFromBuildList(String canonical) { if (canonical == null) { return null; } @@ -767,7 +789,7 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple return null; } - private NpmPackage checkCurrency(String id, NpmPackage p) throws IOException { + private NpmPackage checkCurrency(String id, NpmPackage p) { checkBuildLoaded(); // special case: current versions roll over, and we have to check their currency try { @@ -779,7 +801,7 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple return null; // nup, we need a new copy } } catch (Exception e) { - log("Unable to check package currency: "+id+": "+id); + log("Unable to check package currency: " + id + ": " + id); } return p; } @@ -795,7 +817,6 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple loadFromBuildServer(); } catch (Exception e2) { log("Error connecting to build server - running without build (" + e2.getMessage() + ")"); - // e.printStackTrace(); } } } @@ -825,7 +846,7 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple ciList.put(bld.getPackageId(), "https://build.fhir.org/ig/" + bld.getRepo()); } } - buildLoaded = true; + buildLoaded = true; } private String getRepo(String path) { @@ -849,8 +870,8 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple if (url == null) { try { url = getPackageUrlFromBuildList(id); - } catch (Exception e) { - url = null; + } catch (Exception ignored) { + } } if (url == null) { @@ -860,16 +881,14 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple url = url.substring(0, url.indexOf("/ImplementationGuide/")); } String pu = Utilities.pathURL(url, "package-list.json"); - String aurl = pu; + PackageList pl; try { pl = PackageList.fromUrl(pu); } catch (Exception e) { String pv = Utilities.pathURL(url, v, "package.tgz"); try { - aurl = pv; - InputStreamWithSrc src = new InputStreamWithSrc(fetchFromUrlSpecific(pv, false), pv, v); - return src; + return new InputStreamWithSrc(fetchFromUrlSpecific(pv, false), pv, v); } catch (Exception e1) { throw new FHIRException("Error fetching package directly (" + pv + "), or fetching package list for " + id + " from " + pu + ": " + e1.getMessage(), e1); } @@ -878,7 +897,7 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple throw new FHIRException("Package ids do not match in " + pu + ": " + id + " vs " + pl.pid()); for (PackageListEntry vo : pl.versions()) { if (v.equals(vo.version())) { - aurl = Utilities.pathURL(vo.path(), "package.tgz"); + String u = Utilities.pathURL(vo.path(), "package.tgz"); return new InputStreamWithSrc(fetchFromUrlSpecific(u, true), u, v); } @@ -916,7 +935,7 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple private String getUrlForPackage(String id) { if (CommonPackages.ID_XVER.equals(id)) { - return "http://fhir.org/packages/hl7.fhir.xver-extensions"; + return "https://fhir.org/packages/hl7.fhir.xver-extensions"; } return null; } @@ -931,18 +950,6 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple return res; } - /** - * if you don't provide and implementation of this interface, the PackageCacheManager will use the web directly. - * <p> - * You can use this interface to - * - * @author graha - */ - public interface INetworkServices { - - InputStream resolvePackage(String packageId, String version); - } - public interface CacheLockFunction<T> { T get() throws IOException; } @@ -957,10 +964,10 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple public class BuildRecord { - private String url; - private String packageId; - private String repo; - private Date date; + private final String url; + private final String packageId; + private final String repo; + private final Date date; public BuildRecord(String url, String packageId, String repo, Date date) { super(); @@ -986,47 +993,6 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple return date; } - - } - - - - public class VersionHistory { - private String id; - private String canonical; - private String current; - private Map<String, String> versions = new HashMap<>(); - - public String getCanonical() { - return canonical; - } - - public String getCurrent() { - return current; - } - - public Map<String, String> getVersions() { - return versions; - } - - public String getId() { - return id; - } - } - - public class PackageEntry { - - private byte[] bytes; - private String name; - - public PackageEntry(String name) { - this.name = name; - } - - public PackageEntry(String name, byte[] bytes) { - this.name = name; - this.bytes = bytes; - } } public boolean packageExists(String id, String ver) throws IOException { @@ -1078,5 +1044,5 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple FilesystemPackageCacheManager.packageProvider = packageProvider; } - + } diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/FilesystemPackageCacheManagerLocks.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/FilesystemPackageCacheManagerLocks.java new file mode 100644 index 000000000..d674c750e --- /dev/null +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/FilesystemPackageCacheManagerLocks.java @@ -0,0 +1,213 @@ +package org.hl7.fhir.utilities.npm; + +import lombok.Getter; +import org.hl7.fhir.utilities.TextFile; +import org.hl7.fhir.utilities.Utilities; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.file.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +public class FilesystemPackageCacheManagerLocks { + + private static final ConcurrentHashMap<File, FilesystemPackageCacheManagerLocks> cacheFolderLockManagers = new ConcurrentHashMap<>(); + + @Getter + private final CacheLock cacheLock = new CacheLock(); + + private final ConcurrentHashMap<File, PackageLock> packageLocks = new ConcurrentHashMap<>(); + + private final File cacheFolder; + + private final Long lockTimeoutTime; + + private final TimeUnit lockTimeoutTimeUnit; + + /** + * This method is intended to be used only for testing purposes. + * <p/> + * To ensure that only one instance of the FilesystemPackageCacheManagerLocks is created for a given cacheFolder, use + * the static org.hl7.fhir.utilities.npm.FilesystemPackageCacheManagerLocks#getFilesystemPackageCacheManagerLocks(java.io.File) method. + * <p/> + * Get all the locks necessary to manage a filesystem cache. + * + * @param cacheFolder + * @throws IOException + */ + public FilesystemPackageCacheManagerLocks(File cacheFolder) throws IOException { + this(cacheFolder, 60L, TimeUnit.SECONDS); + } + + private FilesystemPackageCacheManagerLocks(File cacheFolder, Long lockTimeoutTime, TimeUnit lockTimeoutTimeUnit) throws IOException { + this.cacheFolder = cacheFolder; + this.lockTimeoutTime = lockTimeoutTime; + this.lockTimeoutTimeUnit = lockTimeoutTimeUnit; + } + + /** + * This method is intended to be used only for testing purposes. + */ + protected FilesystemPackageCacheManagerLocks withLockTimeout(Long lockTimeoutTime, TimeUnit lockTimeoutTimeUnit) throws IOException { + return new FilesystemPackageCacheManagerLocks(cacheFolder, lockTimeoutTime, lockTimeoutTimeUnit); + } + + /** + * Returns a single FilesystemPackageCacheManagerLocks instance for the given cacheFolder. + * <p/> + * If an instance already exists, it is returned. Otherwise, a new instance is created. + * <p/> + * Using this method ensures that only one instance of FilesystemPackageCacheManagerLocks is created for a given + * cacheFolder, which is useful if multiple ValidationEngine instances are running in parallel. + * + * @param cacheFolder + * @return + * @throws IOException + */ + public static FilesystemPackageCacheManagerLocks getFilesystemPackageCacheManagerLocks(File cacheFolder) throws IOException { + return cacheFolderLockManagers.computeIfAbsent(cacheFolder, k -> { + try { + return new FilesystemPackageCacheManagerLocks(k); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + public class CacheLock { + private final ReadWriteLock lock; + + protected CacheLock() { + lock = new ReentrantReadWriteLock(); + } + + public ReadWriteLock getLock() { + return lock; + } + + public <T> T doWriteWithLock(FilesystemPackageCacheManager.CacheLockFunction<T> f) throws IOException { + lock.writeLock().lock(); + T result = null; + try { + result = f.get(); + } finally { + lock.writeLock().unlock(); + } + return result; + } + } + + public class PackageLock { + @Getter + private final File lockFile; + private final ReadWriteLock lock; + + protected PackageLock(File lockFile, ReadWriteLock lock) { + this.lockFile = lockFile; + this.lock = lock; + } + + private void checkForLockFileWaitForDeleteIfExists(File lockFile) throws IOException { + if (!lockFile.exists()) { + return; + } + try (WatchService watchService = FileSystems.getDefault().newWatchService()) { + Path dir = lockFile.getParentFile().toPath(); + dir.register(watchService, StandardWatchEventKinds.ENTRY_DELETE); + + WatchKey key = watchService.poll(lockTimeoutTime, lockTimeoutTimeUnit); + if (key == null) { + // It is possible that the lock file is deleted before the watch service is registered, so if we timeout at + // this point, we should check if the lock file still exists. + if (lockFile.exists()) { + throw new TimeoutException("Timeout waiting for lock file deletion: " + lockFile.getName()); + } + } else { + for (WatchEvent<?> event : key.pollEvents()) { + WatchEvent.Kind<?> kind = event.kind(); + if (kind == StandardWatchEventKinds.ENTRY_DELETE) { + Path deletedFilePath = (Path) event.context(); + if (deletedFilePath.toString().equals(lockFile.getName())) { + return; + } + } + key.reset(); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Error reading package.", e); + } catch (TimeoutException e) { + throw new IOException("Error reading package.", e); + } + } + + + public <T> T doReadWithLock(FilesystemPackageCacheManager.CacheLockFunction<T> f) throws IOException { + cacheLock.getLock().readLock().lock(); + lock.readLock().lock(); + + checkForLockFileWaitForDeleteIfExists(lockFile); + + T result = null; + try { + result = f.get(); + } finally { + lock.readLock().unlock(); + cacheLock.getLock().readLock().unlock(); + } + return result; + } + + public <T> T doWriteWithLock(FilesystemPackageCacheManager.CacheLockFunction<T> f) throws IOException { + cacheLock.getLock().writeLock().lock(); + lock.writeLock().lock(); + + if (!lockFile.isFile()) { + try { + TextFile.stringToFile("", lockFile); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + try (FileChannel channel = new RandomAccessFile(lockFile, "rw").getChannel()) { + FileLock fileLock = null; + while (fileLock == null) { + fileLock = channel.tryLock(0, Long.MAX_VALUE, true); + if (fileLock == null) { + Thread.sleep(100); // Wait and retry + } + } + T result = null; + try { + result = f.get(); + } finally { + fileLock.release(); + channel.close(); + if (!lockFile.delete()) { + lockFile.deleteOnExit(); + } + lock.writeLock().unlock(); + cacheLock.getLock().writeLock().unlock(); + } + return result; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Thread interrupted while waiting for lock", e); + } + } + } + + public synchronized PackageLock getPackageLock(String packageName) throws IOException { + File lockFile = new File(Utilities.path(cacheFolder.getAbsolutePath(), packageName + ".lock")); + return packageLocks.computeIfAbsent(lockFile, (k) -> new PackageLock(k, new ReentrantReadWriteLock())); + } +} diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/NpmPackage.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/NpmPackage.java index 8ec6b1dd7..5ab8a9113 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/NpmPackage.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/NpmPackage.java @@ -357,9 +357,15 @@ public class NpmPackage { * Factory method that parses a package from an extracted folder */ public static NpmPackage fromFolder(String path) throws IOException { + return fromFolder(path, true); + } + + public static NpmPackage fromFolder(String path, boolean checkIndexed) throws IOException { NpmPackage res = new NpmPackage(); res.loadFiles(path, ManagedFileAccess.file(path)); - res.checkIndexed(path); + if (checkIndexed) { + res.checkIndexed(path); + } return res; } @@ -367,10 +373,15 @@ public class NpmPackage { * Factory method that parses a package from an extracted folder */ public static NpmPackage fromFolderMinimal(String path) throws IOException { + return fromFolderMinimal(path, true); + } + + public static NpmPackage fromFolderMinimal(String path, boolean checkIndexed) throws IOException { NpmPackage res = new NpmPackage(); res.minimalMemory = true; res.loadFiles(path, ManagedFileAccess.file(path)); - res.checkIndexed(path); + if (checkIndexed) { + res.checkIndexed(path);} return res; } @@ -616,7 +627,18 @@ public class NpmPackage { index.content.put(n, data); } - private void checkIndexed(String desc) throws IOException { + public boolean isIndexed() throws IOException { + for (NpmPackageFolder folder : folders.values()) { + JsonObject index = folder.index(); + if (folder.index() == null || index.forceArray("files").size() == 0) { + return false; + } + } + return true; + } + + + public void checkIndexed(String desc) throws IOException { for (NpmPackageFolder folder : folders.values()) { JsonObject index = folder.index(); if (index == null || index.forceArray("files").size() == 0) { @@ -626,6 +648,7 @@ public class NpmPackage { } + public void indexFolder(String desc, NpmPackageFolder folder) throws FileNotFoundException, IOException { List<String> remove = new ArrayList<>(); NpmPackageIndexBuilder indexer = new NpmPackageIndexBuilder(); diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/NpmPackageIndexBuilder.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/NpmPackageIndexBuilder.java index 34efb8a49..1420c14a2 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/NpmPackageIndexBuilder.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/NpmPackageIndexBuilder.java @@ -129,7 +129,7 @@ public class NpmPackageIndexBuilder { } } } catch (Exception e) { - System.out.println("Error parsing "+name+": "+e.getMessage()); +// System.out.println("Error parsing "+name+": "+e.getMessage()); if (name.contains("openapi")) { return false; } diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xls/XLSXmlNormaliser.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xls/XLSXmlNormaliser.java index add5a4b01..270d9700b 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xls/XLSXmlNormaliser.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xls/XLSXmlNormaliser.java @@ -229,7 +229,7 @@ public class XLSXmlNormaliser { private void saveXml(FileOutputStream stream) throws TransformerException, IOException { - TransformerFactory factory = TransformerFactory.newInstance(); + TransformerFactory factory = XMLUtil.newXXEProtectedTransformerFactory(); Transformer transformer = factory.newTransformer(); Result result = new StreamResult(stream); Source source = new DOMSource(xml); diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xml/XMLUtil.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xml/XMLUtil.java index a68dbe688..371f44705 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xml/XMLUtil.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xml/XMLUtil.java @@ -42,6 +42,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Set; +import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; @@ -501,8 +502,16 @@ public class XMLUtil { return e == null ? null : e.getAttribute(aname); } - public static void writeDomToFile(Document doc, String filename) throws TransformerException, IOException { + public static TransformerFactory newXXEProtectedTransformerFactory() { TransformerFactory transformerFactory = TransformerFactory.newInstance(); + transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); + return transformerFactory; + } + + + public static void writeDomToFile(Document doc, String filename) throws TransformerException, IOException { + TransformerFactory transformerFactory = XMLUtil.newXXEProtectedTransformerFactory(); Transformer transformer = transformerFactory.newTransformer(); DOMSource source = new DOMSource(doc); StreamResult streamResult = new StreamResult(ManagedFileAccess.file(filename)); @@ -593,7 +602,7 @@ public class XMLUtil { } public static void saveToFile(Element root, OutputStream stream) throws TransformerException { - Transformer transformer = TransformerFactory.newInstance().newTransformer(); + Transformer transformer = XMLUtil.newXXEProtectedTransformerFactory().newTransformer(); Result output = new StreamResult(stream); Source input = new DOMSource(root); diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index d3f4df145..bae41d4af 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -1137,4 +1137,7 @@ TYPE_SPECIFIC_CHECKS_DT_XHTML_UNKNOWN_HREF = Hyperlink scheme ''{3}'' in ''{0}'' TYPE_SPECIFIC_CHECKS_DT_XHTML_LITERAL_HREF = Hyperlink scheme ''{3}'' in ''{0}'' at ''{1}'' for ''{2}'' is not a valid hyperlinkable scheme SM_TARGET_TYPE_UNKNOWN = The type of the target variable is not known: {0} XHTML_XHTML_ATTRIBUTE_XML_SPACE = The attribute 'xml:space' is legal but has a fixed value of 'preserve'. It''s use is discouraged - \ No newline at end of file +VALIDATION_HL7_PUBLISHER_MULTIPLE_WGS = This resource has more than workgroup extension (http://hl7.org/fhir/StructureDefinition/structuredefinition-wg) +NO_VALID_DISPLAY_FOUND_NONE_FOR_LANG = Wrong Display Name ''{0}'' for {1}#{2}. There are no valid display names found for language(s) ''{3}''. Default display is ''{4}'' +NO_VALID_DISPLAY_AT_ALL = Cannot validate display Name ''{0}'' for {1}#{2}: No displays are known + diff --git a/org.hl7.fhir.utilities/src/main/resources/rendering-phrases.properties b/org.hl7.fhir.utilities/src/main/resources/rendering-phrases.properties index dd07f4549..0147ecbe4 100644 --- a/org.hl7.fhir.utilities/src/main/resources/rendering-phrases.properties +++ b/org.hl7.fhir.utilities/src/main/resources/rendering-phrases.properties @@ -74,6 +74,7 @@ CAPABILITY_CORS_YES = Enable CORS: yes CAPABILITY_CREATE_INT = POST a new resource (create interaction) GENERAL_CRIT = Criteria CAPABILITY_DELETE_INT = DELETE a resource (delete interaction) +CAPABILITY_ERR_DET = Error detected CAPABILITY_EXT_OP = Extended Operations CAPABILITY_FHIR = Core FHIR Resource CAPABILITY_FHIR_VER = FHIR Version: {0} @@ -84,6 +85,8 @@ CAPABILITY_INT = interaction. CAPABILITY_INTER_SUPP = The interactions supported by each resource ( CAPABILITY_INT_DESC = interaction described as follows: CAPABILITY_INT_SUMM = Interaction summary +CAPABILITY_MAY_SUPP = MAY Support the Following Implementation Guides +CAPABILITY_MULT_EXT = this mark indicates that there are more than one expectation extensions present CAPABILITY_NOTE_CAP = Note to Implementers: FHIR Capabilities CAPABILITY_OP = Operations CAPABILITY_OPER = Operation @@ -95,6 +98,7 @@ PARS_SUMMARY_LIST = Parameters: {0} CAPABILITY_PATCH_INT = PATCH a new resource version (patch interaction) GENERAL_PROF = Profile CAPABILITY_PROF_CONF = Profile Conformance +CAPABILITY_PROF_RES_DOC = Document Resource Profile CAPABILITY_PROF_MAP = Profile Mapping CAPABILITY_PUB_BY = Published by: {0} CAPABILITY_PUB_ON = Published on: {0} @@ -102,6 +106,9 @@ CAPABILITY_READ_INT = GET a resource (read interaction) CAPABILITY_REF_PROF = Reference Policy CAPABILITY_REQ_RECOM = Required and recommended search parameters CAPABILITY_REST_CAPS = FHIR RESTful Capabilities +CAPABILITY_DOCUMENT_CAPS = FHIR Document Capabilities +CAPABILITY_MESSAGING_CAPS = FHIR Messaging Capabilities +CAPABILITY_MESSAGING_CAP = Messaging Capability CAPABILITY_REST_CONFIG = REST Configuration: {0} CAPABILITY_RES_CONF = Resource Conformance: {0} CAPABILITY_RES_ENB = The linked resources enabled for @@ -122,7 +129,10 @@ CAPABILITY_SUPPS = Supports CAPABILITY_SUPP_FORM = Supported Formats: CAPABILITY_SUPP_PATCH_FORM = Supported Patch Formats: CAPABILITY_SUPP_PROFS = Supported Profiles -CAPABILITY_SUPP_THE = Supports the +CAPABILITY_SUPP_MSGS = Supported Message(s) +CAPABILITY_ENDPOINTS = Endpoint(s) +CAPABILITY_SUPP_THE = support the +CAPABILITY_SUPPS_THE = Supports the GENERAL_TYPE = Type CAPABILITY_TYPS = Types CAPABILITY_TYP_PRES = ype are only present if at least one of the resources has support for them. diff --git a/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/SimpleHTTPClientTest.java b/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/SimpleHTTPClientTest.java index 866894056..3973cdd94 100644 --- a/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/SimpleHTTPClientTest.java +++ b/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/SimpleHTTPClientTest.java @@ -1,24 +1,111 @@ package org.hl7.fhir.utilities; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; import java.io.IOException; +import java.util.stream.Stream; +import okhttp3.HttpUrl; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; import org.hl7.fhir.utilities.http.HTTPResult; import org.hl7.fhir.utilities.http.SimpleHTTPClient; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; public class SimpleHTTPClientTest { + + private MockWebServer server; + + @BeforeEach + void setup() { + setupMockServer(); + } + + void setupMockServer() { + server = new MockWebServer(); + } + @Test - public void testSimpleHTTPClient() throws IOException { + public void testGetApplicationJson() throws IOException, InterruptedException { + + HttpUrl serverUrl = server.url("fhir/us/core/package-list.json?nocache=1724353440974"); + + server.enqueue( + new MockResponse() + .setBody("Monkeys").setResponseCode(200) + ); SimpleHTTPClient http = new SimpleHTTPClient(); - String url = "https://hl7.org/fhir/us/core/package-list.json?nocache=" + System.currentTimeMillis(); - HTTPResult res = http.get(url, "application/json"); + HTTPResult res = http.get(serverUrl.url().toString(), "application/json"); + + assertThat(res.getCode()).isEqualTo(200); + + RecordedRequest packageRequest = server.takeRequest(); + + assert packageRequest.getRequestUrl() != null; + assertThat(packageRequest.getRequestUrl().toString()).isEqualTo(serverUrl.url().toString()); + assertThat(packageRequest.getMethod()).isEqualTo("GET"); + assertThat(packageRequest.getHeader("Accept")).isEqualTo("application/json"); -// System.out.println(res.getCode()); -// System.out.println(new String(res.getContent(), StandardCharsets.UTF_8)); - assertTrue(res.getCode() != 400); } + + public static Stream<Arguments> getRedirectArgs() { + return Stream.of( + Arguments.of(301, new String[]{"url1", "url2"}), + Arguments.of(301, new String[]{"url1", "url2", "url3"}), + Arguments.of(301, new String[]{"url1", "url2", "url3", "url4"}), + Arguments.of(302, new String[]{"url1", "url2"}), + Arguments.of(302, new String[]{"url1", "url2", "url3"}), + Arguments.of(302, new String[]{"url1", "url2", "url3", "url4"}), + Arguments.of(307, new String[]{"url1", "url2"}), + Arguments.of(307, new String[]{"url1", "url2", "url3"}), + Arguments.of(307, new String[]{"url1", "url2", "url3", "url4"}), + Arguments.of(308, new String[]{"url1", "url2"}), + Arguments.of(308, new String[]{"url1", "url2", "url3"}), + Arguments.of(308, new String[]{"url1", "url2", "url3", "url4"}) + ); + } + + @ParameterizedTest + @MethodSource("getRedirectArgs") + public void testRedirectsGet(int code, String[] urlArgs) throws IOException, InterruptedException { + + HttpUrl[] urls = new HttpUrl[urlArgs.length]; + for (int i = 0; i < urlArgs.length; i++) { + urls[i] = server.url(urlArgs[i]); + if (i > 0) { + server.enqueue( + new MockResponse() + .setResponseCode(code) + .setBody("Pumas") + .addHeader("Location", urls[i].url().toString())); + } + } + server.enqueue( + new MockResponse() + .setBody("Monkeys").setResponseCode(200) + ); + HttpUrl[] url = urls; + + SimpleHTTPClient http = new SimpleHTTPClient(); + + HTTPResult res = http.get(url[0].url().toString(), "application/json"); + + assertThat(res.getCode()).isEqualTo(200); + assertThat(res.getContentAsString()).isEqualTo("Monkeys"); + assertThat(server.getRequestCount()).isEqualTo(urlArgs.length); + + for (int i = 0; i < urlArgs.length; i++) { + RecordedRequest packageRequest = server.takeRequest(); + assertThat(packageRequest.getMethod()).isEqualTo("GET"); + assertThat(packageRequest.getHeader("Accept")).isEqualTo("application/json"); + } + } + } \ No newline at end of file diff --git a/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/TextFileTest.java b/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/TextFileTest.java index 0463ca0e9..5e4dd38a1 100644 --- a/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/TextFileTest.java +++ b/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/TextFileTest.java @@ -3,6 +3,7 @@ package org.hl7.fhir.utilities; import org.hl7.fhir.utilities.filesystem.ManagedFileAccess; import org.junit.jupiter.api.*; + import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -23,6 +24,7 @@ import org.junit.jupiter.api.*; private static final String SAMPLE_CONTENT = "Line 1\nLine 2\nLine 3"; private static final List<String> SAMPLE_CONTENT_LINES = List.of("Line 1", "Line 2", "Line 3"); private static final String BOM = "\uFEFF"; + private static final byte[] BOM_BYTES = new byte[]{(byte)239, (byte)187, (byte)191}; private static File readFile; private final static List<File> createdFiles = new ArrayList<>(4); @@ -104,6 +106,33 @@ import org.junit.jupiter.api.*; assertArrayEquals(SAMPLE_CONTENT.getBytes(StandardCharsets.UTF_8), read); } + @Test + void testBytesToFile() throws IOException { + final var writeFile = createTempFile(); + TextFile.bytesToFile(BOM_BYTES, writeFile); + assertArrayEquals(BOM_BYTES, Files.readAllBytes(writeFile.toPath())); + } + + @Test + void testAppendBytesToFile() throws IOException { + final var writeFile = createTempFile(); + TextFile.bytesToFile(BOM_BYTES, writeFile); + assertArrayEquals(BOM_BYTES, Files.readAllBytes(writeFile.toPath())); + + TextFile.appendBytesToFile(SAMPLE_CONTENT.getBytes(StandardCharsets.UTF_8), writeFile.getAbsolutePath()); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream( ); + outputStream.write( BOM_BYTES ); + outputStream.write(new byte[] {13, 10}); //newline + outputStream.write( SAMPLE_CONTENT.getBytes(StandardCharsets.UTF_8) ); + + byte[] expected = outputStream.toByteArray(); + + byte[] actual = Files.readAllBytes(writeFile.toPath()); + assertArrayEquals(expected, actual); + + } + @Test void testStringToFile() throws IOException { final var writeFile = createTempFile(); diff --git a/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/UtilitiesTest.java b/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/UtilitiesTest.java index be8983216..386033994 100644 --- a/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/UtilitiesTest.java +++ b/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/UtilitiesTest.java @@ -182,7 +182,7 @@ class UtilitiesTest { assertEquals("0.95", Utilities.lowBoundaryForDecimal("1.0", 2)); assertEquals("-1.05000000", Utilities.lowBoundaryForDecimal("-1.0", 8)); assertEquals("1.23", Utilities.lowBoundaryForDecimal("1.234", 2)); - assertEquals("1.57", Utilities.lowBoundaryForDecimal("1.567", 2)); + assertEquals("1.56", Utilities.lowBoundaryForDecimal("1.567", 2)); assertEquals("0.50000000", Utilities.highBoundaryForDecimal("0", 8)); assertEquals("1.500000", Utilities.highBoundaryForDecimal("1", 6)); diff --git a/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/npm/FilesystemPackageManagerLockTests.java b/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/npm/FilesystemPackageManagerLockTests.java new file mode 100644 index 000000000..f1d71dda3 --- /dev/null +++ b/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/npm/FilesystemPackageManagerLockTests.java @@ -0,0 +1,227 @@ +package org.hl7.fhir.utilities.npm; + +import org.hl7.fhir.utilities.TextFile; +import org.hl7.fhir.utilities.filesystem.ManagedFileAccess; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class FilesystemPackageManagerLockTests { + + public static final String DUMMY_PACKAGE = "dummy#1.2.3"; + String cachePath; + File cacheDirectory; + FilesystemPackageCacheManagerLocks filesystemPackageCacheLockManager; + + + @BeforeEach + public void setUp() throws IOException { + cachePath = ManagedFileAccess.fromPath(Files.createTempDirectory("fpcm-multithreadingTest")).getAbsolutePath(); + cacheDirectory = new File(cachePath); + filesystemPackageCacheLockManager = new FilesystemPackageCacheManagerLocks(cacheDirectory); + + } + + @Test + public void testBaseCases() throws IOException { + filesystemPackageCacheLockManager.getCacheLock().doWriteWithLock(() -> { + assertThat(cacheDirectory).exists(); + assertThat(cacheDirectory).isDirectory(); + assertThat(cacheDirectory).canWrite(); + assertThat(cacheDirectory).canRead(); + return null; + }); + + + final FilesystemPackageCacheManagerLocks.PackageLock packageLock = filesystemPackageCacheLockManager.getPackageLock(DUMMY_PACKAGE); + packageLock.doWriteWithLock(() -> { + assertThat(packageLock.getLockFile()).exists(); + return null; + }); + assertThat(packageLock.getLockFile()).doesNotExist(); + + packageLock.doReadWithLock(() -> { + assertThat(packageLock.getLockFile()).doesNotExist(); + return null; + }); + } + + @Test void testNoPackageWriteOrReadWhileWholeCacheIsLocked() throws IOException, InterruptedException { + final FilesystemPackageCacheManagerLocks.PackageLock packageLock = filesystemPackageCacheLockManager.getPackageLock(DUMMY_PACKAGE); + + AtomicBoolean cacheLockFinished = new AtomicBoolean(false); + List<Thread> threadList = new ArrayList<>(); + + Thread cacheThread = new Thread(() -> { + try { + filesystemPackageCacheLockManager.getCacheLock().doWriteWithLock(() -> { + try { + Thread.sleep(300); + cacheLockFinished.set(true); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return null; + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + cacheThread.start(); + Thread.sleep(100); + for (int i = 0; i < 5; i++) { + threadList.add(new Thread(() -> { + try { + packageLock.doWriteWithLock(() -> { + assertThat(cacheLockFinished.get()).isTrue(); + return null; + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + })); + threadList.add(new Thread(() -> { + try { + packageLock.doReadWithLock(() -> { + assertThat(cacheLockFinished.get()).isTrue(); + return null; + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + })); + } + + for (Thread thread: threadList) { + thread.start(); + } + for (Thread thread: threadList) { + try { + thread.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + @Test void testSinglePackageWriteMultiPackageRead() throws IOException { + final FilesystemPackageCacheManagerLocks.PackageLock packageLock = filesystemPackageCacheLockManager.getPackageLock(DUMMY_PACKAGE); + AtomicInteger writeCounter = new AtomicInteger(0); + + AtomicInteger readCounter = new AtomicInteger(0); + List<Thread> threadList = new ArrayList<>(); + + AtomicInteger maxReadThreads = new AtomicInteger(); + + for (int i = 0; i < 10; i++) { + threadList.add(new Thread(() -> { + try { + packageLock.doWriteWithLock(() -> { + int writeCount = writeCounter.incrementAndGet(); + assertThat(writeCount).isEqualTo(1); + writeCounter.decrementAndGet(); + return null; + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + })); + } + + for (int i = 0; i < 10; i++) { + threadList.add(new Thread(() -> { + try { + packageLock.doReadWithLock(() -> { + int readCount = readCounter.incrementAndGet(); + try { + Thread.sleep(100); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + assertThat(readCount).isGreaterThan(0); + if (readCount > maxReadThreads.get()) { + maxReadThreads.set(readCount); + } + readCounter.decrementAndGet(); + return null; + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + })); + } + + for (Thread thread: threadList) { + thread.start(); + } + + for (Thread thread: threadList) { + try { + thread.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + assertThat(maxReadThreads.get()).isGreaterThan(1); + } + + @Test + public void testReadWhenLockedByFileTimesOut() throws IOException { + FilesystemPackageCacheManagerLocks shorterTimeoutManager = filesystemPackageCacheLockManager.withLockTimeout(3L, TimeUnit.SECONDS); + final FilesystemPackageCacheManagerLocks.PackageLock packageLock = shorterTimeoutManager.getPackageLock(DUMMY_PACKAGE); + File lockFile = createPackageLockFile(); + + Exception exception = assertThrows(IOException.class, () -> { + packageLock.doReadWithLock(() -> { + assertThat(lockFile).exists(); + return null; + }); + }); + + assertThat(exception.getMessage()).contains("Error reading package"); + assertThat(exception.getCause().getMessage()).contains("Timeout waiting for lock file deletion: " + lockFile.getName()); + } + + @Test + public void testReadWhenLockFileIsDeleted() throws IOException { + FilesystemPackageCacheManagerLocks shorterTimeoutManager = filesystemPackageCacheLockManager.withLockTimeout(5L, TimeUnit.SECONDS); + final FilesystemPackageCacheManagerLocks.PackageLock packageLock = shorterTimeoutManager.getPackageLock(DUMMY_PACKAGE); + File lockFile = createPackageLockFile(); + + Thread t = new Thread(() -> { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + lockFile.delete(); + }); + t.start(); + + packageLock.doReadWithLock(() -> { + assertThat(lockFile).doesNotExist(); + return null; + }); + + } + + private File createPackageLockFile() throws IOException { + File lockFile = Path.of(cachePath, DUMMY_PACKAGE + ".lock").toFile(); + TextFile.stringToFile("", lockFile); + return lockFile; + } + +} diff --git a/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/npm/FilesystemPackageManagerTests.java b/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/npm/FilesystemPackageManagerTests.java index 156abef30..96754c2b8 100644 --- a/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/npm/FilesystemPackageManagerTests.java +++ b/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/npm/FilesystemPackageManagerTests.java @@ -3,38 +3,48 @@ package org.hl7.fhir.utilities.npm; import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.File; + import java.io.IOException; import java.nio.file.Files; import java.util.ArrayList; import java.util.List; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; import javax.annotation.Nonnull; import org.hl7.fhir.utilities.filesystem.ManagedFileAccess; +import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; public class FilesystemPackageManagerTests { - private static final String DUMMY_URL_1 = "http://dummy1.org"; - private static final String DUMMY_URL_2 = "http://dummy2.org"; + private static final String DUMMY_URL_1 = "https://dummy1.org"; + private static final String DUMMY_URL_2 = "https://dummy2.org"; - private static final String DUMMY_URL_3 = "http://dummy3.org"; + private static final String DUMMY_URL_3 = "https://dummy3.org"; - private static final String DUMMY_URL_4 = "http://dummy4.org"; - private List<PackageServer> dummyPrivateServers = List.of( + private static final String DUMMY_URL_4 = "https://dummy4.org"; + private final List<PackageServer> dummyPrivateServers = List.of( new PackageServer(DUMMY_URL_1), new PackageServer(DUMMY_URL_2) ); - private List<PackageServer> dummyDefaultServers = List.of( + private final List<PackageServer> dummyDefaultServers = List.of( new PackageServer(DUMMY_URL_3), new PackageServer(DUMMY_URL_4) ); + + @Test public void testDefaultServers() throws IOException { FilesystemPackageCacheManager filesystemPackageCacheManager = getFilesystemPackageCacheManager(false); @@ -101,24 +111,56 @@ public class FilesystemPackageManagerTests { assertEquals( System.getenv("ProgramData") + "\\.fhir\\packages", folder.getAbsolutePath()); } - @Test - public void multithreadingTest() throws IOException { + /** + We repeat the same tests multiple times here, in order to catch very rare edge cases. + */ + public static Stream<Arguments> packageCacheMultiThreadTestParams() { + List<Arguments> params = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + params.add(Arguments.of(100, 1)); + params.add(Arguments.of(10,10)); + params.add(Arguments.of(100, 10)); + } + return params.stream(); + } + + @MethodSource("packageCacheMultiThreadTestParams") + @ParameterizedTest + public void packageCacheMultiThreadTest(final int threadTotal, final int packageCacheManagerTotal) throws IOException { + String pcmPath = ManagedFileAccess.fromPath(Files.createTempDirectory("fpcm-multithreadingTest")).getAbsolutePath(); - FilesystemPackageCacheManager pcm = new FilesystemPackageCacheManager.Builder().withCacheFolder(pcmPath).build(); + FilesystemPackageCacheManager[] packageCacheManagers = new FilesystemPackageCacheManager[packageCacheManagerTotal]; + Random rand = new Random(); final AtomicInteger totalSuccessful = new AtomicInteger(); - + final ConcurrentHashMap successfulThreads = new ConcurrentHashMap(); List<Thread> threads = new ArrayList<>(); - for (int i = 0; i < 3; i++) { + for (int i = 0; i < threadTotal; i++) { final int index = i; Thread t = new Thread(() -> { try { - pcm.loadPackage("hl7.fhir.xver-extensions#0.0.12"); + System.out.println("Thread #" + index + ": " + Thread.currentThread().getId() + " started"); + final int randomPCM = rand.nextInt(packageCacheManagerTotal); + final int randomOperation = rand.nextInt(4); + if (packageCacheManagers[randomPCM] == null) { + packageCacheManagers[randomPCM] = new FilesystemPackageCacheManager.Builder().withCacheFolder(pcmPath).build(); + } + FilesystemPackageCacheManager pcm = packageCacheManagers[randomPCM]; + if (randomOperation == 0) { + pcm.addPackageToCache("example.fhir.uv.myig", "1.2.3", this.getClass().getResourceAsStream("/npm/dummy-package.tgz"), "https://packages.fhir.org/example.fhir.uv.myig/1.2.3"); + } else if (randomOperation == 1) { + pcm.clear(); + } else if (randomOperation == 2) { + pcm.loadPackageFromCacheOnly("example.fhir.uv.myig", "1.2.3"); + } else { + pcm.removePackage("example.fhir.uv.myig", "1.2.3"); + } totalSuccessful.incrementAndGet(); - System.out.println("Thread " + index + " completed"); + successfulThreads.put(Thread.currentThread().getId(), index); + System.out.println("Thread #" + index + ": " + Thread.currentThread().getId() + " completed"); } catch (Exception e) { e.printStackTrace(); - System.err.println("Thread " + index + " failed"); + System.err.println("Thread #" + index + ": " + Thread.currentThread().getId() + " failed"); } }); t.start(); @@ -131,6 +173,17 @@ public class FilesystemPackageManagerTests { } }); - assertEquals(3, totalSuccessful.get()); + + printUnsuccessfulThreads(successfulThreads, threads); + assertEquals(threadTotal, totalSuccessful.get(), "Not all threads were successful."); + + } + + private void printUnsuccessfulThreads(final ConcurrentHashMap successfulThreads, List<Thread> threads) { + for (Thread t : threads) { + if (!successfulThreads.containsKey(t.getId())) { + System.out.println("Thread #" + t.getId() + " failed"); + } + } } } diff --git a/org.hl7.fhir.validation.cli/pom.xml b/org.hl7.fhir.validation.cli/pom.xml index f03ca9659..4ea8693a3 100644 --- a/org.hl7.fhir.validation.cli/pom.xml +++ b/org.hl7.fhir.validation.cli/pom.xml @@ -5,7 +5,7 @@ <parent> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>org.hl7.fhir.core</artifactId> - <version>6.3.21-SNAPSHOT</version> + <version>6.3.23-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> diff --git a/org.hl7.fhir.validation/pom.xml b/org.hl7.fhir.validation/pom.xml index 364122db4..076877568 100644 --- a/org.hl7.fhir.validation/pom.xml +++ b/org.hl7.fhir.validation/pom.xml @@ -5,7 +5,7 @@ <parent> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>org.hl7.fhir.core</artifactId> - <version>6.3.21-SNAPSHOT</version> + <version>6.3.23-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/IgLoader.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/IgLoader.java index e0a1a9edc..9f64cdc62 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/IgLoader.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/IgLoader.java @@ -497,10 +497,12 @@ public class IgLoader implements IValidationEngineLoader { private Map<String, ByteProvider> fetchByPackage(String src, boolean loadInContext) throws FHIRException, IOException { NpmPackage pi; - InputStream stream = directProvider.fetchByPackage(src); - if (stream != null) { - pi = NpmPackage.fromPackage(stream); - return loadPackage(pi, loadInContext); + if (directProvider != null) { + InputStream stream = directProvider.fetchByPackage(src); + if (stream != null) { + pi = NpmPackage.fromPackage(stream); + return loadPackage(pi, loadInContext); + } } String id = src; String version = null; 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 72ea6f1e6..071efc120 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 @@ -5730,13 +5730,15 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat private boolean checkPublisherConsistency(ValidationContext valContext, List<ValidationMessage> errors, Element element, NodeStack stack, boolean contained) { + boolean ok = true; String pub = element.getNamedChildValue("publisher", false); + + ok = rule(errors, "2024-08-15", IssueType.BUSINESSRULE, element.line(), element.col(), stack.getLiteralPath(), element.getExtensions(ToolingExtensions.EXT_WORKGROUP).size() <= 1, I18nConstants.VALIDATION_HL7_PUBLISHER_MULTIPLE_WGS) && ok; Base wgT = element.getExtensionValue(ToolingExtensions.EXT_WORKGROUP); String wg = wgT == null ? null : wgT.primitiveValue(); String url = element.getNamedChildValue("url"); if (contained && wg == null) { - boolean ok = true; Element container = valContext.getRootResource(); if (element.hasExtension(ToolingExtensions.EXT_WORKGROUP)) { // container already specified the HL7 WG, so we don't need to test @@ -5775,9 +5777,9 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat if (rule(errors, "2023-09-15", IssueType.BUSINESSRULE, element.line(), element.col(), stack.getLiteralPath(), wgd != null, I18nConstants.VALIDATION_HL7_WG_UNKNOWN, wg)) { String rpub = "HL7 International / "+wgd.getName(); if (warning(errors, "2023-09-15", IssueType.BUSINESSRULE, element.line(), element.col(), stack.getLiteralPath(), pub != null, I18nConstants.VALIDATION_HL7_PUBLISHER_MISSING, wg, rpub)) { - boolean ok = rpub.equals(pub); + ok = rpub.equals(pub) && ok; if (!ok && wgd.getName2() != null) { - ok = ("HL7 International / "+wgd.getName2()).equals(pub); + ok = ("HL7 International / "+wgd.getName2()).equals(pub) && ok; warningOrError(pub.contains("/"), errors, "2023-09-15", IssueType.BUSINESSRULE, element.line(), element.col(), stack.getLiteralPath(), ok, I18nConstants.VALIDATION_HL7_PUBLISHER_MISMATCH2, wg, rpub, "HL7 International / "+wgd.getName2(), pub); } else { warningOrError(pub.contains("/"), errors, "2023-09-15", IssueType.BUSINESSRULE, element.line(), element.col(), stack.getLiteralPath(), ok, I18nConstants.VALIDATION_HL7_PUBLISHER_MISMATCH, wg, rpub, pub); @@ -5785,14 +5787,14 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } warning(errors, "2023-09-15", IssueType.BUSINESSRULE, element.line(), element.col(), stack.getLiteralPath(), Utilities.startsWithInList( wgd.getLink(), urls), I18nConstants.VALIDATION_HL7_WG_URL, wg, wgd.getLink()); - return true; + return ok; } } else { - return true; // HL7 sid. + return ok; // HL7 sid. } } - return false; + return ok; } private boolean statusCodesConsistent(String status, String standardsStatus) { diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/ValueSetValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/ValueSetValidator.java index 00477aceb..d71692825 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/ValueSetValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/ValueSetValidator.java @@ -277,7 +277,7 @@ public class ValueSetValidator extends BaseValidator { } if (version == null) { CodeSystem cs = context.fetchCodeSystem(system); - if (cs != null && !CodeSystemUtilities.isExemptFromMultipleVersionChecking(system)) { + if (cs != null && !CodeSystemUtilities.isExemptFromMultipleVersionChecking(system) && fetcher != null) { Set<String> possibleVersions = fetcher.fetchCanonicalResourceVersions(null, valContext.getAppContext(), system); warning(errors, NO_RULE_DATE, IssueType.INVALID, stack, possibleVersions.size() <= 1, I18nConstants.TYPE_SPECIFIC_CHECKS_DT_CANONICAL_MULTIPLE_POSSIBLE_VERSIONS, system, cs.getVersion(), CommaSeparatedStringBuilder.join(", ", Utilities.sorted(possibleVersions))); diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/special/TxTester.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/special/TxTester.java index d48a135a3..7632b191d 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/special/TxTester.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/special/TxTester.java @@ -163,6 +163,7 @@ public class TxTester { return error; } } + private boolean runSuite(JsonObject suite, ITerminologyClient tx, List<String> modes, String filter, JsonArray output) throws FHIRFormatError, FileNotFoundException, IOException { System.out.println("Group "+suite.asString("name")); JsonObject outputS = new JsonObject(); @@ -187,6 +188,7 @@ public class TxTester { if (output != null) { output.add(outputT); } + long start = System.currentTimeMillis(); Parameters profile = loadProfile(test); outputT.add("name", test.asString("name")); if (Utilities.noString(filter) || filter.equals("*") || test.asString("name").contains(filter)) { @@ -219,7 +221,7 @@ public class TxTester { throw new Exception("Unknown Operation "+test.asString("operation")); } - System.out.println(msg == null ? "Pass" : "Fail"); + System.out.println((msg == null ? "Pass" : "Fail") + " ("+Utilities.describeDuration(System.currentTimeMillis() - start)+")"); if (msg != null) { System.out.println(" "+msg); error = msg; diff --git a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/cli/services/ValidationServiceTests.java b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/cli/services/ValidationServiceTests.java index 1a090cf84..628f493c8 100644 --- a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/cli/services/ValidationServiceTests.java +++ b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/cli/services/ValidationServiceTests.java @@ -1,8 +1,7 @@ package org.hl7.fhir.validation.cli.services; import static org.hl7.fhir.validation.tests.utilities.TestUtilities.getTerminologyCacheDirectory; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.AdditionalMatchers.and; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -38,7 +37,6 @@ import org.hl7.fhir.validation.ValidationEngine; import org.hl7.fhir.validation.cli.model.CliContext; import org.hl7.fhir.validation.cli.model.FileInfo; import org.hl7.fhir.validation.cli.model.ValidationRequest; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatchers; @@ -72,7 +70,7 @@ class ValidationServiceTests { Set<String> sessionIds = sessionCache.getSessionIds(); if (sessionIds.stream().findFirst().isPresent()) { // Verify that after 1 run there is only one entry within the cache - Assertions.assertEquals(1, sessionIds.size()); + assertEquals(1, sessionIds.size()); myService.validateSources(request.setSessionId(sessionIds.stream().findFirst().get())); // Verify that the cache has been called on twice with the id created in the first run verify(sessionCache, Mockito.times(2)).fetchSessionValidatorEngine(sessionIds.stream().findFirst().get()); @@ -150,30 +148,26 @@ class ValidationServiceTests { @Test @DisplayName("Test that conversion throws an Exception when no -output or -outputSuffix params are set") - public void convertSingleSourceNoOutput() throws Exception { + public void convertSingleSourceNoOutput() { SessionCache sessionCache = mock(SessionCache.class); ValidationService validationService = new ValidationService(sessionCache); ValidationEngine validationEngine = mock(ValidationEngine.class); CliContext cliContext = getCliContextSingleSource(); - Exception exception = assertThrows( Exception.class, () -> { - validationService.convertSources(cliContext,validationEngine); - }); + assertThrows( Exception.class, () -> validationService.convertSources(cliContext,validationEngine)); } @Test @DisplayName("Test that conversion throws an Exception when multiple sources are set and an -output param is set") - public void convertMultipleSourceOnlyOutput() throws Exception { + public void convertMultipleSourceOnlyOutput() { SessionCache sessionCache = mock(SessionCache.class); ValidationService validationService = new ValidationService(sessionCache); ValidationEngine validationEngine = mock(ValidationEngine.class); CliContext cliContext = getCliContextMultipleSource(); - assertThrows( Exception.class, () -> { - validationService.convertSources(cliContext,validationEngine); - } + assertThrows( Exception.class, () -> validationService.convertSources(cliContext,validationEngine) ); } @@ -208,28 +202,24 @@ class ValidationServiceTests { @Test @DisplayName("Test that snapshot generation throws an Exception when no -output or -outputSuffix params are set") - public void generateSnapshotSingleSourceNoOutput() throws Exception { + public void generateSnapshotSingleSourceNoOutput() { SessionCache sessionCache = mock(SessionCache.class); ValidationService validationService = new ValidationService(sessionCache); ValidationEngine validationEngine = mock(ValidationEngine.class); CliContext cliContext = getCliContextSingleSource(); - Exception exception = assertThrows( Exception.class, () -> { - validationService.generateSnapshot(cliContext.setSv(DUMMY_SV),validationEngine); - }); + assertThrows( Exception.class, () -> validationService.generateSnapshot(cliContext.setSv(DUMMY_SV),validationEngine)); } @Test @DisplayName("Test that snapshot generation throws an Exception when multiple sources are set and an -output param is set") - public void generateSnapshotMultipleSourceOnlyOutput() throws Exception { + public void generateSnapshotMultipleSourceOnlyOutput() { SessionCache sessionCache = mock(SessionCache.class); ValidationService validationService = new ValidationService(sessionCache); ValidationEngine validationEngine = mock(ValidationEngine.class); CliContext cliContext = getCliContextMultipleSource(); - assertThrows( Exception.class, () -> { - validationService.generateSnapshot(cliContext.setOutput(DUMMY_OUTPUT).setSv(DUMMY_SV),validationEngine); - } + assertThrows( Exception.class, () -> validationService.generateSnapshot(cliContext.setOutput(DUMMY_OUTPUT).setSv(DUMMY_SV),validationEngine) ); } @@ -284,7 +274,7 @@ class ValidationServiceTests { final ValidationEngine mockValidationEngine = mock(ValidationEngine.class); when(mockValidationEngine.getContext()).thenReturn(workerContext); - final ValidationEngine.ValidationEngineBuilder mockValidationEngineBuilder = mock(ValidationEngine.ValidationEngineBuilder.class);; + final ValidationEngine.ValidationEngineBuilder mockValidationEngineBuilder = mock(ValidationEngine.ValidationEngineBuilder.class); final ValidationService validationService = createFakeValidationService(mockValidationEngineBuilder, mockValidationEngine); CliContext cliContext = new CliContext(); @@ -302,7 +292,7 @@ class ValidationServiceTests { final ValidationEngine mockValidationEngine = mock(ValidationEngine.class); when(mockValidationEngine.getContext()).thenReturn(workerContext); - final ValidationEngine.ValidationEngineBuilder mockValidationEngineBuilder = mock(ValidationEngine.ValidationEngineBuilder.class);; + final ValidationEngine.ValidationEngineBuilder mockValidationEngineBuilder = mock(ValidationEngine.ValidationEngineBuilder.class); final ValidationService validationService = createFakeValidationService(mockValidationEngineBuilder, mockValidationEngine); CliContext cliContext = new CliContext(); @@ -323,18 +313,18 @@ class ValidationServiceTests { when(validationEngineBuilder.withUserAgent(anyString())).thenReturn(validationEngineBuilder); try { when(validationEngineBuilder.fromSource(isNull())).thenReturn(validationEngine); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (URISyntaxException e) { + } catch (IOException | URISyntaxException e) { throw new RuntimeException(e); } return validationEngineBuilder; } @Override - protected void loadIgsAndExtensions(ValidationEngine validationEngine, CliContext cliContext, TimeTracker timeTracker) throws IOException, URISyntaxException { + protected void loadIgsAndExtensions(ValidationEngine validationEngine, CliContext cliContext, TimeTracker timeTracker) { //Don't care. Do nothing. } }; } + + } \ No newline at end of file diff --git a/pom.xml b/pom.xml index c5f5508ca..c2b718c78 100644 --- a/pom.xml +++ b/pom.xml @@ -14,14 +14,14 @@ HAPI FHIR --> <artifactId>org.hl7.fhir.core</artifactId> - <version>6.3.21-SNAPSHOT</version> + <version>6.3.23-SNAPSHOT</version> <packaging>pom</packaging> <properties> <commons_compress_version>1.26.0</commons_compress_version> <guava_version>32.0.1-jre</guava_version> <hapi_fhir_version>6.4.1</hapi_fhir_version> - <validator_test_case_version>1.5.19</validator_test_case_version> + <validator_test_case_version>1.5.20-SNAPSHOT</validator_test_case_version> <jackson_version>2.17.0</jackson_version> <junit_jupiter_version>5.9.2</junit_jupiter_version> <junit_platform_launcher_version>1.8.2</junit_platform_launcher_version> @@ -32,7 +32,7 @@ <lombok_version>1.18.32</lombok_version> <byte_buddy_version>1.14.8</byte_buddy_version> <apache_poi_version>5.2.1</apache_poi_version> - <saxon_he_version>9.8.0-15</saxon_he_version> + <saxon_he_version>11.6</saxon_he_version> <maven.compiler.release>11</maven.compiler.release> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target>