diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseResourceReturningMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseResourceReturningMethodBinding.java index e1adccb80ed..7c044f90d16 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseResourceReturningMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseResourceReturningMethodBinding.java @@ -63,6 +63,7 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; +import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; import ca.uhn.fhir.util.ReflectionUtil; import ca.uhn.fhir.util.UrlUtil; @@ -81,6 +82,7 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding myInterceptors = new ArrayList(); private IPagingProvider myPagingProvider; - private Collection myPlainProviders = new ArrayList(); + private final List myPlainProviders = new ArrayList(); private Map myResourceNameToBinding = new HashMap(); - private Collection myResourceProviders = new ArrayList(); + private final List myResourceProviders = new ArrayList(); private Map myTypeToProvider = new HashMap(); private IServerAddressStrategy myServerAddressStrategy = new IncomingRequestAddressStrategy(); private ResourceBinding myServerBinding = new ResourceBinding(); @@ -1208,7 +1208,10 @@ public class RestfulServer extends HttpServlet { * @see #setResourceProviders(Collection) */ public void setPlainProviders(Collection theProviders) { - myPlainProviders = theProviders; + myPlainProviders.clear(); + if (theProviders != null) { + myPlainProviders.addAll(theProviders); + } } /** @@ -1226,21 +1229,30 @@ public class RestfulServer extends HttpServlet { * @see #setResourceProviders(Collection) */ public void setProviders(Object... theProviders) { - myPlainProviders = Arrays.asList(theProviders); + myPlainProviders.clear(); + if (theProviders != null) { + myPlainProviders.addAll(Arrays.asList(theProviders)); + } } /** * Sets the resource providers for this server */ public void setResourceProviders(Collection theResourceProviders) { - myResourceProviders = theResourceProviders; + myResourceProviders.clear(); + if (theResourceProviders != null) { + myResourceProviders.addAll(theResourceProviders); + } } /** * Sets the resource providers for this server */ public void setResourceProviders(IResourceProvider... theResourceProviders) { - myResourceProviders = Arrays.asList(theResourceProviders); + myResourceProviders.clear(); + if (theResourceProviders != null) { + myResourceProviders.addAll(Arrays.asList(theResourceProviders)); + } } /** diff --git a/hapi-fhir-jpaserver-example/pom.xml b/hapi-fhir-jpaserver-example/pom.xml index 59e9cfe9a86..7824fabc1e1 100644 --- a/hapi-fhir-jpaserver-example/pom.xml +++ b/hapi-fhir-jpaserver-example/pom.xml @@ -188,6 +188,11 @@ org.apache.maven.plugins maven-war-plugin + + + ${maven.build.timestamp} + + ca.uhn.hapi.fhir diff --git a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml index 0d78d701ccb..d2a6457014e 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml +++ b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml @@ -198,6 +198,11 @@ org.apache.maven.plugins maven-war-plugin + + + ${maven.build.timestamp} + + ca.uhn.hapi.fhir diff --git a/hapi-fhir-structures-dstu/src/main/java/ca/uhn/fhir/rest/server/provider/ServerConformanceProvider.java b/hapi-fhir-structures-dstu/src/main/java/ca/uhn/fhir/rest/server/provider/ServerConformanceProvider.java index 0007bed8659..84e059a7d9e 100644 --- a/hapi-fhir-structures-dstu/src/main/java/ca/uhn/fhir/rest/server/provider/ServerConformanceProvider.java +++ b/hapi-fhir-structures-dstu/src/main/java/ca/uhn/fhir/rest/server/provider/ServerConformanceProvider.java @@ -20,14 +20,15 @@ package ca.uhn.fhir.rest.server.provider; * #L% */ -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.TreeSet; +import java.io.IOException; +import java.io.InputStream; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.jar.Manifest; +import ca.uhn.fhir.parser.DataFormatException; import org.apache.commons.lang3.StringUtils; import ca.uhn.fhir.context.RuntimeResourceDefinition; @@ -121,7 +122,7 @@ public class ServerConformanceProvider implements IServerConformanceProvider includes, DynamicSearchMethodBinding searchMethodBinding) { includes.addAll(searchMethodBinding.getIncludes()); diff --git a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java index 901f617c19a..8115aef6246 100644 --- a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java +++ b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java @@ -21,21 +21,15 @@ package ca.uhn.fhir.rest.server.provider.dstu2; */ import static org.apache.commons.lang3.StringUtils.isNotBlank; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Map; +import java.io.IOException; +import java.io.InputStream; +import java.util.*; import java.util.Map.Entry; -import java.util.Set; -import java.util.TreeMap; -import java.util.TreeSet; +import java.util.jar.Manifest; import javax.servlet.http.HttpServletRequest; +import ca.uhn.fhir.parser.DataFormatException; import org.apache.commons.lang3.StringUtils; import ca.uhn.fhir.context.RuntimeResourceDefinition; @@ -171,7 +165,7 @@ public class ServerConformanceProvider implements IServerConformanceProvider includes, DynamicSearchMethodBinding searchMethodBinding) { includes.addAll(searchMethodBinding.getIncludes()); diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerFeaturesDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerFeaturesDstu2Test.java new file mode 100644 index 00000000000..55e0527504a --- /dev/null +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerFeaturesDstu2Test.java @@ -0,0 +1,154 @@ +package ca.uhn.fhir.rest.server; + +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpOptions; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.model.dstu2.resource.Patient; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Read; +import ca.uhn.fhir.util.PortUtil; + +/** + * Created by dsotnikov on 2/25/2014. + */ +public class ServerFeaturesDstu2Test { + + private static CloseableHttpClient ourClient; + private static int ourPort; + private static Server ourServer; + private static FhirContext ourCtx = FhirContext.forDstu2(); + private static RestfulServer ourServlet; + + @Test + public void testOptions() throws Exception { + HttpOptions httpGet = new HttpOptions("http://localhost:" + ourPort + ""); + HttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + + assertEquals(200, status.getStatusLine().getStatusCode()); + assertThat(responseContent, containsString(" providers = new ArrayList(ourServlet.getResourceProviders()); + for (IResourceProvider provider : providers) { + ourServlet.unregisterProvider(provider); + } + + ourServlet.registerProvider(new DummyPatientResourceProvider2()); + + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1"); + status = ourClient.execute(httpGet); + responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertThat(responseContent, containsString("PRP2")); + + } + + @Test + public void testOptionsJson() throws Exception { + HttpOptions httpGet = new HttpOptions("http://localhost:" + ourPort + "?_format=json"); + HttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + + assertEquals(200, status.getStatusLine().getStatusCode()); + assertThat(responseContent, containsString("resourceType\":\"Conformance")); + } + + @AfterClass + public static void afterClass() throws Exception { + ourServer.stop(); + } + + @BeforeClass + public static void beforeClass() throws Exception { + + ourPort = PortUtil.findFreePort(); + ourServer = new Server(ourPort); + + DummyPatientResourceProvider patientProvider = new DummyPatientResourceProvider(); + + ServletHandler proxyHandler = new ServletHandler(); + ourServlet = new RestfulServer(ourCtx); + ourServlet.setFhirContext(ourCtx); + ourServlet.setResourceProviders(patientProvider); + ServletHolder servletHolder = new ServletHolder(ourServlet); + proxyHandler.addServletWithMapping(servletHolder, "/*"); + ourServer.setHandler(proxyHandler); + ourServer.start(); + + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); + HttpClientBuilder builder = HttpClientBuilder.create(); + builder.setConnectionManager(connectionManager); + ourClient = builder.build(); + + } + + public static class DummyPatientResourceProvider implements IResourceProvider { + + @Read + public Patient read(@IdParam IdDt theId) { + Patient p1 = new Patient(); + p1.setId("p1ReadId"); + p1.addIdentifier().setValue("PRP1"); + return p1; + } + + @Override + public Class getResourceType() { + return Patient.class; + } + + } + + public static class DummyPatientResourceProvider2 implements IResourceProvider { + + @Read + public Patient read(@IdParam IdDt theId) { + Patient p1 = new Patient(); + p1.setId("p1ReadId"); + p1.addIdentifier().setValue("PRP2"); + return p1; + } + + @Override + public Class getResourceType() { + return Patient.class; + } + + } + +} diff --git a/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/conf/ServerConformanceProvider.java b/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/conf/ServerConformanceProvider.java index 2f50e4b1c4c..af402912521 100644 --- a/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/conf/ServerConformanceProvider.java +++ b/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/conf/ServerConformanceProvider.java @@ -21,6 +21,11 @@ package org.hl7.fhir.instance.conf; */ import static org.apache.commons.lang3.StringUtils.isNotBlank; +import java.io.IOException; +import java.io.InputStream; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -34,6 +39,7 @@ import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; +import java.util.jar.Manifest; import javax.servlet.http.HttpServletRequest; @@ -75,6 +81,7 @@ import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.IServerConformanceProvider; import ca.uhn.fhir.rest.server.ResourceBinding; import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; /** @@ -92,29 +99,30 @@ import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; */ public class ServerConformanceProvider implements IServerConformanceProvider { - private boolean myCache = true; - private volatile Conformance myConformance; - private IdentityHashMap myOperationBindingToName; - private HashMap> myOperationNameToBindings; - private String myPublisher = "Not provided"; - private RestfulServer myRestfulServer; + private boolean myCache = true; + private volatile Conformance myConformance; + private IdentityHashMap myOperationBindingToName; + private HashMap> myOperationNameToBindings; + private String myPublisher = "Not provided"; + private RestfulServer myRestfulServer; - public ServerConformanceProvider(RestfulServer theRestfulServer) { - myRestfulServer = theRestfulServer; - } - - /* - * Add a no-arg constructor and seetter so that the ServerConfirmanceProvider - * can be Spring-wired with the RestfulService avoiding the potential - * reference cycle that would happen. - */ - public ServerConformanceProvider() { - super(); - } - - public void setRestfulServer(RestfulServer theRestfulServer) { - myRestfulServer = theRestfulServer; - } + public ServerConformanceProvider(RestfulServer theRestfulServer) { + myRestfulServer = theRestfulServer; + } + + /* + * Add a no-arg constructor and seetter so that the + * ServerConfirmanceProvider can be Spring-wired with + * the RestfulService avoiding the potential reference + * cycle that would happen. + */ + public ServerConformanceProvider () { + super(); + } + + public void setRestfulServer (RestfulServer theRestfulServer) { + myRestfulServer = theRestfulServer; + } private void checkBindingForSystemOps(ConformanceRestComponent rest, Set systemOps, BaseMethodBinding nextMethodBinding) { @@ -184,13 +192,9 @@ public class ServerConformanceProvider implements IServerConformanceProvider includes, DynamicSearchMethodBinding searchMethodBinding) { includes.addAll(searchMethodBinding.getIncludes()); diff --git a/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/validation/InstanceValidator.java b/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/validation/InstanceValidator.java index 172aba3ce8d..1bef6377b1e 100644 --- a/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/validation/InstanceValidator.java +++ b/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/validation/InstanceValidator.java @@ -1,12 +1,7 @@ package org.hl7.fhir.instance.validation; - -import static org.apache.commons.lang3.StringUtils.lowerCase; - import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Map.Entry; import org.hl7.fhir.instance.formats.FormatUtilities; @@ -17,7 +12,6 @@ import org.hl7.fhir.instance.model.Coding; import org.hl7.fhir.instance.model.ContactPoint; import org.hl7.fhir.instance.model.ElementDefinition; import org.hl7.fhir.instance.model.ElementDefinition.ElementDefinitionBindingComponent; -import org.hl7.fhir.instance.model.ElementDefinition.ElementDefinitionConstraintComponent; import org.hl7.fhir.instance.model.ElementDefinition.TypeRefComponent; import org.hl7.fhir.instance.model.Enumerations.BindingStrength; import org.hl7.fhir.instance.model.Extension; @@ -65,92 +59,1240 @@ import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; - /* * todo: * check urn's don't start oid: or uuid: */ public class InstanceValidator extends BaseValidator implements IResourceValidator { - - public class ElementInfo { - private String name; - private WrapperElement element; - private String path; - public ElementDefinition definition; - public int count; - - public ElementInfo(String name, WrapperElement element, String path, int count) { - this.name = name; - this.element = element; - this.path = path; - this.count = count; - } - - public int line() { - return element.line(); - } - - public int col() { - return element.col(); - } - - } + private boolean anyExtensionsAllowed; + private BestPracticeWarningLevel bpWarnings; + private ValueSetExpanderFactory cache; // configuration items private CheckDisplayOption checkDisplay; - private BestPracticeWarningLevel bpWarnings; - @Override - public CheckDisplayOption getCheckDisplay() { - return checkDisplay; + private IWorkerContext context; + + private List extensionDomains = new ArrayList(); + + private boolean requiresResourceId; + + // used during the build process to keep the overall volume of messages down + private boolean suppressLoincSnomedMessages; + + public InstanceValidator(IWorkerContext theContext) throws Exception { + super(); + this.context = theContext; + source = Source.InstanceValidator; + cache = new ValueSetExpansionCache(theContext, null); } - @Override - public void setCheckDisplay(CheckDisplayOption checkDisplay) { - this.checkDisplay = checkDisplay; + + public InstanceValidator(IWorkerContext theContext, ValueSetExpanderFactory theValueSetExpander) throws Exception { + super(); + this.context = theContext; + source = Source.InstanceValidator; + this.cache = theValueSetExpander; + } + + private boolean allowUnknownExtension(String url) { + if (url.contains("example.org") || url.contains("acme.com") || url.contains("nema.org")) + return true; + for (String s : extensionDomains) + if (url.startsWith(s)) + return true; + return anyExtensionsAllowed; + } + + private void bpCheck(List errors, IssueType invalid, int line, int col, String literalPath, boolean test, String message) { + if (bpWarnings != null) { + switch (bpWarnings) { + case Error: + rule(errors, invalid, line, col, literalPath, test, message); + case Warning: + warning(errors, invalid, line, col, literalPath, test, message); + case Hint: + hint(errors, invalid, line, col, literalPath, test, message); + default: // do nothing + } + } + } + + private boolean check(String v1, String v2) { + return v1 == null ? Utilities.noString(v1) : v1.equals(v2); + } + + private void checkAddress(List errors, String path, WrapperElement focus, Address fixed) { + checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), "use"); + checkFixedValue(errors, path + ".text", focus.getNamedChild("text"), fixed.getTextElement(), "text"); + checkFixedValue(errors, path + ".city", focus.getNamedChild("city"), fixed.getCityElement(), "city"); + checkFixedValue(errors, path + ".state", focus.getNamedChild("state"), fixed.getStateElement(), "state"); + checkFixedValue(errors, path + ".country", focus.getNamedChild("country"), fixed.getCountryElement(), "country"); + checkFixedValue(errors, path + ".zip", focus.getNamedChild("zip"), fixed.getPostalCodeElement(), "postalCode"); + + List lines = new ArrayList(); + focus.getNamedChildren("line", lines); + if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, lines.size() == fixed.getLine().size(), + "Expected " + Integer.toString(fixed.getLine().size()) + " but found " + Integer.toString(lines.size()) + " line elements")) { + for (int i = 0; i < lines.size(); i++) + checkFixedValue(errors, path + ".coding", lines.get(i), fixed.getLine().get(i), "coding"); + } + } + + private void checkAttachment(List errors, String path, WrapperElement focus, Attachment fixed) { + checkFixedValue(errors, path + ".contentType", focus.getNamedChild("contentType"), fixed.getContentTypeElement(), "contentType"); + checkFixedValue(errors, path + ".language", focus.getNamedChild("language"), fixed.getLanguageElement(), "language"); + checkFixedValue(errors, path + ".data", focus.getNamedChild("data"), fixed.getDataElement(), "data"); + checkFixedValue(errors, path + ".url", focus.getNamedChild("url"), fixed.getUrlElement(), "url"); + checkFixedValue(errors, path + ".size", focus.getNamedChild("size"), fixed.getSizeElement(), "size"); + checkFixedValue(errors, path + ".hash", focus.getNamedChild("hash"), fixed.getHashElement(), "hash"); + checkFixedValue(errors, path + ".title", focus.getNamedChild("title"), fixed.getTitleElement(), "title"); + } + + // public API + + private boolean checkCode(List errors, WrapperElement element, String path, String code, String system, String display) throws Exception { + if (context.supportsSystem(system)) { + ValidationResult s = context.validateCode(system, code, display); + if (s == null || s.isOk()) + return true; + if (s.getSeverity() == IssueSeverity.INFORMATION) + hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, s == null, s.getMessage()); + else if (s.getSeverity() == IssueSeverity.WARNING) + warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, s == null, s.getMessage()); + else + return rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, s == null, s.getMessage()); + return true; + } else if (system.startsWith("http://hl7.org/fhir")) { + if (system.equals("http://hl7.org/fhir/sid/icd-10")) + return true; // else don't check ICD-10 (for now) + else { + ValueSet vs = getValueSet(system); + if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, vs != null, "Unknown Code System " + system)) { + ConceptDefinitionComponent def = getCodeDefinition(vs, code); + if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, def != null, "Unknown Code (" + system + "#" + code + ")")) + return warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, display == null || display.equals(def.getDisplay()), "Display should be '" + def.getDisplay() + "'"); + } + return false; + } + } else if (system.startsWith("http://loinc.org")) { + return true; + } else if (system.startsWith("http://unitsofmeasure.org")) { + return true; + } else + return true; + } + + private void checkCodeableConcept(List errors, String path, WrapperElement focus, CodeableConcept fixed) { + checkFixedValue(errors, path + ".text", focus.getNamedChild("text"), fixed.getTextElement(), "text"); + List codings = new ArrayList(); + focus.getNamedChildren("coding", codings); + if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, codings.size() == fixed.getCoding().size(), + "Expected " + Integer.toString(fixed.getCoding().size()) + " but found " + Integer.toString(codings.size()) + " coding elements")) { + for (int i = 0; i < codings.size(); i++) + checkFixedValue(errors, path + ".coding", codings.get(i), fixed.getCoding().get(i), "coding"); + } + } + + private void checkCodeableConcept(List errors, String path, WrapperElement element, StructureDefinition profile, ElementDefinition theElementCntext) + throws EOperationOutcome, Exception { + if (theElementCntext != null && theElementCntext.hasBinding()) { + ElementDefinitionBindingComponent binding = theElementCntext.getBinding(); + if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, binding != null, "Binding for " + path + " missing (cc)")) { + if (binding.hasValueSet() && binding.getValueSet() instanceof Reference) { + ValueSet unexpandedVs = resolveBindingReference(binding.getValueSet()); + if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, unexpandedVs != null, "ValueSet " + describeReference(binding.getValueSet()) + " not found")) { + ValueSet vs; + try { + boolean found = false; + boolean any = false; + WrapperElement c = element.getFirstChild(); + while (c != null) { + if (c.getName().equals("coding")) { + any = true; + String system = c.getNamedChildValue("system"); + String code = c.getNamedChildValue("code"); + if (system != null && code != null) { + ValueSetExpansionOutcome exp = cache.getExpander().expand(unexpandedVs); + vs = exp != null ? exp.getValueset() : null; + if (vs == null) { + if (binding.getStrength() != BindingStrength.REQUIRED) { + ValidationResult validationResult = context.validateCode(system, code, null); + if (validationResult.isOk()) { + found = true; + } else { + warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Unable to validate code \"{0}\" in code system \"{1}\"", code, system); + return; + } + } + } + if (found == false) { + if (!warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, vs != null, "Unable to expand value set for " + describeReference(binding.getValueSet()))) { + return; + } + } + found = found || codeInExpansion(vs, system, code); + } + } + c = c.getNextSibling(); + } + if (!any && binding.getStrength() == BindingStrength.REQUIRED) + warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, found, + "No code provided, and value set " + describeReference(binding.getValueSet()) + " (" + unexpandedVs.getUrl() + ") is required"); + if (any) + if (binding.getStrength() == BindingStrength.PREFERRED) + hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, found, + "None of the codes are in the example value set " + describeReference(binding.getValueSet()) + " (" + unexpandedVs.getUrl() + ")"); + else if (binding.getStrength() == BindingStrength.EXTENSIBLE) + warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, found, + "None of the codes are in the expected value set " + describeReference(binding.getValueSet()) + " (" + unexpandedVs.getUrl() + ")"); + + } catch (Exception e) { + if (e.getMessage() == null) { + warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, + "Exception opening value set " + unexpandedVs.getUrl() + " for " + describeReference(binding.getValueSet()) + ": --Null--"); + // } else if (!e.getMessage().contains("unable to find value set http://snomed.info/sct")) { + // hint(errors, IssueType.CODEINVALID, path, suppressLoincSnomedMessages, "Snomed value set - not validated"); + // } else if (!e.getMessage().contains("unable to find value set http://loinc.org")) { + // hint(errors, IssueType.CODEINVALID, path, suppressLoincSnomedMessages, "Loinc value set - not validated"); + } else + warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, + "Exception opening value set " + unexpandedVs.getUrl() + " for " + describeReference(binding.getValueSet()) + ": " + e.getMessage()); + } + } + } else if (binding.hasValueSet()) { + hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding by URI reference cannot be checked"); + } else { + hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding for path " + path + " has no source, so can't be checked"); + } + } + } + } + + private void checkCoding(List errors, String path, WrapperElement focus, Coding fixed) { + checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), "system"); + checkFixedValue(errors, path + ".code", focus.getNamedChild("code"), fixed.getCodeElement(), "code"); + checkFixedValue(errors, path + ".display", focus.getNamedChild("display"), fixed.getDisplayElement(), "display"); + checkFixedValue(errors, path + ".userSelected", focus.getNamedChild("userSelected"), fixed.getUserSelectedElement(), "userSelected"); + } + + private void checkCoding(List errors, String path, WrapperElement element, StructureDefinition profile, ElementDefinition context) throws EOperationOutcome, Exception { + String code = element.getNamedChildValue("code"); + String system = element.getNamedChildValue("system"); + String display = element.getNamedChildValue("display"); + rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, isAbsolute(system), "Coding.system must be an absolute reference, not a local reference"); + + if (system != null && code != null) { + if (checkCode(errors, element, path, code, system, display)) + if (context != null && context.getBinding() != null) { + ElementDefinitionBindingComponent binding = context.getBinding(); + if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, binding != null, "Binding for " + path + " missing")) { + if (binding.hasValueSet() && binding.getValueSet() instanceof Reference) { + ValueSet vs = resolveBindingReference(binding.getValueSet()); + if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, vs != null, "ValueSet " + describeReference(binding.getValueSet()) + " not found")) { + try { + vs = cache.getExpander().expand(vs).getValueset(); + if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, vs != null, "Unable to expand value set for " + describeReference(binding.getValueSet()))) { + if (binding.getStrength() == BindingStrength.REQUIRED) + rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, codeInExpansion(vs, system, code), + "Code {" + system + "}" + code + " is not in value set " + describeReference(binding.getValueSet()) + " (" + vs.getUrl() + ")"); + else if (binding.getStrength() == BindingStrength.EXTENSIBLE) + warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, codeInExpansion(vs, system, code), + "Code {" + system + "}" + code + " is not in value set " + describeReference(binding.getValueSet()) + " (" + vs.getUrl() + ")"); + else + hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, codeInExpansion(vs, system, code), + "Code {" + system + "}" + code + " is not in value set " + describeReference(binding.getValueSet()) + " (" + vs.getUrl() + ")"); + } + } catch (Exception e) { + if (e.getMessage() == null) + warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, + "Exception opening value set " + vs.getUrl() + " for " + describeReference(binding.getValueSet()) + ": --Null--"); + else + warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, + "Exception opening value set " + vs.getUrl() + " for " + describeReference(binding.getValueSet()) + ": " + e.getMessage()); + } + } + } else if (binding.hasValueSet()) { + hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding by URI reference cannot be checked"); + } else { + hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding for path " + path + " has no source, so can't be checked"); + } + } + } + } + } + + private void checkContactPoint(List errors, String path, WrapperElement focus, ContactPoint fixed) { + checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), "system"); + checkFixedValue(errors, path + ".value", focus.getNamedChild("value"), fixed.getValueElement(), "value"); + checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), "use"); + checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriod(), "period"); + + } + + private void checkDeclaredProfiles(List errors, WrapperElement element, NodeStack stack) throws Exception { + WrapperElement meta = element.getNamedChild("meta"); + if (meta != null) { + List profiles = new ArrayList(); + meta.getNamedChildren("profile", profiles); + int i = 0; + for (WrapperElement profile : profiles) { + String ref = profile.getAttribute("value"); + String p = stack.addToLiteralPath("meta", "profile", ":" + Integer.toString(i)); + if (rule(errors, IssueType.INVALID, element.line(), element.col(), p, !Utilities.noString(ref), "StructureDefinition reference invalid")) { + StructureDefinition pr = context.fetchResource(StructureDefinition.class, ref); + if (warning(errors, IssueType.INVALID, element.line(), element.col(), p, pr != null, "StructureDefinition reference \"{0}\" could not be resolved", ref)) { + if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), p, pr.hasSnapshot(), + "StructureDefinition has no snapshot - validation is against the snapshot, so it must be provided")) { + validateElement(errors, pr, pr.getSnapshot().getElement().get(0), null, null, element, element.getName(), stack); + } + } + i++; + } + } + } + } + + private StructureDefinition checkExtension(List errors, String path, WrapperElement element, ElementDefinition def, StructureDefinition profile, NodeStack stack) + throws Exception { + String url = element.getAttribute("url"); + boolean isModifier = element.getName().equals("modifierExtension"); + + StructureDefinition ex = context.fetchResource(StructureDefinition.class, url); + if (ex == null) { + if (!rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, allowUnknownExtension(url), "The extension " + url + " is unknown, and not allowed here")) + warning(errors, IssueType.STRUCTURE, element.line(), element.col(), path, allowUnknownExtension(url), "Unknown extension " + url); + } else { + if (def.getIsModifier()) + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", ex.getSnapshot().getElement().get(0).getIsModifier(), + "Extension modifier mismatch: the extension element is labelled as a modifier, but the underlying extension is not"); + else + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", !ex.getSnapshot().getElement().get(0).getIsModifier(), + "Extension modifier mismatch: the extension element is not labelled as a modifier, but the underlying extension is"); + + // two questions + // 1. can this extension be used here? + checkExtensionContext(errors, element, /* path+"[url='"+url+"']", */ ex, stack, ex.getUrl()); + + if (isModifier) + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", ex.getSnapshot().getElement().get(0).getIsModifier(), + "The Extension '" + url + "' must be used as a modifierExtension"); + else + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", !ex.getSnapshot().getElement().get(0).getIsModifier(), + "The Extension '" + url + "' must not be used as an extension (it's a modifierExtension)"); + + // 2. is the content of the extension valid? + + } + return ex; + } + + private boolean checkExtensionContext(List errors, WrapperElement element, StructureDefinition definition, NodeStack stack, String extensionParent) { + String extUrl = definition.getUrl(); + CommaSeparatedStringBuilder p = new CommaSeparatedStringBuilder(); + for (String lp : stack.getLogicalPaths()) + p.append(lp); + if (definition.getContextType() == ExtensionContext.DATATYPE) { + boolean ok = false; + CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); + for (StringType ct : definition.getContext()) { + b.append(ct.getValue()); + if (ct.getValue().equals("*") || stack.getLogicalPaths().contains(ct.getValue() + ".extension")) + ok = true; + } + return rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), ok, + "The extension " + extUrl + " is not allowed to be used on the logical path set [" + p.toString() + "] (allowed: datatype=" + b.toString() + ")"); + } else if (definition.getContextType() == ExtensionContext.EXTENSION) { + boolean ok = false; + for (StringType ct : definition.getContext()) + if (ct.getValue().equals("*") || ct.getValue().equals(extensionParent)) + ok = true; + return rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), ok, + "The extension " + extUrl + " is not allowed to be used with the extension '" + extensionParent + "'"); + } else if (definition.getContextType() == ExtensionContext.MAPPING) { + throw new Error("Not handled yet (extensionContext)"); + } else if (definition.getContextType() == ExtensionContext.RESOURCE) { + boolean ok = false; + // String simplePath = container.getPath(); + // System.out.println(simplePath); + // if (effetive.endsWith(".extension") || simplePath.endsWith(".modifierExtension")) + // simplePath = simplePath.substring(0, simplePath.lastIndexOf('.')); + CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); + for (StringType ct : definition.getContext()) { + String c = ct.getValue(); + b.append(c); + if (c.equals("*") || stack.getLogicalPaths().contains(c + ".extension") || (c.startsWith("@") && stack.getLogicalPaths().contains(c.substring(1) + ".extension"))) + ; + ok = true; + } + return rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), ok, + "The extension " + extUrl + " is not allowed to be used on the logical path set " + p.toString() + " (allowed: resource=" + b.toString() + ")"); + } else + throw new Error("Unknown context type"); + } + // + // private String simplifyPath(String path) { + // String s = path.replace("/f:", "."); + // while (s.contains("[")) + // s = s.substring(0, s.indexOf("["))+s.substring(s.indexOf("]")+1); + // String[] parts = s.split("\\."); + // int i = 0; + // while (i < parts.length && !context.getProfiles().containsKey(parts[i].toLowerCase())) + // i++; + // if (i >= parts.length) + // throw new Error("Unable to process part "+path); + // int j = parts.length - 1; + // while (j > 0 && (parts[j].equals("extension") || parts[j].equals("modifierExtension"))) + // j--; + // StringBuilder b = new StringBuilder(); + // boolean first = true; + // for (int k = i; k <= j; k++) { + // if (k == j || !parts[k].equals(parts[k+1])) { + // if (first) + // first = false; + // else + // b.append("."); + // b.append(parts[k]); + // } + // } + // return b.toString(); + // } + // + + private void checkFixedValue(List errors, String path, WrapperElement focus, org.hl7.fhir.instance.model.Element fixed, String propName) { + if (fixed == null && focus == null) + ; // this is all good + else if (fixed == null && focus != null) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, false, "Unexpected element " + focus.getName()); + else if (fixed != null && focus == null) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, false, "Mising element " + propName); + else { + String value = focus.getAttribute("value"); + if (fixed instanceof org.hl7.fhir.instance.model.BooleanType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.BooleanType) fixed).asStringValue(), value), + "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.model.BooleanType) fixed).asStringValue() + "'"); + else if (fixed instanceof org.hl7.fhir.instance.model.IntegerType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.IntegerType) fixed).asStringValue(), value), + "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.model.IntegerType) fixed).asStringValue() + "'"); + else if (fixed instanceof org.hl7.fhir.instance.model.DecimalType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.DecimalType) fixed).asStringValue(), value), + "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.model.DecimalType) fixed).asStringValue() + "'"); + else if (fixed instanceof org.hl7.fhir.instance.model.Base64BinaryType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.Base64BinaryType) fixed).asStringValue(), value), + "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.model.Base64BinaryType) fixed).asStringValue() + "'"); + else if (fixed instanceof org.hl7.fhir.instance.model.InstantType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.InstantType) fixed).getValue().toString(), value), + "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.model.InstantType) fixed).asStringValue() + "'"); + else if (fixed instanceof org.hl7.fhir.instance.model.StringType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.StringType) fixed).getValue(), value), + "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.model.StringType) fixed).getValue() + "'"); + else if (fixed instanceof org.hl7.fhir.instance.model.UriType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.UriType) fixed).getValue(), value), + "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.model.UriType) fixed).getValue() + "'"); + else if (fixed instanceof org.hl7.fhir.instance.model.DateType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.DateType) fixed).getValue().toString(), value), + "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.model.DateType) fixed).getValue() + "'"); + else if (fixed instanceof org.hl7.fhir.instance.model.DateTimeType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.DateTimeType) fixed).getValue().toString(), value), + "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.model.DateTimeType) fixed).getValue() + "'"); + else if (fixed instanceof org.hl7.fhir.instance.model.OidType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.OidType) fixed).getValue(), value), + "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.model.OidType) fixed).getValue() + "'"); + else if (fixed instanceof org.hl7.fhir.instance.model.UuidType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.UuidType) fixed).getValue(), value), + "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.model.UuidType) fixed).getValue() + "'"); + else if (fixed instanceof org.hl7.fhir.instance.model.CodeType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.CodeType) fixed).getValue(), value), + "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.model.CodeType) fixed).getValue() + "'"); + else if (fixed instanceof org.hl7.fhir.instance.model.IdType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.IdType) fixed).getValue(), value), + "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.model.IdType) fixed).getValue() + "'"); + else if (fixed instanceof Quantity) + checkQuantity(errors, path, focus, (Quantity) fixed); + else if (fixed instanceof Address) + checkAddress(errors, path, focus, (Address) fixed); + else if (fixed instanceof ContactPoint) + checkContactPoint(errors, path, focus, (ContactPoint) fixed); + else if (fixed instanceof Attachment) + checkAttachment(errors, path, focus, (Attachment) fixed); + else if (fixed instanceof Identifier) + checkIdentifier(errors, path, focus, (Identifier) fixed); + else if (fixed instanceof Coding) + checkCoding(errors, path, focus, (Coding) fixed); + else if (fixed instanceof HumanName) + checkHumanName(errors, path, focus, (HumanName) fixed); + else if (fixed instanceof CodeableConcept) + checkCodeableConcept(errors, path, focus, (CodeableConcept) fixed); + else if (fixed instanceof Timing) + checkTiming(errors, path, focus, (Timing) fixed); + else if (fixed instanceof Period) + checkPeriod(errors, path, focus, (Period) fixed); + else if (fixed instanceof Range) + checkRange(errors, path, focus, (Range) fixed); + else if (fixed instanceof Ratio) + checkRatio(errors, path, focus, (Ratio) fixed); + else if (fixed instanceof SampledData) + checkSampledData(errors, path, focus, (SampledData) fixed); + + else + rule(errors, IssueType.EXCEPTION, focus.line(), focus.col(), path, false, "Unhandled fixed value type " + fixed.getClass().getName()); + List extensions = new ArrayList(); + focus.getNamedChildren("extension", extensions); + if (fixed.getExtension().size() == 0) { + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, extensions.size() == 0, "No extensions allowed"); + } else if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, extensions.size() == fixed.getExtension().size(), + "Extensions count mismatch: expected " + Integer.toString(fixed.getExtension().size()) + " but found " + Integer.toString(extensions.size()))) { + for (Extension e : fixed.getExtension()) { + WrapperElement ex = getExtensionByUrl(extensions, e.getUrl()); + if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, ex != null, "Extension count mismatch: unable to find extension: " + e.getUrl())) { + checkFixedValue(errors, path, ex.getFirstChild().getNextSibling(), e.getValue(), "extension.value"); + } + } + } + } + } + + private void checkForProcessingInstruction(List errors, Document document) { + Node node = document.getFirstChild(); + while (node != null) { + rule(errors, IssueType.INVALID, -1, -1, "(document)", node.getNodeType() != Node.PROCESSING_INSTRUCTION_NODE, "No processing instructions allowed in resources"); + node = node.getNextSibling(); + } + } + + private void checkHumanName(List errors, String path, WrapperElement focus, HumanName fixed) { + checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), "use"); + checkFixedValue(errors, path + ".text", focus.getNamedChild("text"), fixed.getTextElement(), "text"); + checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriod(), "period"); + + List parts = new ArrayList(); + focus.getNamedChildren("family", parts); + if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getFamily().size(), + "Expected " + Integer.toString(fixed.getFamily().size()) + " but found " + Integer.toString(parts.size()) + " family elements")) { + for (int i = 0; i < parts.size(); i++) + checkFixedValue(errors, path + ".family", parts.get(i), fixed.getFamily().get(i), "family"); + } + focus.getNamedChildren("given", parts); + if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getGiven().size(), + "Expected " + Integer.toString(fixed.getGiven().size()) + " but found " + Integer.toString(parts.size()) + " given elements")) { + for (int i = 0; i < parts.size(); i++) + checkFixedValue(errors, path + ".given", parts.get(i), fixed.getGiven().get(i), "given"); + } + focus.getNamedChildren("prefix", parts); + if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getPrefix().size(), + "Expected " + Integer.toString(fixed.getPrefix().size()) + " but found " + Integer.toString(parts.size()) + " prefix elements")) { + for (int i = 0; i < parts.size(); i++) + checkFixedValue(errors, path + ".prefix", parts.get(i), fixed.getPrefix().get(i), "prefix"); + } + focus.getNamedChildren("suffix", parts); + if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getSuffix().size(), + "Expected " + Integer.toString(fixed.getSuffix().size()) + " but found " + Integer.toString(parts.size()) + " suffix elements")) { + for (int i = 0; i < parts.size(); i++) + checkFixedValue(errors, path + ".suffix", parts.get(i), fixed.getSuffix().get(i), "suffix"); + } + } + + private void checkIdentifier(List errors, String path, WrapperElement element, ElementDefinition context) { + String system = element.getNamedChildValue("system"); + rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, isAbsolute(system), "Identifier.system must be an absolute reference, not a local reference"); + } + + private void checkIdentifier(List errors, String path, WrapperElement focus, Identifier fixed) { + checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), "use"); + checkFixedValue(errors, path + ".type", focus.getNamedChild("type"), fixed.getType(), "type"); + checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), "system"); + checkFixedValue(errors, path + ".value", focus.getNamedChild("value"), fixed.getValueElement(), "value"); + checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriod(), "period"); + checkFixedValue(errors, path + ".assigner", focus.getNamedChild("assigner"), fixed.getAssigner(), "assigner"); + } + + private void checkPeriod(List errors, String path, WrapperElement focus, Period fixed) { + checkFixedValue(errors, path + ".start", focus.getNamedChild("start"), fixed.getStartElement(), "start"); + checkFixedValue(errors, path + ".end", focus.getNamedChild("end"), fixed.getEndElement(), "end"); + } + + private void checkPrimitive(List errors, String path, String type, ElementDefinition context, WrapperElement e) throws Exception { + if (type.equals("uri")) { + rule(errors, IssueType.INVALID, e.line(), e.col(), path, !e.getAttribute("value").startsWith("oid:"), "URI values cannot start with oid:"); + rule(errors, IssueType.INVALID, e.line(), e.col(), path, !e.getAttribute("value").startsWith("uuid:"), "URI values cannot start with uuid:"); + rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.getAttribute("value").equals(e.getAttribute("value").trim()), "URI values cannot have leading or trailing whitespace"); + } + if (!type.equalsIgnoreCase("string") && e.hasAttribute("value")) { + if (rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.getAttribute("value").length() > 0, "@value cannot be empty")) { + warning(errors, IssueType.INVALID, e.line(), e.col(), path, e.getAttribute("value").trim().equals(e.getAttribute("value")), "value should not start or finish with whitespace"); + } + } + if (type.equals("dateTime")) { + rule(errors, IssueType.INVALID, e.line(), e.col(), path, yearIsValid(e.getAttribute("value")), "The value '" + e.getAttribute("value") + "' does not have a valid year"); + rule(errors, IssueType.INVALID, e.line(), e.col(), path, + e.getAttribute("value") + .matches("-?[0-9]{4}(-(0[1-9]|1[0-2])(-(0[0-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))?)?)?)?"), + "Not a valid date time"); + rule(errors, IssueType.INVALID, e.line(), e.col(), path, !hasTime(e.getAttribute("value")) || hasTimeZone(e.getAttribute("value")), "if a date has a time, it must have a timezone"); + + } + if (type.equals("instant")) { + rule(errors, IssueType.INVALID, e.line(), e.col(), path, + e.getAttribute("value").matches("-?[0-9]{4}-(0[1-9]|1[0-2])-(0[0-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))"), + "The instant '" + e.getAttribute("value") + "' is not valid (by regex)"); + rule(errors, IssueType.INVALID, e.line(), e.col(), path, yearIsValid(e.getAttribute("value")), "The value '" + e.getAttribute("value") + "' does not have a valid year"); + } + + if (type.equals("code")) { + // Technically, a code is restricted to string which has at least one character and no leading or trailing whitespace, and where there is no whitespace + // other than single spaces in the contents + rule(errors, IssueType.INVALID, e.line(), e.col(), path, passesCodeWhitespaceRules(e.getAttribute("value")), "The code '" + e.getAttribute("value") + "' is not valid (whitespace rules)"); + } + + if (context.hasBinding()) { + checkPrimitiveBinding(errors, path, type, context, e); + } + // for nothing to check + } + + // note that we don't check the type here; it could be string, uri or code. + private void checkPrimitiveBinding(List errors, String path, String type, ElementDefinition context, WrapperElement element) throws Exception { + if (!element.hasAttribute("value")) + return; + + String value = element.getAttribute("value"); + + // System.out.println("check "+value+" in "+path); + + // firstly, resolve the value set + ElementDefinitionBindingComponent binding = context.getBinding(); + if (binding.hasValueSet() && binding.getValueSet() instanceof Reference) { + ValueSet vs = resolveBindingReference(binding.getValueSet()); + if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, vs != null, "ValueSet {0} not found", describeReference(binding.getValueSet()))) { + try { + ValueSetExpansionOutcome expansionOutcome = cache.getExpander().expand(vs); + vs = expansionOutcome != null ? expansionOutcome.getValueset() : null; + if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, vs != null, "Unable to expand value set for {0}", describeReference(binding.getValueSet()))) { + boolean ok = codeInExpansion(vs, null, value); + if (binding.getStrength() == BindingStrength.REQUIRED) + rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, ok, "Coded value {0} is not in value set {1} ({2})", value, describeReference(binding.getValueSet()), + vs.getUrl()); + else if (binding.getStrength() == BindingStrength.EXTENSIBLE) + warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, ok, "Coded value {0} is not in value set {1} ({2})", value, describeReference(binding.getValueSet()), + vs.getUrl()); + else + hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, ok, "Coded value {0} is not in value set {1} ({2})", value, describeReference(binding.getValueSet()), + vs.getUrl()); + } + } catch (ETooCostly e) { + if (e.getMessage() == null) + warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, + "Exception opening value set " + vs.getUrl() + " for " + describeReference(binding.getValueSet()) + ": --Null--"); + else + warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, + "Exception opening value set " + vs.getUrl() + " for " + describeReference(binding.getValueSet()) + ": " + e.getMessage()); + } + } + } else + hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding has no source, so can't be checked"); + } + + private void checkQuantity(List errors, String path, WrapperElement focus, Quantity fixed) { + checkFixedValue(errors, path + ".value", focus.getNamedChild("value"), fixed.getValueElement(), "value"); + checkFixedValue(errors, path + ".comparator", focus.getNamedChild("comparator"), fixed.getComparatorElement(), "comparator"); + checkFixedValue(errors, path + ".units", focus.getNamedChild("unit"), fixed.getUnitElement(), "units"); + checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), "system"); + checkFixedValue(errors, path + ".code", focus.getNamedChild("code"), fixed.getCodeElement(), "code"); + } + + // implementation + + private void checkRange(List errors, String path, WrapperElement focus, Range fixed) { + checkFixedValue(errors, path + ".low", focus.getNamedChild("low"), fixed.getLow(), "low"); + checkFixedValue(errors, path + ".high", focus.getNamedChild("high"), fixed.getHigh(), "high"); + + } + + private void checkRatio(List errors, String path, WrapperElement focus, Ratio fixed) { + checkFixedValue(errors, path + ".numerator", focus.getNamedChild("numerator"), fixed.getNumerator(), "numerator"); + checkFixedValue(errors, path + ".denominator", focus.getNamedChild("denominator"), fixed.getDenominator(), "denominator"); + } + + private void checkReference(List errors, String path, WrapperElement element, StructureDefinition profile, ElementDefinition container, String parentType, NodeStack stack) + throws Exception { + String ref = element.getNamedChildValue("reference"); + if (Utilities.noString(ref)) { + // todo - what should we do in this case? + hint(errors, IssueType.STRUCTURE, element.line(), element.col(), path, !Utilities.noString(element.getNamedChildValue("display")), + "A Reference without an actual reference should have a display"); + return; + } + + WrapperElement we = resolve(ref, stack); + String ft; + if (we != null) + ft = we.getResourceType(); + else + ft = tryParse(ref); + if (hint(errors, IssueType.STRUCTURE, element.line(), element.col(), path, ft != null, "Unable to determine type of target resource")) { + boolean ok = false; + CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); + for (TypeRefComponent type : container.getType()) { + if (!ok && type.getCode().equals("Reference")) { + // we validate as much as we can. First, can we infer a type from the profile? + if (!type.hasProfile() || type.getProfile().get(0).getValue().equals("http://hl7.org/fhir/StructureDefinition/Resource")) + ok = true; + else { + String pr = type.getProfile().get(0).getValue(); + + String bt = getBaseType(profile, pr); + if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, bt != null, "Unable to resolve the profile reference '" + pr + "'")) { + b.append(bt); + ok = bt.equals(ft); + } else + ok = true; // suppress following check + } + } + if (!ok && type.getCode().equals("*")) { + ok = true; // can refer to anything + } + } + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, ok, "Invalid Resource target type. Found " + ft + ", but expected one of (" + b.toString() + ")"); + } + } + + private String checkResourceType(String type) throws EOperationOutcome, Exception { + if (context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + type) != null) + return type; + else + return null; + } + + private void checkSampledData(List errors, String path, WrapperElement focus, SampledData fixed) { + checkFixedValue(errors, path + ".origin", focus.getNamedChild("origin"), fixed.getOrigin(), "origin"); + checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriodElement(), "period"); + checkFixedValue(errors, path + ".factor", focus.getNamedChild("factor"), fixed.getFactorElement(), "factor"); + checkFixedValue(errors, path + ".lowerLimit", focus.getNamedChild("lowerLimit"), fixed.getLowerLimitElement(), "lowerLimit"); + checkFixedValue(errors, path + ".upperLimit", focus.getNamedChild("upperLimit"), fixed.getUpperLimitElement(), "upperLimit"); + checkFixedValue(errors, path + ".dimensions", focus.getNamedChild("dimensions"), fixed.getDimensionsElement(), "dimensions"); + checkFixedValue(errors, path + ".data", focus.getNamedChild("data"), fixed.getDataElement(), "data"); + } + + private void checkTiming(List errors, String path, WrapperElement focus, Timing fixed) { + checkFixedValue(errors, path + ".repeat", focus.getNamedChild("repeat"), fixed.getRepeat(), "value"); + + List events = new ArrayList(); + focus.getNamedChildren("event", events); + if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, events.size() == fixed.getEvent().size(), + "Expected " + Integer.toString(fixed.getEvent().size()) + " but found " + Integer.toString(events.size()) + " event elements")) { + for (int i = 0; i < events.size(); i++) + checkFixedValue(errors, path + ".event", events.get(i), fixed.getEvent().get(i), "event"); + } + } + + private boolean codeinExpansion(ValueSetExpansionContainsComponent cnt, String system, String code) { + for (ValueSetExpansionContainsComponent c : cnt.getContains()) { + if (code.equals(c.getCode()) && system.equals(c.getSystem().toString())) + return true; + if (codeinExpansion(c, system, code)) + return true; + } + return false; + } + + private boolean codeInExpansion(ValueSet vs, String system, String code) { + for (ValueSetExpansionContainsComponent c : vs.getExpansion().getContains()) { + if (code.equals(c.getCode()) && (system == null || system.equals(c.getSystem()))) + return true; + if (codeinExpansion(c, system, code)) + return true; + } + return false; + } + + private String describeReference(Type reference) { + if (reference == null) + return "null"; + if (reference instanceof UriType) + return ((UriType) reference).getValue(); + if (reference instanceof Reference) + return ((Reference) reference).getReference(); + return "??"; + } + + private String describeTypes(List types) { + CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); + for (TypeRefComponent t : types) { + b.append(t.getCode()); + } + return b.toString(); + } + + private boolean empty(WrapperElement element) { + if (element.hasAttribute("value")) + return false; + if (element.hasAttribute("xml:id")) + return false; + WrapperElement child = element.getFirstChild(); + while (child != null) { + if (!child.isXml() || FormatUtilities.FHIR_NS.equals(child.getNamespace())) { + return false; + } + child = child.getNextSibling(); + } + return true; + } + + private ElementDefinition findElement(StructureDefinition profile, String name) { + for (ElementDefinition c : profile.getSnapshot().getElement()) { + if (c.getPath().equals(name)) { + return c; + } + } + return null; + } + + private String genFullUrl(String bundleBase, String entryBase, String type, String id) { + String base = Utilities.noString(entryBase) ? bundleBase : entryBase; + if (Utilities.noString(base)) { + return type + "/" + id; + } else if ("urn:uuid".equals(base) || "urn:oid".equals(base)) + return base + id; + else + return Utilities.appendSlash(base) + type + "/" + id; } public BestPracticeWarningLevel getBasePracticeWarningLevel() { return bpWarnings; } - - public void setBestPracticeWarningLevel(BestPracticeWarningLevel value) { - bpWarnings = value; - } - - - // used during the build process to keep the overall volume of messages down - private boolean suppressLoincSnomedMessages; - public boolean isSuppressLoincSnomedMessages() { - return suppressLoincSnomedMessages; + private String getBaseType(StructureDefinition profile, String pr) throws EOperationOutcome, Exception { + // if (pr.startsWith("http://hl7.org/fhir/StructureDefinition/")) { + // // this just has to be a base type + // return pr.substring(40); + // } else { + StructureDefinition p = resolveProfile(profile, pr); + if (p == null) + return null; + else if (p.getKind() == StructureDefinitionKind.RESOURCE) + return p.getSnapshot().getElement().get(0).getPath(); + else + return p.getSnapshot().getElement().get(0).getType().get(0).getCode(); + // } } - public void setSuppressLoincSnomedMessages(boolean suppressLoincSnomedMessages) { - this.suppressLoincSnomedMessages = suppressLoincSnomedMessages; + + @Override + public CheckDisplayOption getCheckDisplay() { + return checkDisplay; + } + + // private String findProfileTag(WrapperElement element) { + // String uri = null; + // List list = new ArrayList(); + // element.getNamedChildren("category", list); + // for (WrapperElement c : list) { + // if ("http://hl7.org/fhir/tag/profile".equals(c.getAttribute("scheme"))) { + // uri = c.getAttribute("term"); + // } + // } + // return uri; + // } + + private ConceptDefinitionComponent getCodeDefinition(ConceptDefinitionComponent c, String code) { + if (code.equals(c.getCode())) + return c; + for (ConceptDefinitionComponent g : c.getConcept()) { + ConceptDefinitionComponent r = getCodeDefinition(g, code); + if (r != null) + return r; + } + return null; + } + + private ConceptDefinitionComponent getCodeDefinition(ValueSet vs, String code) { + for (ConceptDefinitionComponent c : vs.getCodeSystem().getConcept()) { + ConceptDefinitionComponent r = getCodeDefinition(c, code); + if (r != null) + return r; + } + return null; + } + + private WrapperElement getContainedById(WrapperElement container, String id) { + List contained = new ArrayList(); + container.getNamedChildren("contained", contained); + for (WrapperElement we : contained) { + WrapperElement res = we.isXml() ? we.getFirstChild() : we; + if (id.equals(res.getNamedChildValue("id"))) + return res; + } + return null; + } + + public IWorkerContext getContext() { + return context; + } + + private ElementDefinition getCriteriaForDiscriminator(String path, ElementDefinition ed, String discriminator, StructureDefinition profile) throws Exception { + List childDefinitions = ProfileUtilities.getChildMap(profile, ed); + List snapshot = null; + if (childDefinitions.isEmpty()) { + // going to look at the type + if (ed.getType().size() == 0) + throw new Exception("Error in profile for " + path + " no children, no type"); + if (ed.getType().size() > 1) + throw new Exception("Error in profile for " + path + " multiple types defined in slice discriminator"); + StructureDefinition type; + if (ed.getType().get(0).hasProfile()) + type = context.fetchResource(StructureDefinition.class, ed.getType().get(0).getProfile().get(0).getValue()); + else + type = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + ed.getType().get(0).getCode()); + snapshot = type.getSnapshot().getElement(); + ed = snapshot.get(0); + } else { + snapshot = profile.getSnapshot().getElement(); + } + String originalPath = ed.getPath(); + String goal = originalPath + "." + discriminator; + + int index = snapshot.indexOf(ed); + assert(index > -1); + index++; + while (index < snapshot.size() && !snapshot.get(index).getPath().equals(originalPath)) { + if (snapshot.get(index).getPath().equals(goal)) + return snapshot.get(index); + index++; + } + throw new Error("Unable to find discriminator definition for " + goal + " in " + discriminator + " at " + path); + } + + private WrapperElement getExtensionByUrl(List extensions, String urlSimple) { + for (WrapperElement e : extensions) { + if (urlSimple.equals(e.getNamedChildValue("url"))) + return e; + } + return null; + } + + public List getExtensionDomains() { + return extensionDomains; + } + + private WrapperElement getFromBundle(WrapperElement bundle, String ref) { + List entries = new ArrayList(); + bundle.getNamedChildren("entry", entries); + for (WrapperElement we : entries) { + WrapperElement res = we.getNamedChild("resource").getFirstChild(); + if (res != null) { + String url = genFullUrl(bundle.getNamedChildValue("base"), we.getNamedChildValue("base"), res.getName(), res.getNamedChildValue("id")); + if (url.endsWith(ref)) + return res; + } + } + return null; + } + + private StructureDefinition getProfileForType(String type) throws Exception { + return context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + type); + } + + private Element getValueForDiscriminator(WrapperElement element, String discriminator, ElementDefinition criteria) { + throw new Error("validation of slices not done yet"); + } + + private ValueSet getValueSet(String system) throws Exception { + return context.fetchCodeSystem(system); + } + + private boolean hasTime(String fmt) { + return fmt.contains("T"); + } + + private boolean hasTimeZone(String fmt) { + return fmt.length() > 10 && (fmt.substring(10).contains("-") || fmt.substring(10).contains("+") || fmt.substring(10).contains("Z")); + } + + private boolean isAbsolute(String uri) { + return Utilities.noString(uri) || uri.startsWith("http:") || uri.startsWith("https:") || uri.startsWith("urn:uuid:") || uri.startsWith("urn:oid:") || uri.startsWith("urn:ietf:") + || uri.startsWith("urn:iso:"); + } + + public boolean isAnyExtensionsAllowed() { + return anyExtensionsAllowed; + } + + private boolean isBundleEntry(String path) { + String[] parts = path.split("\\/"); + if (path.startsWith("/f:")) + return parts.length > 2 && parts[parts.length - 1].startsWith("f:resource") && (parts[parts.length - 2].equals("f:entry") || parts[parts.length - 2].startsWith("f:entry[")); + else + return parts.length > 2 && parts[parts.length - 1].equals("resource") && ((parts.length > 2 && parts[parts.length - 3].equals("entry")) || parts[parts.length - 2].equals("entry")); + } + + private boolean isPrimitiveType(String type) { + return type.equalsIgnoreCase("boolean") || type.equalsIgnoreCase("integer") || type.equalsIgnoreCase("string") || type.equalsIgnoreCase("decimal") || type.equalsIgnoreCase("uri") + || type.equalsIgnoreCase("base64Binary") || type.equalsIgnoreCase("instant") || type.equalsIgnoreCase("date") || type.equalsIgnoreCase("uuid") || type.equalsIgnoreCase("id") + || type.equalsIgnoreCase("xhtml") || type.equalsIgnoreCase("markdown") || type.equalsIgnoreCase("dateTime") || type.equalsIgnoreCase("time") || type.equalsIgnoreCase("code") + || type.equalsIgnoreCase("oid") || type.equalsIgnoreCase("id"); } public boolean isRequireResourceId() { return requiresResourceId; } + + public boolean isSuppressLoincSnomedMessages() { + return suppressLoincSnomedMessages; + } + + private boolean nameMatches(String name, String tail) { + if (tail.endsWith("[x]")) + return name.startsWith(tail.substring(0, tail.length() - 3)); + else + return (name.equals(tail)); + } + + // private String mergePath(String path1, String path2) { + // // path1 is xpath path + // // path2 is dotted path + // String[] parts = path2.split("\\."); + // StringBuilder b = new StringBuilder(path1); + // for (int i = 1; i < parts.length -1; i++) + // b.append("/f:"+parts[i]); + // return b.toString(); + // } + + private boolean passesCodeWhitespaceRules(String v) { + if (!v.trim().equals(v)) + return false; + boolean lastWasSpace = true; + for (char c : v.toCharArray()) { + if (c == ' ') { + if (lastWasSpace) + return false; + else + lastWasSpace = true; + } else if (Character.isWhitespace(c)) + return false; + else + lastWasSpace = false; + } + return true; + } + + private WrapperElement resolve(String ref, NodeStack stack) { + if (ref.startsWith("#")) { + // work back through the contained list. + // really, there should only be one level for this (contained resources cannot contain + // contained resources), but we'll leave that to some other code to worry about + while (stack != null && stack.getElement() != null) { + WrapperElement res = getContainedById(stack.getElement(), ref.substring(1)); + if (res != null) + return res; + stack = stack.parent; + } + return null; + } else { + // work back through the contained list - if any of them are bundles, try to resolve + // the resource in the bundle + while (stack != null && stack.getElement() != null) { + if ("Bundle".equals(stack.getElement().getResourceType())) { + WrapperElement res = getFromBundle(stack.getElement(), ref.substring(1)); + if (res != null) + return res; + } + stack = stack.parent; + } + + // todo: consult the external host for resolution + return null; + + } + } + + private ValueSet resolveBindingReference(Type reference) throws EOperationOutcome, Exception { + if (reference instanceof UriType) + return context.fetchResource(ValueSet.class, ((UriType) reference).getValue().toString()); + else if (reference instanceof Reference) + return context.fetchResource(ValueSet.class, ((Reference) reference).getReference()); + else + return null; + } + + private WrapperElement resolveInBundle(List entries, String ref, String fullUrl, String type, String id) { + if (Utilities.isAbsoluteUrl(ref)) { + // if the reference is absolute, then you resolve by fullUrl. No other thinking is required. + for (WrapperElement entry : entries) { + String fu = entry.getNamedChildValue("fullUrl"); + if (ref.equals(fu)) + return entry; + } + return null; + } else { + // split into base, type, and id + String u = null; + if (fullUrl != null && fullUrl.endsWith(type + "/" + id)) + // fullUrl = complex + u = fullUrl.substring((type + "/" + id).length()) + ref; + String[] parts = ref.split("\\/"); + if (parts.length >= 2) { + String t = parts[0]; + String i = parts[1]; + for (WrapperElement entry : entries) { + String fu = entry.getNamedChildValue("fullUrl"); + if (u != null && fullUrl.equals(u)) + return entry; + if (u == null) { + WrapperElement res = entry.getNamedChild("resource"); + WrapperElement resource = res.getFirstChild(); + String et = resource.getResourceType(); + String eid = resource.getNamedChildValue("id"); + if (t.equals(et) && i.equals(eid)) + return entry; + } + } + } + return null; + } + } + + private ElementDefinition resolveNameReference(StructureDefinitionSnapshotComponent snapshot, String name) { + for (ElementDefinition ed : snapshot.getElement()) + if (name.equals(ed.getName())) + return ed; + return null; + } + + private StructureDefinition resolveProfile(StructureDefinition profile, String pr) throws EOperationOutcome, Exception { + if (pr.startsWith("#")) { + for (Resource r : profile.getContained()) { + if (r.getId().equals(pr.substring(1)) && r instanceof StructureDefinition) + return (StructureDefinition) r; + } + return null; + } else + return context.fetchResource(StructureDefinition.class, pr); + } + + private ElementDefinition resolveType(String type) throws EOperationOutcome, Exception { + String url = "http://hl7.org/fhir/StructureDefinition/" + type; + StructureDefinition sd = context.fetchResource(StructureDefinition.class, url); + if (sd == null || !sd.hasSnapshot()) + return null; + else + return sd.getSnapshot().getElement().get(0); + } + + public void setAnyExtensionsAllowed(boolean anyExtensionsAllowed) { + this.anyExtensionsAllowed = anyExtensionsAllowed; + } + + public void setBestPracticeWarningLevel(BestPracticeWarningLevel value) { + bpWarnings = value; + } + + @Override + public void setCheckDisplay(CheckDisplayOption checkDisplay) { + this.checkDisplay = checkDisplay; + } + public void setRequireResourceId(boolean requiresResourceId) { this.requiresResourceId = requiresResourceId; } - - public boolean isAnyExtensionsAllowed() { - return anyExtensionsAllowed; - } - public void setAnyExtensionsAllowed(boolean anyExtensionsAllowed) { - this.anyExtensionsAllowed = anyExtensionsAllowed; - } - public List getExtensionDomains() { - return extensionDomains; - } - - // public API + + public void setSuppressLoincSnomedMessages(boolean suppressLoincSnomedMessages) { + this.suppressLoincSnomedMessages = suppressLoincSnomedMessages; + } + + /** + * + * @param element + * - the candidate that might be in the slice + * @param path + * - for reporting any errors. the XPath for the element + * @param slice + * - the definition of how slicing is determined + * @param ed + * - the slice for which to test membership + * @return + * @throws Exception + */ + private boolean sliceMatches(WrapperElement element, String path, ElementDefinition slice, ElementDefinition ed, StructureDefinition profile) throws Exception { + if (!slice.getSlicing().hasDiscriminator()) + return false; // cannot validate in this case + for (StringType s : slice.getSlicing().getDiscriminator()) { + String discriminator = s.getValue(); + ElementDefinition criteria = getCriteriaForDiscriminator(path, ed, discriminator, profile); + if (discriminator.equals("url") && criteria.getPath().equals("Extension.url")) { + if (!element.getAttribute("url").equals(((UriType) criteria.getFixed()).asStringValue())) + return false; + } else { + Element value = getValueForDiscriminator(element, discriminator, criteria); + if (!valueMatchesCriteria(value, criteria)) + return false; + } + } + return true; + } + + // we assume that the following things are true: + // the instance at root is valid against the schema and schematron + // the instance validator had no issues against the base resource profile + private void start(List errors, WrapperElement element, StructureDefinition profile, NodeStack stack) throws Exception { + // profile is valid, and matches the resource name + if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), profile.hasSnapshot(), + "StructureDefinition has no snapshot - validation is against the snapshot, so it must be provided")) { + validateElement(errors, profile, profile.getSnapshot().getElement().get(0), null, null, element, element.getName(), stack); + + checkDeclaredProfiles(errors, element, stack); + + // specific known special validations + if (element.getResourceType().equals("Bundle")) + validateBundle(errors, element, stack); + if (element.getResourceType().equals("Observation")) + validateObservation(errors, element, stack); + } + } + + private String tail(String path) { + return path.substring(path.lastIndexOf(".") + 1); + } + + private String tryParse(String ref) throws EOperationOutcome, Exception { + String[] parts = ref.split("\\/"); + switch (parts.length) { + case 1: + return null; + case 2: + return checkResourceType(parts[0]); + default: + if (parts[parts.length - 2].equals("_history")) + return checkResourceType(parts[parts.length - 4]); + else + return checkResourceType(parts[parts.length - 2]); + } + } + + private boolean typesAreAllReference(List theType) { + for (TypeRefComponent typeRefComponent : theType) { + if (typeRefComponent.getCode().equals("Reference") == false) { + return false; + } + } + return true; + } @Override - public List validate(JsonObject obj) throws Exception { + public List validate(Document document) throws Exception { List results = new ArrayList(); - validate(results, obj); + validate(results, document); + return results; + } + + @Override + public List validate(Document document, String profile) throws Exception { + List results = new ArrayList(); + validate(results, document, profile); + return results; + } + + @Override + public List validate(Document document, StructureDefinition profile) throws Exception { + List results = new ArrayList(); + validate(results, document, profile); return results; } @@ -160,16 +1302,25 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat validate(results, element); return results; } + @Override public List validate(Element element, String profile) throws Exception { List results = new ArrayList(); validate(results, element, profile); return results; } + @Override - public List validate(JsonObject obj, StructureDefinition profile) throws Exception { + public List validate(Element element, StructureDefinition profile) throws Exception { List results = new ArrayList(); - validate(results, obj, profile); + validate(results, element, profile); + return results; + } + + @Override + public List validate(JsonObject obj) throws Exception { + List results = new ArrayList(); + validate(results, obj); return results; } @@ -181,737 +1332,70 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } @Override - public List validate(Element element, StructureDefinition profile) throws Exception { + public List validate(JsonObject obj, StructureDefinition profile) throws Exception { List results = new ArrayList(); - validate(results, element, profile); + validate(results, obj, profile); return results; } - - @Override - public List validate(Document document) throws Exception { - List results = new ArrayList(); - validate(results, document); - return results; - } - - @Override - public List validate(Document document, String profile) throws Exception { - List results = new ArrayList(); - validate(results, document, profile); - return results; - } - @Override - public List validate(Document document, StructureDefinition profile) throws Exception { - List results = new ArrayList(); - validate(results, document, profile); - return results; - } - - @Override - public void validate(List errors, Element element) throws Exception { - validateResource(errors, new DOMWrapperElement(element), null, requiresResourceId, null); - } - @Override - public void validate(List errors, JsonObject object) throws Exception { - validateResource(errors, new JsonWrapperElement(object), null, requiresResourceId, null); - } - @Override - public void validate(List errors, Element element, String profile) throws Exception { - StructureDefinition p = context.fetchResource(StructureDefinition.class, profile); - if (p == null) - throw new Exception("StructureDefinition '"+profile+"' not found"); - validateResource(errors, new DOMWrapperElement(element), p, requiresResourceId, null); - } - @Override - public void validate(List errors, Element element, StructureDefinition profile) throws Exception { - validateResource(errors, new DOMWrapperElement(element), profile, requiresResourceId, null); - } - - @Override - public void validate(List errors, JsonObject object, StructureDefinition profile) throws Exception { - validateResource(errors, new JsonWrapperElement(object), profile, requiresResourceId, null); - } - - @Override - public void validate(List errors, JsonObject object, String profile) throws Exception { - StructureDefinition p = context.fetchResource(StructureDefinition.class, profile); - if (p == null) - throw new Exception("StructureDefinition '"+profile+"' not found"); - validateResource(errors, new JsonWrapperElement(object), p, requiresResourceId, null); - } - + @Override public void validate(List errors, Document document) throws Exception { - checkForProcessingInstruction(errors, document); + checkForProcessingInstruction(errors, document); validateResource(errors, new DOMWrapperElement(document.getDocumentElement()), null, requiresResourceId, null); } + @Override public void validate(List errors, Document document, String profile) throws Exception { - checkForProcessingInstruction(errors, document); + checkForProcessingInstruction(errors, document); StructureDefinition p = context.fetchResource(StructureDefinition.class, profile); if (p == null) - throw new Exception("StructureDefinition '"+profile+"' not found"); + throw new Exception("StructureDefinition '" + profile + "' not found"); validateResource(errors, new DOMWrapperElement(document.getDocumentElement()), p, requiresResourceId, null); } @Override public void validate(List errors, Document document, StructureDefinition profile) throws Exception { - checkForProcessingInstruction(errors, document); + checkForProcessingInstruction(errors, document); validateResource(errors, new DOMWrapperElement(document.getDocumentElement()), profile, requiresResourceId, null); } - - - // implementation - - private void checkForProcessingInstruction(List errors, Document document) { - Node node = document.getFirstChild(); - while (node != null) { - rule(errors, IssueType.INVALID, -1, -1, "(document)", node.getNodeType() != Node.PROCESSING_INSTRUCTION_NODE, "No processing instructions allowed in resources"); - node = node.getNextSibling(); - } + + @Override + public void validate(List errors, Element element) throws Exception { + validateResource(errors, new DOMWrapperElement(element), null, requiresResourceId, null); } - public abstract class WrapperElement { - public abstract WrapperElement getNamedChild(String name); - public abstract WrapperElement getFirstChild(); - public abstract WrapperElement getNextSibling(); - public abstract String getName(); - public abstract String getResourceType(); - public abstract String getNamedChildValue(String name); - public abstract void getNamedChildren(String name, List list); - public abstract String getAttribute(String name); - public abstract void getNamedChildrenWithWildcard(String name, List list); - public abstract boolean hasAttribute(String name); - public abstract String getNamespace(); - public abstract boolean isXml(); - - public abstract String getText(); - public abstract boolean hasNamespace(String string); - public abstract boolean hasProcessingInstruction(); - public abstract int line(); - public abstract int col(); + @Override + public void validate(List errors, Element element, String profile) throws Exception { + StructureDefinition p = context.fetchResource(StructureDefinition.class, profile); + if (p == null) + throw new Exception("StructureDefinition '" + profile + "' not found"); + validateResource(errors, new DOMWrapperElement(element), p, requiresResourceId, null); } - public class DOMWrapperElement extends WrapperElement { - - private Element element; - private int line; - private int col; - - public DOMWrapperElement(Element element) { - super(); - this.element = element; - XmlLocationData loc = (XmlLocationData) element.getUserData(XmlLocationData.LOCATION_DATA_KEY ); - if (loc != null) { - line = loc.getStartLine(); - col = loc.getStartColumn(); - } else { - line = -1; - col = -1; - } - } - - @Override - public WrapperElement getNamedChild(String name) { - Element res = XMLUtil.getNamedChild(element, name); - return res == null ? null : new DOMWrapperElement(res); - } - - @Override - public WrapperElement getFirstChild() { - Element res = XMLUtil.getFirstChild(element); - return res == null ? null : new DOMWrapperElement(res); - } - - @Override - public WrapperElement getNextSibling() { - Element res = XMLUtil.getNextSibling(element); - return res == null ? null : new DOMWrapperElement(res); - } - - @Override - public String getName() { - return element.getLocalName(); - } - - @Override - public String getNamedChildValue(String name) { - return XMLUtil.getNamedChildValue(element, name); - } - - @Override - public void getNamedChildren(String name, List list) { - List el = new ArrayList(); - XMLUtil.getNamedChildren(element, name, el); - for (Element e : el) - list.add(new DOMWrapperElement(e)); - } - - @Override - public String getAttribute(String name) { - return element.getAttribute(name); - } - - @Override - public void getNamedChildrenWithWildcard(String name, List list) { - List el = new ArrayList(); - XMLUtil.getNamedChildrenWithWildcard(element, name, el); - for (Element e : el) - list.add(new DOMWrapperElement(e)); - } - - @Override - public boolean hasAttribute(String name) { - return element.hasAttribute(name); - } - - @Override - public String getNamespace() { - return element.getNamespaceURI(); - } - - @Override - public boolean isXml() { - return true; - } - - @Override - public String getText() { - return element.getTextContent(); - } - - @Override - public boolean hasNamespace(String ns) { - for (int i = 0; i < element.getAttributes().getLength(); i++) { - Node a = element.getAttributes().item(i); - if ((a.getNodeName().equals("xmlns") || a.getNodeName().startsWith("xmlns:")) && a.getNodeValue().equals(ns)) - return true; - } - return false; - } - - @Override - public boolean hasProcessingInstruction() { - Node node = element.getFirstChild(); - while (node != null) { - if (node.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) - return true; - node = node.getNextSibling(); - } - return false; - } - - @Override - public String getResourceType() { - return element.getLocalName(); - } - - @Override - public int line() { - return line; - } - - @Override - public int col() { - return col; - } - + @Override + public void validate(List errors, Element element, StructureDefinition profile) throws Exception { + validateResource(errors, new DOMWrapperElement(element), profile, requiresResourceId, null); } - public class JsonWrapperElement extends WrapperElement { - - private String path; - private JsonElement element; - private JsonElement _element; - private String name; - private String resourceType; - private JsonWrapperElement parent; - private int index; - private List children = new ArrayList(); - - public JsonWrapperElement(String path, String name, JsonElement element, JsonElement _element, JsonWrapperElement parent, int index) { - super(); - this.path = path+"/"+name; - this.name = name; - this.element = element; - if (element instanceof JsonObject && ((JsonObject) element).has("resourceType")) - this.resourceType = ((JsonObject) element).get("resourceType").getAsString(); - this._element = _element; - this.parent = parent; - this.index = index; - createChildren(); - } - - public JsonWrapperElement(JsonElement element) { - super(); - this.name = null; - this.resourceType = ((JsonObject) element).get("resourceType").getAsString(); - this.element = element; - this.path =""; - createChildren(); - } - - private void createChildren() { -// System.out.println(" ..: "+path); - // we're going to make this look like the XML - if (element == null) - throw new Error("not done yet"); - - if (element instanceof JsonPrimitive) { - // we may have an element_ too - if (_element != null && _element instanceof JsonObject) - for (Entry t : ((JsonObject) _element).entrySet()) - processChild(t.getKey(), t.getValue()); - } else if (element instanceof JsonObject) { - for (Entry t : ((JsonObject) element).entrySet()) - if (!t.getKey().equals("resourceType")) { - processChild(t.getKey(), t.getValue()); - } - } else if (element instanceof JsonNull) { - // nothing to do - } else - throw new Error("unexpected condition: "+element.getClass().getName()); - } - - private void processChild(String name, JsonElement e) throws Error { - if (name.startsWith("_")) { - name = name.substring(1); - if (((JsonObject) element).has(name)) - return; // it will get processed anyway - e = null; - } - JsonElement _e = element instanceof JsonObject ? ((JsonObject) element).get("_"+name) : null; - - if (e instanceof JsonPrimitive || (e == null && _e != null && !(_e instanceof JsonArray))) { - children.add(new JsonWrapperElement(path, name, e, _e, this, children.size())); - } else if (e instanceof JsonArray || (e == null && _e != null)) { - JsonArray array = (JsonArray) e; - JsonArray _array = (JsonArray) _e; - int max = array != null ? array.size() : 0; - if (_array != null && _array.size() > max) - max = _array.size(); - for (int i = 0; i < max; i++) { - JsonElement a = array == null || array.size() < i ? null : array.get(i); - JsonElement _a = _array == null || _array.size() < i ? null : _array.get(i); - children.add(new JsonWrapperElement(path, name, a, _a, this, children.size())); - } - } else if (e instanceof JsonObject) { - children.add(new JsonWrapperElement(path, name, e, null, this, children.size())); - } else - throw new Error("not done yet: "+e.getClass().getName()); - } - - @Override - public WrapperElement getNamedChild(String name) { - for (JsonWrapperElement j : children) - if (j.name.equals(name)) - return j; - return null; - } - - @Override - public WrapperElement getFirstChild() { - if (children.isEmpty()) - return null; - else - return children.get(0); - } - - @Override - public WrapperElement getNextSibling() { - if (parent == null) - return null; - if (index >= parent.children.size() - 1) - return null; - return parent.children.get(index+1); - } - - @Override - public String getName() { - return name; - } - - @Override - public String getNamedChildValue(String name) { - WrapperElement c = getNamedChild(name); - return c == null ? null : c.getAttribute("value"); - } - - @Override - public void getNamedChildren(String name, List list) { - for (JsonWrapperElement j : children) - if (j.name.equals(name)) - list.add(j); - } - - @Override - public String getAttribute(String name) { - if (name.equals("value")) { - if (element == null) - return null; - if (element instanceof JsonPrimitive) - return ((JsonPrimitive) element).getAsString(); - return null; - } - if (name.equals("xml:id")) { - WrapperElement c = getNamedChild("id"); - return c == null ? null : c.getAttribute("value"); - } - if (name.equals("url")) { - WrapperElement c = getNamedChild("url"); - return c == null ? null : c.getAttribute("value"); - } - throw new Error("not done yet: "+name); - } - - @Override - public void getNamedChildrenWithWildcard(String name, List list) { - throw new Error("not done yet"); - } - - @Override - public boolean hasAttribute(String name) { - if (name.equals("value")) { - if (element == null) - return false; - if (element instanceof JsonPrimitive) - return true; - return false; - } - if (name.equals("xml:id")) { - return getNamedChild("id") != null; - } - throw new Error("not done yet: "+name); - } - - @Override - public String getNamespace() { -// return element.getNamespaceURI(); - throw new Error("not done yet"); - } - - @Override - public boolean isXml() { - return false; - } - - @Override - public String getText() { - throw new Error("not done yet"); - } - - @Override - public boolean hasNamespace(String ns) { - throw new Error("not done"); - } - - @Override - public boolean hasProcessingInstruction() { - return false; - } - - @Override - public String getResourceType() { - return resourceType; - } - - @Override - public int line() { - return -1; - } - - @Override - public int col() { - // TODO Auto-generated method stub - return -1; - } - + @Override + public void validate(List errors, JsonObject object) throws Exception { + validateResource(errors, new JsonWrapperElement(object), null, requiresResourceId, null); } - public class ChildIterator { - private WrapperElement parent; - private String basePath; - private int lastCount; - private WrapperElement child; - - public ChildIterator(String path, WrapperElement element) { - parent = element; - basePath = path; - } - - public boolean next() { - if (child == null) { - child = parent.getFirstChild(); - lastCount = 0; - } else { - String lastName = child.getName(); - child = child.getNextSibling(); - if (child != null && child.getName().equals(lastName)) - lastCount++; - else - lastCount = 0; - } - return child != null; - } - - public String name() { - return child.getName(); - } - - public WrapperElement element() { - return child; - } - - public String path() { - WrapperElement n = child.getNextSibling(); - if (parent.isXml()) { - String sfx = ""; - if (n != null && n.getName().equals(child.getName())) { - sfx = "["+Integer.toString(lastCount+1)+"]"; - } - String prefix; - String namespace = element().getNamespace(); - if ("http://www.w3.org/1999/xhtml".equals(namespace)) { - prefix = "/h:"; - } else { - prefix = "/f:"; - } - String retVal = basePath + prefix+name() + sfx; - return retVal; - } else { - String sfx = ""; - if (n != null && n.getName().equals(child.getName())) { - sfx = "/"+Integer.toString(lastCount+1); - } - return basePath+"/"+name()+sfx; - } - } - - public int count() { - WrapperElement n = child.getNextSibling(); - if (n != null && n.getName().equals(child.getName())) { - return lastCount+1; - } else - return -1; - } - } - - private class NodeStack { - private boolean xml; - private NodeStack parent; - private String literalPath; // xpath format - private List logicalPaths; // dotted format, various entry points - private WrapperElement element; - private ElementDefinition definition; - private ElementDefinition type; - private ElementDefinition extension; - - public NodeStack(boolean xml) { - this.xml = xml; - } - - private NodeStack push(WrapperElement element, int count, ElementDefinition definition, ElementDefinition type) { - NodeStack res = new NodeStack(element.isXml()); - res.parent = this; - res.element = element; - res.definition = definition; - if (element.isXml()) { - res.literalPath = getLiteralPath() + (element.getNamespace().equals(FormatUtilities.XHTML_NS) ? "/h:" : "/f:")+element.getName(); - if (count > -1) - res.literalPath = res.literalPath + "["+Integer.toString(count)+"]"; - } else { - if (element.getName() == null) - res.literalPath = ""; - else - res.literalPath = getLiteralPath() + "/" +element.getName(); - if (count > -1) - res.literalPath = res.literalPath + "/"+Integer.toString(count); - } - res.logicalPaths = new ArrayList(); - if (type != null) { - // type will be bull if we on a stitching point of a contained resource, or if.... - res.type = type; - String t = tail(definition.getPath()); - for (String lp : getLogicalPaths()) { - res.logicalPaths.add(lp+"."+t); - if (t.endsWith("[x]")) - res.logicalPaths.add(lp+"."+t.substring(0, t.length()-3)+type.getPath()); - } - res.logicalPaths.add(type.getPath()); - } else if (definition != null) { - for (String lp : getLogicalPaths()) - res.logicalPaths.add(lp+"."+element.getName()); - } else - res.logicalPaths.addAll(getLogicalPaths()); -// CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); -// for (String lp : res.logicalPaths) -// b.append(lp); -// System.out.println(res.literalPath+" : "+b.toString()); - return res; - } - - private String getLiteralPath() { - return literalPath == null ? "" : literalPath; - } - private List getLogicalPaths() { - return logicalPaths == null ? new ArrayList() : logicalPaths; - } - - private WrapperElement getElement() { - return element; - } - - private ElementDefinition getType() { - return type; - } - - private ElementDefinition getDefinition() { - return definition; - } - - private void setType(ElementDefinition type) { - this.type = type; - } - - public String addToLiteralPath(String... path) { - StringBuilder b = new StringBuilder(); - b.append(getLiteralPath()); - if (xml) { - for (String p : path) { - if (p.startsWith(":")) { - b.append("["); - b.append(p.substring(1)); - b.append("]"); - } else { - b.append("/f:"); - b.append(p); - } - } - } else { - for (String p : path) { - b.append("/"); - if (p.startsWith(":")) { - b.append(p.substring(1)); - } else { - b.append(p); - } - } - } - return b.toString(); - } - } - - private IWorkerContext context; - private ValueSetExpanderFactory cache; - private boolean requiresResourceId; - private List extensionDomains = new ArrayList(); - private boolean anyExtensionsAllowed; - - public InstanceValidator(IWorkerContext theContext) throws Exception { - super(); - this.context = theContext; - source = Source.InstanceValidator; - cache = new ValueSetExpansionCache(theContext, null); - } - - - public InstanceValidator(IWorkerContext theContext, ValueSetExpanderFactory theValueSetExpander) throws Exception { - super(); - this.context = theContext; - source = Source.InstanceValidator; - this.cache = theValueSetExpander; - } - - - public IWorkerContext getContext() { - return context; - } - /* - * The actual base entry point - */ - private void validateResource(List errors, WrapperElement element, StructureDefinition profile, boolean needsId, NodeStack stack) throws Exception { - if (stack == null) - stack = new NodeStack(element.isXml()); - - // getting going - either we got a profile, or not. - boolean ok = true; - if (element.isXml()) { - ok = rule(errors, IssueType.INVALID, element.line(), element.col(), "/", element.getNamespace().equals(FormatUtilities.FHIR_NS), "Namespace mismatch - expected '"+FormatUtilities.FHIR_NS+"', found '"+element.getNamespace()+"'"); - } - if (ok) { - String resourceName = element.getResourceType(); - if (profile == null) { - profile = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+resourceName); - ok = rule(errors, IssueType.INVALID, element.line(), element.col(), stack.addToLiteralPath(resourceName), profile != null, "No profile found for resource type '"+resourceName+"'"); - } else { - String type = profile.hasConstrainedType() ? profile.getConstrainedType() : profile.getName(); - ok = rule(errors, IssueType.INVALID, -1, -1, stack.addToLiteralPath(resourceName), type.equals(resourceName), "Specified profile type was '"+profile.getConstrainedType()+"', but resource type was '"+resourceName+"'"); - } - } - - if (ok) { - stack = stack.push(element, -1, profile.getSnapshot().getElement().get(0), profile.getSnapshot().getElement().get(0)); - if (needsId && (element.getNamedChild("id") == null)) - rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), false, "Resource has no id"); - start(errors, element, profile, stack); // root is both definition and type - } - } - - - // we assume that the following things are true: - // the instance at root is valid against the schema and schematron - // the instance validator had no issues against the base resource profile - private void start(List errors, WrapperElement element, StructureDefinition profile, NodeStack stack) throws Exception { - // profile is valid, and matches the resource name - if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), profile.hasSnapshot(), "StructureDefinition has no snapshot - validation is against the snapshot, so it must be provided")) { - validateElement(errors, profile, profile.getSnapshot().getElement().get(0), null, null, element, element.getName(), stack); - - checkDeclaredProfiles(errors, element, stack); - - // specific known special validations - if (element.getResourceType().equals("Bundle")) - validateBundle(errors, element, stack); - if (element.getResourceType().equals("Observation")) - validateObservation(errors, element, stack); - } + @Override + public void validate(List errors, JsonObject object, String profile) throws Exception { + StructureDefinition p = context.fetchResource(StructureDefinition.class, profile); + if (p == null) + throw new Exception("StructureDefinition '" + profile + "' not found"); + validateResource(errors, new JsonWrapperElement(object), p, requiresResourceId, null); } -// private String findProfileTag(WrapperElement element) { -// String uri = null; -// List list = new ArrayList(); -// element.getNamedChildren("category", list); -// for (WrapperElement c : list) { -// if ("http://hl7.org/fhir/tag/profile".equals(c.getAttribute("scheme"))) { -// uri = c.getAttribute("term"); -// } -// } -// return uri; -// } - - - private void checkDeclaredProfiles(List errors, WrapperElement element, NodeStack stack) throws Exception { - WrapperElement meta = element.getNamedChild("meta"); - if (meta != null) { - List profiles = new ArrayList(); - meta.getNamedChildren("profile", profiles); - int i = 0; - for (WrapperElement profile : profiles) { - String ref = profile.getAttribute("value"); - String p = stack.addToLiteralPath("meta", "profile", ":"+Integer.toString(i)); - if (rule(errors, IssueType.INVALID, element.line(), element.col(), p, !Utilities.noString(ref), "StructureDefinition reference invalid")) { - StructureDefinition pr = context.fetchResource(StructureDefinition.class, ref); - if (warning(errors, IssueType.INVALID, element.line(), element.col(), p, pr != null, "StructureDefinition reference \"{0}\" could not be resolved", ref)) { - if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), p, pr.hasSnapshot(), "StructureDefinition has no snapshot - validation is against the snapshot, so it must be provided")) { - validateElement(errors, pr, pr.getSnapshot().getElement().get(0), null, null, element, element.getName(), stack); - } - } - i++; - } - } - } + @Override + public void validate(List errors, JsonObject object, StructureDefinition profile) throws Exception { + validateResource(errors, new JsonWrapperElement(object), profile, requiresResourceId, null); } - - private void validateBundle(List errors, WrapperElement bundle, NodeStack stack) { + + private void validateBundle(List errors, WrapperElement bundle, NodeStack stack) { List entries = new ArrayList(); bundle.getNamedChildren("entry", entries); String type = bundle.getNamedChildValue("type"); @@ -939,1495 +1423,1044 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } } - private void validateMessage(List errors, WrapperElement bundle) { - // TODO Auto-generated method stub - - } - - - private void validateDocument(List errors, List entries, WrapperElement composition, NodeStack stack, String fullUrl, String id) { - // first entry must be a composition - if (rule(errors, IssueType.INVALID, composition.line(), composition.col(), stack.getLiteralPath(), composition.getResourceType().equals("Composition"), "The first entry in a document must be a composition")) { - // the composition subject and section references must resolve in the bundle - validateBundleReference(errors, entries, composition.getNamedChild("subject"), "Composition Subject", stack.push(composition.getNamedChild("subject"), -1, null, null), fullUrl, "Composition", id); - validateSections(errors, entries, composition, stack, fullUrl, id); - } - } -//rule(errors, IssueType.INVALID, bundle.line(), bundle.col(), "Bundle", !"urn:guid:".equals(base), "The base 'urn:guid:' is not valid (use urn:uuid:)"); -//rule(errors, IssueType.INVALID, entry.line(), entry.col(), localStack.getLiteralPath(), !"urn:guid:".equals(ebase), "The base 'urn:guid:' is not valid"); -//rule(errors, IssueType.INVALID, entry.line(), entry.col(), localStack.getLiteralPath(), !Utilities.noString(base) || !Utilities.noString(ebase), "entry does not have a base"); -//String firstBase = null; -//firstBase = ebase == null ? base : ebase; - - private void validateSections(List errors, List entries, WrapperElement focus, NodeStack stack, String fullUrl, String id) { - List sections = new ArrayList(); - focus.getNamedChildren("entry", sections); - int i = 0; - for (WrapperElement section : sections) { - NodeStack localStack = stack.push(section, 1, null, null); - validateBundleReference(errors, entries, section.getNamedChild("content"), "Section Content", localStack, fullUrl, "Composition", id); - validateSections(errors, entries, section, localStack, fullUrl, id); - i++; - } - } - + // public class ProfileStructureIterator { + // + // private StructureDefinition profile; + // private ElementDefinition elementDefn; + // private List names = new ArrayList(); + // private Map> children = new HashMap>(); + // private int cursor; + // + // public ProfileStructureIterator(StructureDefinition profile, ElementDefinition elementDefn) { + // this.profile = profile; + // this.elementDefn = elementDefn; + // loadMap(); + // cursor = -1; + // } + // + // private void loadMap() { + // int i = profile.getSnapshot().getElement().indexOf(elementDefn) + 1; + // String lead = elementDefn.getPath(); + // while (i < profile.getSnapshot().getElement().size()) { + // String name = profile.getSnapshot().getElement().get(i).getPath(); + // if (name.length() <= lead.length()) + // return; // cause we've got to the end of the possible matches + // String tail = name.substring(lead.length()+1); + // if (Utilities.isToken(tail) && name.substring(0, lead.length()).equals(lead)) { + // List list = children.get(tail); + // if (list == null) { + // list = new ArrayList(); + // names.add(tail); + // children.put(tail, list); + // } + // list.add(profile.getSnapshot().getElement().get(i)); + // } + // i++; + // } + // } + // + // public boolean more() { + // cursor++; + // return cursor < names.size(); + // } + // + // public List current() { + // return children.get(name()); + // } + // + // public String name() { + // return names.get(cursor); + // } + // + // } + // + // private void checkByProfile(List errors, String path, WrapperElement focus, StructureDefinition profile, ElementDefinition elementDefn) + // throws Exception { + // // we have an element, and the structure that describes it. + // // we know that's it's valid against the underlying spec - is it valid against this one? + // // in the instance validator above, we assume that schema or schmeatron has taken care of cardinalities, but here, we have no such reliance. + // // so the walking algorithm is different: we're going to walk the definitions + // String type; + // if (elementDefn.getPath().endsWith("[x]")) { + // String tail = elementDefn.getPath().substring(elementDefn.getPath().lastIndexOf(".")+1, elementDefn.getPath().length()-3); + // type = focus.getName().substring(tail.length()); + // rule(errors, IssueType.STRUCTURE, focus.line(), focus.col(), path, typeAllowed(type, elementDefn.getType()), "The type '"+type+"' is not allowed at this + // point (must be one of '"+typeSummary(elementDefn)+")"); + // } else { + // if (elementDefn.getType().size() == 1) { + // type = elementDefn.getType().size() == 0 ? null : elementDefn.getType().get(0).getCode(); + // } else + // type = null; + // } + // // constraints: + // for (ElementDefinitionConstraintComponent c : elementDefn.getConstraint()) + // checkConstraint(errors, path, focus, c); + // if (elementDefn.hasBinding() && type != null) + // checkBinding(errors, path, focus, profile, elementDefn, type); + // + // // type specific checking: + // if (type != null && isPrimitiveType(type)) { + // checkPrimitiveByProfile(errors, path, focus, elementDefn); + // } else { + // if (elementDefn.hasFixed()) + // checkFixedValue(errors, path, focus, elementDefn.getFixed(), ""); + // + // ProfileStructureIterator walker = new ProfileStructureIterator(profile, elementDefn); + // while (walker.more()) { + // // collect all the slices for the path + // List childset = walker.current(); + // // collect all the elements that match it by name + // List children = new ArrayList(); + // focus.getNamedChildrenWithWildcard(walker.name(), children); + // + // if (children.size() == 0) { + // // well, there's no children - should there be? + // for (ElementDefinition defn : childset) { + // if (!rule(errors, IssueType.REQUIRED, focus.line(), focus.col(), path, defn.getMin() == 0, "Required Element '"+walker.name()+"' missing")) + // break; // no point complaining about missing ones after the first one + // } + // } else if (childset.size() == 1) { + // // simple case: one possible definition, and one or more children. + // rule(errors, IssueType.STRUCTURE, focus.line(), focus.col(), path, childset.get(0).getMax().equals("*") || Integer.parseInt(childset.get(0).getMax()) >= + // children.size(), + // "Too many elements for '"+walker.name()+"'"); // todo: sort out structure + // for (WrapperElement child : children) { + // checkByProfile(errors, childset.get(0).getPath(), child, profile, childset.get(0)); + // } + // } else { + // // ok, this is the full case - we have a list of definitions, and a list of candidates for meeting those definitions. + // // we need to decide *if* that match a given definition + // } + // } + // } + // } + + // private void checkBinding(List errors, String path, WrapperElement focus, StructureDefinition profile, ElementDefinition elementDefn, + // String type) throws EOperationOutcome, Exception { + // ElementDefinitionBindingComponent bc = elementDefn.getBinding(); + // + // if (bc != null && bc.hasValueSet() && bc.getValueSet() instanceof Reference) { + // String url = ((Reference) bc.getValueSet()).getReference(); + // ValueSet vs = resolveValueSetReference(profile, (Reference) bc.getValueSet()); + // if (vs == null) { + // rule(errors, IssueType.STRUCTURE, focus.line(), focus.col(), path, false, "Cannot check binding on type '"+type+"' as the value set '"+url+"' could not be + // located"); + // } else if (type.equals("code")) + // checkBindingCode(errors, path, focus, vs); + // else if (type.equals("Coding")) + // checkBindingCoding(errors, path, focus, vs); + // else if (type.equals("CodeableConcept")) + // checkBindingCodeableConcept(errors, path, focus, vs); + // else + // rule(errors, IssueType.STRUCTURE, focus.line(), focus.col(), path, false, "Cannot check binding on type '"+type+"'"); + // } + // } + // + // private ValueSet resolveValueSetReference(StructureDefinition profile, Reference reference) throws EOperationOutcome, Exception { + // if (reference.getReference().startsWith("#")) { + // for (Resource r : profile.getContained()) { + // if (r instanceof ValueSet && r.getId().equals(reference.getReference().substring(1))) + // return (ValueSet) r; + // } + // return null; + // } else + // return resolveBindingReference(reference); + // + // } + // + // private void checkBindingCode(List errors, String path, WrapperElement focus, ValueSet vs) { + // // rule(errors, "exception", path, false, "checkBindingCode not done yet"); + // } + // + // private void checkBindingCoding(List errors, String path, WrapperElement focus, ValueSet vs) { + // // rule(errors, "exception", path, false, "checkBindingCoding not done yet"); + // } + // + // private void checkBindingCodeableConcept(List errors, String path, WrapperElement focus, ValueSet vs) { + // // rule(errors, "exception", path, false, "checkBindingCodeableConcept not done yet"); + // } + // + // private String typeSummary(ElementDefinition elementDefn) { + // StringBuilder b = new StringBuilder(); + // for (TypeRefComponent t : elementDefn.getType()) { + // b.append("|"+t.getCode()); + // } + // return b.toString().substring(1); + // } + // + // private boolean typeAllowed(String t, List types) { + // for (TypeRefComponent type : types) { + // if (t.equals(Utilities.capitalize(type.getCode()))) + // return true; + // if (t.equals("Resource") && Utilities.capitalize(type.getCode()).equals("Reference")) + // return true; + // } + // return false; + // } + // + // private void checkConstraint(List errors, String path, WrapperElement focus, ElementDefinitionConstraintComponent c) throws Exception { + // + //// try + //// { + //// XPathFactory xpf = new net.sf.saxon.xpath.XPathFactoryImpl(); + //// NamespaceContext context = new NamespaceContextMap("f", "http://hl7.org/fhir", "h", "http://www.w3.org/1999/xhtml"); + //// + //// XPath xpath = xpf.newXPath(); + //// xpath.setNamespaceContext(context); + //// Boolean ok = (Boolean) xpath.evaluate(c.getXpath(), focus, XPathConstants.BOOLEAN); + //// if (ok == null || !ok) { + //// if (c.getSeverity() == ConstraintSeverity.warning) + //// warning(errors, "invariant", path, false, c.getHuman()); + //// else + //// rule(errors, "invariant", path, false, c.getHuman()); + //// } + //// } + //// catch (XPathExpressionException e) { + //// rule(errors, "invariant", path, false, "error executing invariant: "+e.getMessage()); + //// } + // } + // + // private void checkPrimitiveByProfile(List errors, String path, WrapperElement focus, ElementDefinition elementDefn) { + // // two things to check - length, and fixed value + // String value = focus.getAttribute("value"); + // if (elementDefn.hasMaxLengthElement()) { + // rule(errors, IssueType.TOOLONG, focus.line(), focus.col(), path, value.length() <= elementDefn.getMaxLength(), "The value '"+value+"' exceeds the allow + // length limit of "+Integer.toString(elementDefn.getMaxLength())); + // } + // if (elementDefn.hasFixed()) { + // checkFixedValue(errors, path, focus, elementDefn.getFixed(), ""); + // } + // } + private void validateBundleReference(List errors, List entries, WrapperElement ref, String name, NodeStack stack, String fullUrl, String type, String id) { if (ref != null && !Utilities.noString(ref.getNamedChildValue("reference"))) { WrapperElement target = resolveInBundle(entries, ref.getNamedChildValue("reference"), fullUrl, type, id); - rule(errors, IssueType.INVALID, target.line(), target.col(), stack.addToLiteralPath("reference"), target != null, "Unable to resolve the target of the reference in the bundle ("+name+")"); + rule(errors, IssueType.INVALID, target.line(), target.col(), stack.addToLiteralPath("reference"), target != null, "Unable to resolve the target of the reference in the bundle (" + name + ")"); } } - - private WrapperElement resolveInBundle(List entries, String ref, String fullUrl, String type, String id) { - if (Utilities.isAbsoluteUrl(ref)) { - // if the reference is absolute, then you resolve by fullUrl. No other thinking is required. - for (WrapperElement entry : entries) { - String fu = entry.getNamedChildValue("fullUrl"); - if (ref.equals(fu)) - return entry; - } - return null; - } else { - // split into base, type, and id - String u = null; - if (fullUrl != null && fullUrl.endsWith(type+"/"+id)) - // fullUrl = complex - u = fullUrl.substring((type+"/"+id).length())+ref; - String[] parts = ref.split("\\/"); - if (parts.length >= 2) { - String t = parts[0]; - String i = parts[1]; - for (WrapperElement entry : entries) { - String fu = entry.getNamedChildValue("fullUrl"); - if (u != null && fullUrl.equals(u)) - return entry; - if (u == null) { - WrapperElement res = entry.getNamedChild("resource"); - WrapperElement resource = res.getFirstChild(); - String et = resource.getResourceType(); - String eid = resource.getNamedChildValue("id"); - if (t.equals(et) && i.equals(eid)) - return entry; - } - } - } - return null; - } - } - - private StructureDefinition getProfileForType(String type) throws Exception { - return context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+type); - } - private void validateObservation(List errors, WrapperElement element, NodeStack stack) { - // all observations should have a subject, a performer, and a time - - bpCheck(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), element.getNamedChild("subject") != null, "All observations should have a subject"); - bpCheck(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), element.getNamedChild("performer") != null, "All observations should have a performer"); - bpCheck(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), element.getNamedChild("effectiveDateTime") != null || element.getNamedChild("effectivePeriod") != null , "All observations should have an effectiveDateTime or an effectivePeriod"); + private void validateContains(List errors, String path, ElementDefinition child, ElementDefinition context, WrapperElement element, NodeStack stack, boolean needsId) + throws Exception { + WrapperElement e = element.isXml() ? element.getFirstChild() : element; + String resourceName = e.getResourceType(); + StructureDefinition profile = this.context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + resourceName); + if (rule(errors, IssueType.INVALID, element.line(), element.col(), stack.addToLiteralPath(resourceName), profile != null, "No profile found for contained resource of type '" + resourceName + "'")) + validateResource(errors, e, profile, needsId, stack); } - private void bpCheck(List errors, IssueType invalid, int line, int col, String literalPath, boolean test, String message) { - if (bpWarnings != null) { - switch (bpWarnings) { - case Error: rule(errors, invalid, line, col, literalPath, test, message); - case Warning: warning(errors, invalid, line, col, literalPath, test, message); - case Hint: hint(errors, invalid, line, col, literalPath, test, message); - default: // do nothing + private void validateDocument(List errors, List entries, WrapperElement composition, NodeStack stack, String fullUrl, String id) { + // first entry must be a composition + if (rule(errors, IssueType.INVALID, composition.line(), composition.col(), stack.getLiteralPath(), composition.getResourceType().equals("Composition"), + "The first entry in a document must be a composition")) { + // the composition subject and section references must resolve in the bundle + validateBundleReference(errors, entries, composition.getNamedChild("subject"), "Composition Subject", stack.push(composition.getNamedChild("subject"), -1, null, null), fullUrl, "Composition", + id); + validateSections(errors, entries, composition, stack, fullUrl, id); } } - } - - private void validateElement(List errors, StructureDefinition profile, ElementDefinition definition, StructureDefinition cprofile, ElementDefinition context, WrapperElement element, String actualType, NodeStack stack) throws Exception { + // rule(errors, IssueType.INVALID, bundle.line(), bundle.col(), "Bundle", !"urn:guid:".equals(base), "The base 'urn:guid:' is not valid (use urn:uuid:)"); + // rule(errors, IssueType.INVALID, entry.line(), entry.col(), localStack.getLiteralPath(), !"urn:guid:".equals(ebase), "The base 'urn:guid:' is not valid"); + // rule(errors, IssueType.INVALID, entry.line(), entry.col(), localStack.getLiteralPath(), !Utilities.noString(base) || !Utilities.noString(ebase), "entry + // does not have a base"); + // String firstBase = null; + // firstBase = ebase == null ? base : ebase; + + private void validateElement(List errors, StructureDefinition profile, ElementDefinition definition, StructureDefinition cprofile, ElementDefinition context, + WrapperElement element, String actualType, NodeStack stack) throws Exception { // irrespective of what element it is, it cannot be empty - if (element.isXml()) { - rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), FormatUtilities.FHIR_NS.equals(element.getNamespace()), "Namespace mismatch - expected '"+FormatUtilities.FHIR_NS+"', found '"+element.getNamespace()+"'"); - rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), !element.hasNamespace("http://www.w3.org/2001/XMLSchema-instance"), "Schema Instance Namespace is not allowed in instances"); + if (element.isXml()) { + rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), FormatUtilities.FHIR_NS.equals(element.getNamespace()), + "Namespace mismatch - expected '" + FormatUtilities.FHIR_NS + "', found '" + element.getNamespace() + "'"); + rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), !element.hasNamespace("http://www.w3.org/2001/XMLSchema-instance"), + "Schema Instance Namespace is not allowed in instances"); rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), !element.hasProcessingInstruction(), "No Processing Instructions in resources"); - } + } rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), !empty(element), "Elements must have some content (@value, extensions, or children elements)"); - + // get the list of direct defined children, including slices List childDefinitions = ProfileUtilities.getChildMap(profile, definition.getName(), definition.getPath(), definition.getNameReference()); // 1. List the children, and remember their exact path (convenience) List children = new ArrayList(); ChildIterator iter = new ChildIterator(stack.getLiteralPath(), element); - while (iter.next()) - children.add(new ElementInfo(iter.name(), iter.element(), iter.path(), iter.count())); - + while (iter.next()) + children.add(new ElementInfo(iter.name(), iter.element(), iter.path(), iter.count())); + // 2. assign children to a definition - // for each definition, for each child, check whether it belongs in the slice + // for each definition, for each child, check whether it belongs in the slice ElementDefinition slice = null; for (ElementDefinition ed : childDefinitions) { - boolean process = true; - // where are we with slicing - if (ed.hasSlicing()) { - if (slice != null && slice.getPath().equals(ed.getPath())) - throw new Exception("Slice encountered midway through path on "+slice.getPath()); - slice = ed; - process = false; - } else if (slice != null && !slice.getPath().equals(ed.getPath())) - slice = null; + boolean process = true; + // where are we with slicing + if (ed.hasSlicing()) { + if (slice != null && slice.getPath().equals(ed.getPath())) + throw new Exception("Slice encountered midway through path on " + slice.getPath()); + slice = ed; + process = false; + } else if (slice != null && !slice.getPath().equals(ed.getPath())) + slice = null; - if (process) { - for (ElementInfo ei : children) { - boolean match = false; - if (slice == null) { - match = nameMatches(ei.name, tail(ed.getPath())); - } else { - if (nameMatches(ei.name, tail(ed.getPath()))) - match = sliceMatches(ei.element, ei.path, slice, ed, profile); - } - if (match) { - if (rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.path, ei.definition == null, "Element matches more than one slice")) - ei.definition = ed; - } - } + if (process) { + for (ElementInfo ei : children) { + boolean match = false; + if (slice == null) { + match = nameMatches(ei.name, tail(ed.getPath())); + } else { + if (nameMatches(ei.name, tail(ed.getPath()))) + match = sliceMatches(ei.element, ei.path, slice, ed, profile); + } + if (match) { + if (rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.path, ei.definition == null, "Element matches more than one slice")) + ei.definition = ed; + } + } + } } - } - for (ElementInfo ei : children) - if (ei.path.endsWith(".extension")) - rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.path, ei.definition != null, "Element is unknown or does not match any slice (url=\""+ei.element.getAttribute("url")+"\")"); - else - rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.path, (ei.definition != null) || (!ei.element.isXml() && ei.element.getName().equals("fhir_comments")), "Element is unknown or does not match any slice"); - + for (ElementInfo ei : children) + if (ei.path.endsWith(".extension")) + rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.path, ei.definition != null, "Element is unknown or does not match any slice (url=\"" + ei.element.getAttribute("url") + "\")"); + else + rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.path, (ei.definition != null) || (!ei.element.isXml() && ei.element.getName().equals("fhir_comments")), + "Element is unknown or does not match any slice"); + // 3. report any definitions that have a cardinality problem for (ElementDefinition ed : childDefinitions) { - if (ed.getRepresentation().isEmpty()) { // ignore xml attributes - int count = 0; - for (ElementInfo ei : children) - if (ei.definition == ed) - count++; - if (ed.getMin() > 0) { - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), count >= ed.getMin(), "Element '"+stack.getLiteralPath()+"."+tail(ed.getPath())+"': minimum required = "+Integer.toString(ed.getMin())+", but only found "+Integer.toString(count)); - } - if (ed.hasMax() && !ed.getMax().equals("*")) { - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), count <= Integer.parseInt(ed.getMax()), "Element "+tail(ed.getPath())+" @ "+stack.getLiteralPath()+": max allowed = "+Integer.toString(ed.getMin())+", but found "+Integer.toString(count)); - } - - } + if (ed.getRepresentation().isEmpty()) { // ignore xml attributes + int count = 0; + for (ElementInfo ei : children) + if (ei.definition == ed) + count++; + if (ed.getMin() > 0) { + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), count >= ed.getMin(), + "Element '" + stack.getLiteralPath() + "." + tail(ed.getPath()) + "': minimum required = " + Integer.toString(ed.getMin()) + ", but only found " + Integer.toString(count)); + } + if (ed.hasMax() && !ed.getMax().equals("*")) { + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), count <= Integer.parseInt(ed.getMax()), + "Element " + tail(ed.getPath()) + " @ " + stack.getLiteralPath() + ": max allowed = " + ed.getMax() + ", but found " + Integer.toString(count)); + } + + } } // 4. check order if any slices are orderd. (todo) // 5. inspect each child for validity for (ElementInfo ei : children) { - if (ei.definition != null) { - String type = null; - ElementDefinition typeDefn = null; - if (ei.definition.getType().size() == 1 && !ei.definition.getType().get(0).getCode().equals("*") && !ei.definition.getType().get(0).getCode().equals("Element") && !ei.definition.getType().get(0).getCode().equals("BackboneElement") ) - type = ei.definition.getType().get(0).getCode(); - else if (ei.definition.getType().size() == 1 && ei.definition.getType().get(0).getCode().equals("*")) { + if (ei.definition != null) { + String type = null; + ElementDefinition typeDefn = null; + if (ei.definition.getType().size() == 1 && !ei.definition.getType().get(0).getCode().equals("*") && !ei.definition.getType().get(0).getCode().equals("Element") + && !ei.definition.getType().get(0).getCode().equals("BackboneElement")) + type = ei.definition.getType().get(0).getCode(); + else if (ei.definition.getType().size() == 1 && ei.definition.getType().get(0).getCode().equals("*")) { String prefix = tail(ei.definition.getPath()); assert prefix.endsWith("[x]"); - type = ei.name.substring(prefix.length()-3); + type = ei.name.substring(prefix.length() - 3); if (isPrimitiveType(type)) type = Utilities.uncapitalize(type); - } else if (ei.definition.getType().size() > 1) { + } else if (ei.definition.getType().size() > 1) { - String prefix = tail(ei.definition.getPath()); - assert typesAreAllReference(ei.definition.getType()) || prefix.endsWith("[x]") : prefix; - - prefix = prefix.substring(0, prefix.length()-3); - for (TypeRefComponent t : ei.definition.getType()) - if ((prefix+Utilities.capitalize(t.getCode())).equals(ei.name)) - type = t.getCode(); - if (type == null) { - TypeRefComponent trc = ei.definition.getType().get(0); - if(trc.getCode().equals("Reference")) - type = "Reference"; - else - rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), stack.getLiteralPath(), false, "The element "+ei.name+" is illegal. Valid types at this point are "+describeTypes(ei.definition.getType())); + String prefix = tail(ei.definition.getPath()); + assert typesAreAllReference(ei.definition.getType()) || prefix.endsWith("[x]") : prefix; + + prefix = prefix.substring(0, prefix.length() - 3); + for (TypeRefComponent t : ei.definition.getType()) + if ((prefix + Utilities.capitalize(t.getCode())).equals(ei.name)) + type = t.getCode(); + if (type == null) { + TypeRefComponent trc = ei.definition.getType().get(0); + if (trc.getCode().equals("Reference")) + type = "Reference"; + else + rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), stack.getLiteralPath(), false, + "The element " + ei.name + " is illegal. Valid types at this point are " + describeTypes(ei.definition.getType())); } - } else if (ei.definition.getNameReference() != null) { - typeDefn = resolveNameReference(profile.getSnapshot(), ei.definition.getNameReference()); + } else if (ei.definition.getNameReference() != null) { + typeDefn = resolveNameReference(profile.getSnapshot(), ei.definition.getNameReference()); } - if (type != null) { if (type.startsWith("@")) { - ei.definition = findElement(profile, type.substring(1)); + ei.definition = findElement(profile, type.substring(1)); type = null; } - } - NodeStack localStack = stack.push(ei.element, ei.count, ei.definition, type == null ? typeDefn : resolveType(type)); - String localStackLiterapPath = localStack.getLiteralPath(); + } + NodeStack localStack = stack.push(ei.element, ei.count, ei.definition, type == null ? typeDefn : resolveType(type)); + String localStackLiterapPath = localStack.getLiteralPath(); String eiPath = ei.path; assert(eiPath.equals(localStackLiterapPath)) : "ei.path: " + ei.path + " - localStack.getLiterapPath: " + localStackLiterapPath; - if (type != null) { - if (typeIsPrimitive(type)) - checkPrimitive(errors, ei.path, type, ei.definition, ei.element); - else { - if (type.equals("Identifier")) - checkIdentifier(errors, ei.path, ei.element, ei.definition); - else if (type.equals("Coding")) - checkCoding(errors, ei.path, ei.element, profile, ei.definition); - else if (type.equals("CodeableConcept")) - checkCodeableConcept(errors, ei.path, ei.element, profile, ei.definition); - else if (type.equals("Reference")) - checkReference(errors, ei.path, ei.element, profile, ei.definition, actualType, localStack); - - if (type.equals("Extension")) - checkExtension(errors, ei.path, ei.element, ei.definition, profile, localStack); - else if (type.equals("Resource")) - validateContains(errors, ei.path, ei.definition, definition, ei.element, localStack, !isBundleEntry(ei.path)); // if (str.matches(".*([.,/])work\\1$")) + if (type != null) { + if (isPrimitiveType(type)) + checkPrimitive(errors, ei.path, type, ei.definition, ei.element); else { - StructureDefinition p = getProfileForType(type); - if (rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.path, p != null, "Unknown type "+type)) { - validateElement(errors, p, p.getSnapshot().getElement().get(0), profile, ei.definition, ei.element, type, localStack); + if (type.equals("Identifier")) + checkIdentifier(errors, ei.path, ei.element, ei.definition); + else if (type.equals("Coding")) + checkCoding(errors, ei.path, ei.element, profile, ei.definition); + else if (type.equals("CodeableConcept")) + checkCodeableConcept(errors, ei.path, ei.element, profile, ei.definition); + else if (type.equals("Reference")) + checkReference(errors, ei.path, ei.element, profile, ei.definition, actualType, localStack); + + if (type.equals("Extension")) + checkExtension(errors, ei.path, ei.element, ei.definition, profile, localStack); + else if (type.equals("Resource")) + validateContains(errors, ei.path, ei.definition, definition, ei.element, localStack, !isBundleEntry(ei.path)); // if + // (str.matches(".*([.,/])work\\1$")) + else { + StructureDefinition p = getProfileForType(type); + if (rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.path, p != null, "Unknown type " + type)) { + validateElement(errors, p, p.getSnapshot().getElement().get(0), profile, ei.definition, ei.element, type, localStack); + } } } + } else { + if (rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), stack.getLiteralPath(), ei.definition != null, "Unrecognised Content " + ei.name)) + validateElement(errors, profile, ei.definition, null, null, ei.element, type, localStack); } - } else { - if (rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), stack.getLiteralPath(), ei.definition != null, "Unrecognised Content "+ei.name)) - validateElement(errors, profile, ei.definition, null, null, ei.element, type, localStack); - } } } } - - private boolean typesAreAllReference(List theType) { - for (TypeRefComponent typeRefComponent : theType) { - if (typeRefComponent.getCode().equals("Reference") == false) { - return false; - } - } - return true; + + private void validateMessage(List errors, WrapperElement bundle) { + // TODO Auto-generated method stub + } - - /** - * - * @param element - the candidate that might be in the slice - * @param path - for reporting any errors. the XPath for the element - * @param slice - the definition of how slicing is determined - * @param ed - the slice for which to test membership - * @return - * @throws Exception + + private void validateObservation(List errors, WrapperElement element, NodeStack stack) { + // all observations should have a subject, a performer, and a time + + bpCheck(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), element.getNamedChild("subject") != null, "All observations should have a subject"); + bpCheck(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), element.getNamedChild("performer") != null, "All observations should have a performer"); + bpCheck(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), element.getNamedChild("effectiveDateTime") != null || element.getNamedChild("effectivePeriod") != null, + "All observations should have an effectiveDateTime or an effectivePeriod"); + } + + /* + * The actual base entry point */ - private boolean sliceMatches(WrapperElement element, String path, ElementDefinition slice, ElementDefinition ed, StructureDefinition profile) throws Exception { - if (!slice.getSlicing().hasDiscriminator()) - return false; // cannot validate in this case - for (StringType s : slice.getSlicing().getDiscriminator()) { - String discriminator = s.getValue(); - ElementDefinition criteria = getCriteriaForDiscriminator(path, ed, discriminator, profile); - if (discriminator.equals("url") && criteria.getPath().equals("Extension.url")) { - if (!element.getAttribute("url").equals(((UriType) criteria.getFixed()).asStringValue())) - return false; - } else { - Element value = getValueForDiscriminator(element, discriminator, criteria); - if (!valueMatchesCriteria(value, criteria)) - return false; - } - } - return true; - } - - private boolean valueMatchesCriteria(Element value, ElementDefinition criteria) { - throw new Error("validation of slices not done yet"); - } - - private Element getValueForDiscriminator(WrapperElement element, String discriminator, ElementDefinition criteria) { - throw new Error("validation of slices not done yet"); - } - - private ElementDefinition getCriteriaForDiscriminator(String path, ElementDefinition ed, String discriminator, StructureDefinition profile) throws Exception { - List childDefinitions = ProfileUtilities.getChildMap(profile, ed); - List snapshot = null; - if (childDefinitions.isEmpty()) { - // going to look at the type - if (ed.getType().size() == 0) - throw new Exception("Error in profile for "+path+" no children, no type"); - if (ed.getType().size() > 1) - throw new Exception("Error in profile for "+path+" multiple types defined in slice discriminator"); - StructureDefinition type; - if (ed.getType().get(0).hasProfile()) - type = context.fetchResource(StructureDefinition.class, ed.getType().get(0).getProfile().get(0).getValue()); - else - type = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+ed.getType().get(0).getCode()); - snapshot = type.getSnapshot().getElement(); - ed = snapshot.get(0); - } else { - snapshot = profile.getSnapshot().getElement(); - } - String originalPath = ed.getPath(); - String goal = originalPath+"."+discriminator; + private void validateResource(List errors, WrapperElement element, StructureDefinition profile, boolean needsId, NodeStack stack) throws Exception { + if (stack == null) + stack = new NodeStack(element.isXml()); - int index = snapshot.indexOf(ed); - assert (index > -1); - index++; - while (index < snapshot.size() && !snapshot.get(index).getPath().equals(originalPath)) { - if (snapshot.get(index).getPath().equals(goal)) - return snapshot.get(index); - index++; - } - throw new Error("Unable to find discriminator definition for "+goal+" in "+discriminator+" at "+path); - } - - private boolean isPrimitiveType(String type) { - return - type.equalsIgnoreCase("boolean") || type.equalsIgnoreCase("integer") || type.equalsIgnoreCase("string") || type.equalsIgnoreCase("decimal") || - type.equalsIgnoreCase("uri") || type.equalsIgnoreCase("base64Binary") || type.equalsIgnoreCase("instant") || type.equalsIgnoreCase("date") || - type.equalsIgnoreCase("dateTime") || type.equalsIgnoreCase("time") || type.equalsIgnoreCase("code") || type.equalsIgnoreCase("oid") || type.equalsIgnoreCase("id"); - } - - private boolean nameMatches(String name, String tail) { - if (tail.endsWith("[x]")) - return name.startsWith(tail.substring(0, tail.length()-3)); - else - return (name.equals(tail)); - } - - private ElementDefinition resolveNameReference(StructureDefinitionSnapshotComponent snapshot, String name) { - for (ElementDefinition ed : snapshot.getElement()) - if (name.equals(ed.getName())) - return ed; - return null; - } - - private ElementDefinition resolveType(String type) throws EOperationOutcome, Exception { - String url = "http://hl7.org/fhir/StructureDefinition/"+type; - StructureDefinition sd = context.fetchResource(StructureDefinition.class, url); - if (sd == null || !sd.hasSnapshot()) - return null; - else - return sd.getSnapshot().getElement().get(0); - } - -// private String mergePath(String path1, String path2) { -// // path1 is xpath path -// // path2 is dotted path -// String[] parts = path2.split("\\."); -// StringBuilder b = new StringBuilder(path1); -// for (int i = 1; i < parts.length -1; i++) -// b.append("/f:"+parts[i]); -// return b.toString(); -// } - - private boolean isBundleEntry(String path) { - String[] parts = path.split("\\/"); - if (path.startsWith("/f:")) - return parts.length > 2 && parts[parts.length-1].startsWith("f:resource") && (parts[parts.length-2].equals("f:entry") || parts[parts.length-2].startsWith("f:entry[")); - else - return parts.length > 2 && parts[parts.length-1].equals("resource") && ((parts.length > 2 && parts[parts.length-3].equals("entry")) || parts[parts.length-2].equals("entry")); - } - - private String describeTypes(List types) { - CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); - for (TypeRefComponent t : types) { - b.append(t.getCode()); + // getting going - either we got a profile, or not. + boolean ok = true; + if (element.isXml()) { + ok = rule(errors, IssueType.INVALID, element.line(), element.col(), "/", element.getNamespace().equals(FormatUtilities.FHIR_NS), + "Namespace mismatch - expected '" + FormatUtilities.FHIR_NS + "', found '" + element.getNamespace() + "'"); } - return b.toString(); - } - - private void checkReference(List errors, String path, WrapperElement element, StructureDefinition profile, ElementDefinition container, String parentType, NodeStack stack) throws Exception { - String ref = element.getNamedChildValue("reference"); - if (Utilities.noString(ref)) { - // todo - what should we do in this case? - hint(errors, IssueType.STRUCTURE, element.line(), element.col(), path, !Utilities.noString(element.getNamedChildValue("display")), "A Reference without an actual reference should have a display"); - return; - } - - WrapperElement we = resolve(ref, stack); - String ft; - if (we != null) - ft = we.getResourceType(); - else - ft = tryParse(ref); - if (hint(errors, IssueType.STRUCTURE, element.line(), element.col(), path, ft != null, "Unable to determine type of target resource")) { - boolean ok = false; - CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); - for (TypeRefComponent type : container.getType()) { - if (!ok && type.getCode().equals("Reference")) { - // we validate as much as we can. First, can we infer a type from the profile? - if (!type.hasProfile() || type.getProfile().get(0).getValue().equals("http://hl7.org/fhir/StructureDefinition/Resource")) - ok = true; - else { - String pr = type.getProfile().get(0).getValue(); - - String bt = getBaseType(profile, pr); - if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, bt != null, "Unable to resolve the profile reference '"+pr+"'")) { - b.append(bt); - ok = bt.equals(ft); - } else - ok = true; // suppress following check - } - } - if (!ok && type.getCode().equals("*")) { - ok = true; // can refer to anything - } - } - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, ok, "Invalid Resource target type. Found "+ft+", but expected one of ("+b.toString()+")"); - } - } - - private WrapperElement resolve(String ref, NodeStack stack) { - if (ref.startsWith("#")) { - // work back through the contained list. - // really, there should only be one level for this (contained resources cannot contain - // contained resources), but we'll leave that to some other code to worry about - while (stack != null && stack.getElement() != null) { - WrapperElement res = getContainedById(stack.getElement(), ref.substring(1)); - if (res != null) - return res; - stack = stack.parent; - } - return null; - } else { - // work back through the contained list - if any of them are bundles, try to resolve - // the resource in the bundle - while (stack != null && stack.getElement() != null) { - if ("Bundle".equals(stack.getElement().getResourceType())) { - WrapperElement res = getFromBundle(stack.getElement(), ref.substring(1)); - if (res != null) - return res; - } - stack = stack.parent; - } - - // todo: consult the external host for resolution - return null; - - } - } - - private WrapperElement getFromBundle(WrapperElement bundle, String ref) { - List entries = new ArrayList(); - bundle.getNamedChildren("entry", entries); - for (WrapperElement we : entries) { - WrapperElement res = we.getNamedChild("resource").getFirstChild(); - if (res != null) { - String url = genFullUrl(bundle.getNamedChildValue("base"), we.getNamedChildValue("base"), res.getName(), res.getNamedChildValue("id")); - if (url.endsWith(ref)) - return res; + if (ok) { + String resourceName = element.getResourceType(); + if (profile == null) { + profile = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + resourceName); + ok = rule(errors, IssueType.INVALID, element.line(), element.col(), stack.addToLiteralPath(resourceName), profile != null, "No profile found for resource type '" + resourceName + "'"); + } else { + String type = profile.hasConstrainedType() ? profile.getConstrainedType() : profile.getName(); + ok = rule(errors, IssueType.INVALID, -1, -1, stack.addToLiteralPath(resourceName), type.equals(resourceName), + "Specified profile type was '" + profile.getConstrainedType() + "', but resource type was '" + resourceName + "'"); } } - return null; - } - private String genFullUrl(String bundleBase, String entryBase, String type, String id) { - String base = Utilities.noString(entryBase) ? bundleBase : entryBase; - if (Utilities.noString(base)) { - return type+"/"+id; - } else if ("urn:uuid".equals(base) || "urn:oid".equals(base)) - return base+id; - else - return Utilities.appendSlash(base)+type+"/"+id; - } - - private WrapperElement getContainedById(WrapperElement container, String id) { - List contained = new ArrayList(); - container.getNamedChildren("contained", contained); - for (WrapperElement we : contained) { - WrapperElement res = we.isXml() ? we.getFirstChild() : we; - if (id.equals(res.getNamedChildValue("id"))) - return res; - } - return null; - } - - private String tryParse(String ref) throws EOperationOutcome, Exception { - String[] parts = ref.split("\\/"); - switch (parts.length) { - case 1: - return null; - case 2: - return checkResourceType(parts[0]); - default: - if (parts[parts.length-2].equals("_history")) - return checkResourceType(parts[parts.length-4]); - else - return checkResourceType(parts[parts.length-2]); + if (ok) { + stack = stack.push(element, -1, profile.getSnapshot().getElement().get(0), profile.getSnapshot().getElement().get(0)); + if (needsId && (element.getNamedChild("id") == null)) + rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), false, "Resource has no id"); + start(errors, element, profile, stack); // root is both definition and type } } - - private String checkResourceType(String type) throws EOperationOutcome, Exception { - if (context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+type) != null) - return type; - else - return null; - } - private String getBaseType(StructureDefinition profile, String pr) throws EOperationOutcome, Exception { -// if (pr.startsWith("http://hl7.org/fhir/StructureDefinition/")) { -// // this just has to be a base type -// return pr.substring(40); -// } else { - StructureDefinition p = resolveProfile(profile, pr); - if (p == null) - return null; - else if (p.getKind() == StructureDefinitionKind.RESOURCE) - return p.getSnapshot().getElement().get(0).getPath(); - else - return p.getSnapshot().getElement().get(0).getType().get(0).getCode(); -// } - } - - private StructureDefinition resolveProfile(StructureDefinition profile, String pr) throws EOperationOutcome, Exception { - if (pr.startsWith("#")) { - for (Resource r : profile.getContained()) { - if (r.getId().equals(pr.substring(1)) && r instanceof StructureDefinition) - return (StructureDefinition) r; - } - return null; + + private void validateSections(List errors, List entries, WrapperElement focus, NodeStack stack, String fullUrl, String id) { + List sections = new ArrayList(); + focus.getNamedChildren("entry", sections); + int i = 0; + for (WrapperElement section : sections) { + NodeStack localStack = stack.push(section, 1, null, null); + validateBundleReference(errors, entries, section.getNamedChild("content"), "Section Content", localStack, fullUrl, "Composition", id); + validateSections(errors, entries, section, localStack, fullUrl, id); + i++; } - else - return context.fetchResource(StructureDefinition.class, pr); - } - - private StructureDefinition checkExtension(List errors, String path, WrapperElement element, ElementDefinition def, StructureDefinition profile, NodeStack stack) throws Exception { - String url = element.getAttribute("url"); - boolean isModifier = element.getName().equals("modifierExtension"); - - StructureDefinition ex = context.fetchResource(StructureDefinition.class, url); - if (ex == null) { - if (!rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, allowUnknownExtension(url), "The extension "+url+" is unknown, and not allowed here")) - warning(errors, IssueType.STRUCTURE, element.line(), element.col(), path, allowUnknownExtension(url), "Unknown extension "+url); - } else { - if (def.getIsModifier()) - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path+"[url='"+url+"']", ex.getSnapshot().getElement().get(0).getIsModifier(), "Extension modifier mismatch: the extension element is labelled as a modifier, but the underlying extension is not"); - else - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path+"[url='"+url+"']", !ex.getSnapshot().getElement().get(0).getIsModifier(), "Extension modifier mismatch: the extension element is not labelled as a modifier, but the underlying extension is"); - - // two questions - // 1. can this extension be used here? - checkExtensionContext(errors, element, /*path+"[url='"+url+"']",*/ ex, stack, ex.getUrl()); - - if (isModifier) - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path+"[url='"+url+"']", ex.getSnapshot().getElement().get(0).getIsModifier(), "The Extension '"+url+"' must be used as a modifierExtension"); - else - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path+"[url='"+url+"']", !ex.getSnapshot().getElement().get(0).getIsModifier(), "The Extension '"+url+"' must not be used as an extension (it's a modifierExtension)"); - - // 2. is the content of the extension valid? - - } - return ex; } - private boolean allowUnknownExtension(String url) { - if (url.contains("example.org") || url.contains("acme.com") || url.contains("nema.org")) - return true; - for (String s : extensionDomains) - if (url.startsWith(s)) - return true; - return anyExtensionsAllowed; - } - - private boolean isKnownType(String code) throws EOperationOutcome, Exception { - return context.fetchResource(StructureDefinition.class, code.toLowerCase()) != null; + private boolean valueMatchesCriteria(Element value, ElementDefinition criteria) { + throw new Error("validation of slices not done yet"); } - private ElementDefinition getElementByPath(StructureDefinition definition, String path) { - for (ElementDefinition e : definition.getSnapshot().getElement()) { - if (e.getPath().equals(path)) - return e; - } - return null; - } - - private boolean checkExtensionContext(List errors, WrapperElement element, StructureDefinition definition, NodeStack stack, String extensionParent) { - String extUrl = definition.getUrl(); - CommaSeparatedStringBuilder p = new CommaSeparatedStringBuilder(); - for (String lp : stack.getLogicalPaths()) - p.append(lp); - if (definition.getContextType() == ExtensionContext.DATATYPE) { - boolean ok = false; - CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); - for (StringType ct : definition.getContext()) { - b.append(ct.getValue()); - if (ct.getValue().equals("*") || stack.getLogicalPaths().contains(ct.getValue()+".extension")) - ok = true; - } - return rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), ok, "The extension "+extUrl+" is not allowed to be used on the logical path set ["+p.toString()+"] (allowed: datatype="+b.toString()+")"); - } else if (definition.getContextType() == ExtensionContext.EXTENSION) { - boolean ok = false; - for (StringType ct : definition.getContext()) - if (ct.getValue().equals("*") || ct.getValue().equals(extensionParent)) - ok = true; - return rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), ok, "The extension "+extUrl+" is not allowed to be used with the extension '"+extensionParent+"'"); - } else if (definition.getContextType() == ExtensionContext.MAPPING) { - throw new Error("Not handled yet (extensionContext)"); - } else if (definition.getContextType() == ExtensionContext.RESOURCE) { - boolean ok = false; -// String simplePath = container.getPath(); -// System.out.println(simplePath); -// if (effetive.endsWith(".extension") || simplePath.endsWith(".modifierExtension")) -// simplePath = simplePath.substring(0, simplePath.lastIndexOf('.')); - CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); - for (StringType ct : definition.getContext()) { - String c = ct.getValue(); - b.append(c); - if (c.equals("*") || stack.getLogicalPaths().contains(c+".extension") || (c.startsWith("@") && stack.getLogicalPaths().contains(c.substring(1)+".extension"))); - ok = true; - } - return rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), ok, "The extension "+extUrl+" is not allowed to be used on the logical path set "+p.toString()+" (allowed: resource="+b.toString()+")"); - } else - throw new Error("Unknown context type"); - } -// -// private String simplifyPath(String path) { -// String s = path.replace("/f:", "."); -// while (s.contains("[")) -// s = s.substring(0, s.indexOf("["))+s.substring(s.indexOf("]")+1); -// String[] parts = s.split("\\."); -// int i = 0; -// while (i < parts.length && !context.getProfiles().containsKey(parts[i].toLowerCase())) -// i++; -// if (i >= parts.length) -// throw new Error("Unable to process part "+path); -// int j = parts.length - 1; -// while (j > 0 && (parts[j].equals("extension") || parts[j].equals("modifierExtension"))) -// j--; -// StringBuilder b = new StringBuilder(); -// boolean first = true; -// for (int k = i; k <= j; k++) { -// if (k == j || !parts[k].equals(parts[k+1])) { -// if (first) -// first = false; -// else -// b.append("."); -// b.append(parts[k]); -// } -// } -// return b.toString(); -// } -// - - private boolean empty(WrapperElement element) { - if (element.hasAttribute("value")) - return false; - if (element.hasAttribute("xml:id")) - return false; - WrapperElement child = element.getFirstChild(); - while (child != null) { - if (!child.isXml() || FormatUtilities.FHIR_NS.equals(child.getNamespace())) { - return false; - } - child = child.getNextSibling(); - } - return true; - } - - private ElementDefinition findElement(StructureDefinition profile, String name) { - for (ElementDefinition c : profile.getSnapshot().getElement()) { - if (c.getPath().equals(name)) { - return c; - } - } - return null; - } - - private ElementDefinition getDefinitionByTailNameChoice(List children, String name) { - for (ElementDefinition ed : children) { - String n = tail(ed.getPath()); - if (n.endsWith("[x]") && name.startsWith(n.substring(0, n.length()-3))) { - return ed; - } - } - return null; - } - - private String tail(String path) { - return path.substring(path.lastIndexOf(".")+1); - } - - private void validateContains(List errors, String path, ElementDefinition child, ElementDefinition context, WrapperElement element, NodeStack stack, boolean needsId) throws Exception { - WrapperElement e = element.isXml() ? element.getFirstChild() : element; - String resourceName = e.getResourceType(); - StructureDefinition profile = this.context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+resourceName); - if (rule(errors, IssueType.INVALID, element.line(), element.col(), stack.addToLiteralPath(resourceName), profile != null, "No profile found for contained resource of type '"+resourceName+"'")) - validateResource(errors, e, profile, needsId, stack); - } - - private boolean typeIsPrimitive(String t) { - if ("boolean".equalsIgnoreCase(t)) return true; - if ("integer".equalsIgnoreCase(t)) return true; - if ("decimal".equalsIgnoreCase(t)) return true; - if ("base64Binary".equalsIgnoreCase(t)) return true; - if ("instant".equalsIgnoreCase(t)) return true; - if ("string".equalsIgnoreCase(t)) return true; - if ("uri".equalsIgnoreCase(t)) return true; - if ("date".equalsIgnoreCase(t)) return true; - if ("dateTime".equalsIgnoreCase(t)) return true; - if ("date".equalsIgnoreCase(t)) return true; - if ("oid".equalsIgnoreCase(t)) return true; - if ("uuid".equalsIgnoreCase(t)) return true; - if ("code".equalsIgnoreCase(t)) return true; - if ("id".equalsIgnoreCase(t)) return true; - if ("xhtml".equalsIgnoreCase(t)) return true; - return false; - } - - private void checkPrimitive(List errors, String path, String type, ElementDefinition context, WrapperElement e) throws Exception { - if (type.equals("uri")) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, !e.getAttribute("value").startsWith("oid:"), "URI values cannot start with oid:"); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, !e.getAttribute("value").startsWith("uuid:"), "URI values cannot start with uuid:"); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.getAttribute("value").equals(e.getAttribute("value").trim()), "URI values cannot have leading or trailing whitespace"); - } - if (!type.equalsIgnoreCase("string") && e.hasAttribute("value")) { - if (rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.getAttribute("value").length() > 0, "@value cannot be empty")) { - warning(errors, IssueType.INVALID, e.line(), e.col(), path, e.getAttribute("value").trim().equals(e.getAttribute("value")), "value should not start or finish with whitespace"); - } - } - if (type.equals("dateTime")) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, yearIsValid(e.getAttribute("value")), "The value '"+e.getAttribute("value")+"' does not have a valid year"); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.getAttribute("value").matches("-?[0-9]{4}(-(0[1-9]|1[0-2])(-(0[0-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))?)?)?)?"), "Not a valid date time"); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, !hasTime(e.getAttribute("value")) || hasTimeZone(e.getAttribute("value")), "if a date has a time, it must have a timezone"); - - } - if (type.equals("instant")) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.getAttribute("value").matches("-?[0-9]{4}-(0[1-9]|1[0-2])-(0[0-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))"), "The instant '"+e.getAttribute("value")+"' is not valid (by regex)"); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, yearIsValid(e.getAttribute("value")), "The value '"+e.getAttribute("value")+"' does not have a valid year"); - } - - if (type.equals("code")) { - // Technically, a code is restricted to string which has at least one character and no leading or trailing whitespace, and where there is no whitespace other than single spaces in the contents - rule(errors, IssueType.INVALID, e.line(), e.col(), path, passesCodeWhitespaceRules(e.getAttribute("value")), "The code '"+e.getAttribute("value")+"' is not valid (whitespace rules)"); - } - - if (context.hasBinding()) { - checkPrimitiveBinding(errors, path, type, context, e); - } - // for nothing to check - } - - private boolean passesCodeWhitespaceRules(String v) { - if (!v.trim().equals(v)) - return false; - boolean lastWasSpace = true; - for (char c : v.toCharArray()) { - if (c == ' ') { - if (lastWasSpace) - return false; - else - lastWasSpace = true; - } else if (Character.isWhitespace(c)) - return false; - else - lastWasSpace = false; - } - return true; - } - - // note that we don't check the type here; it could be string, uri or code. - private void checkPrimitiveBinding(List errors, String path, String type, ElementDefinition context, WrapperElement element) throws Exception { - if (!element.hasAttribute("value")) - return; - - String value = element.getAttribute("value"); - -// System.out.println("check "+value+" in "+path); - - // firstly, resolve the value set - ElementDefinitionBindingComponent binding = context.getBinding(); - if (binding.hasValueSet() && binding.getValueSet() instanceof Reference) { - ValueSet vs = resolveBindingReference(binding.getValueSet()); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, vs != null, "ValueSet {0} not found", describeReference(binding.getValueSet()))) { - try { - ValueSetExpansionOutcome expansionOutcome = cache.getExpander().expand(vs); - vs = expansionOutcome != null ? expansionOutcome.getValueset() : null; - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, vs != null, "Unable to expand value set for {0}", describeReference(binding.getValueSet()))) { - boolean ok = codeInExpansion(vs, null, value); - if (binding.getStrength() == BindingStrength.REQUIRED) - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, ok, "Coded value {0} is not in value set {1} ({2})", value, describeReference(binding.getValueSet()), vs.getUrl()); - else if (binding.getStrength() == BindingStrength.EXTENSIBLE) - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, ok, "Coded value {0} is not in value set {1} ({2})", value, describeReference(binding.getValueSet()), vs.getUrl()); - else - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, ok, "Coded value {0} is not in value set {1} ({2})", value, describeReference(binding.getValueSet()), vs.getUrl()); - } - } catch (ETooCostly e) { - if (e.getMessage() == null) - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Exception opening value set "+vs.getUrl()+" for "+describeReference(binding.getValueSet())+": --Null--"); - else - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Exception opening value set "+vs.getUrl()+" for "+describeReference(binding.getValueSet())+": "+e.getMessage()); - } - } - } else - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding has no source, so can't be checked"); - } - private boolean yearIsValid(String v) { if (v == null) { - return false; + return false; } try { - int i = Integer.parseInt(v.substring(0, Math.min(4, v.length()))); - return i >= 1800 && i <= 2100; + int i = Integer.parseInt(v.substring(0, Math.min(4, v.length()))); + return i >= 1800 && i <= 2100; } catch (NumberFormatException e) { - return false; - } - } - - private boolean hasTimeZone(String fmt) { - return fmt.length() > 10 && (fmt.substring(10).contains("-") || fmt.substring(10).contains("-") || fmt.substring(10).contains("+") || fmt.substring(10).contains("Z")); - } - - private boolean hasTime(String fmt) { - return fmt.contains("T"); - } - - private void checkIdentifier(List errors, String path, WrapperElement element, ElementDefinition context) { - String system = element.getNamedChildValue("system"); - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, isAbsolute(system), "Identifier.system must be an absolute reference, not a local reference"); - } - - private boolean isAbsolute(String uri) { - return Utilities.noString(uri) || uri.startsWith("http:") || uri.startsWith("https:") || uri.startsWith("urn:uuid:") || uri.startsWith("urn:oid:") || - uri.startsWith("urn:ietf:") || uri.startsWith("urn:iso:"); - } - - private void checkIdentifier(String path, Element element, ElementDefinition context) { - - } - - private void checkQuantity(List errors, String path, WrapperElement element, ElementDefinition context, boolean b) throws Exception { - String code = element.getNamedChildValue("code"); - String system = element.getNamedChildValue("system"); - String units = element.getNamedChildValue("units"); - - if (system != null && code != null) { - checkCode(errors, element, path, code, system, units); + return false; } } + public class ChildIterator { + private String basePath; + private WrapperElement child; + private int lastCount; + private WrapperElement parent; - private void checkCoding(List errors, String path, WrapperElement element, StructureDefinition profile, ElementDefinition context) throws EOperationOutcome, Exception { - String code = element.getNamedChildValue("code"); - String system = element.getNamedChildValue("system"); - String display = element.getNamedChildValue("display"); - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, isAbsolute(system), "Coding.system must be an absolute reference, not a local reference"); - - if (system != null && code != null) { - if (checkCode(errors, element, path, code, system, display)) - if (context != null && context.getBinding() != null) { - ElementDefinitionBindingComponent binding = context.getBinding(); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, binding != null, "Binding for "+path+" missing")) { - if (binding.hasValueSet() && binding.getValueSet() instanceof Reference) { - ValueSet vs = resolveBindingReference(binding.getValueSet()); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, vs != null, "ValueSet "+describeReference(binding.getValueSet())+" not found")) { - try { - vs = cache.getExpander().expand(vs).getValueset(); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, vs != null, "Unable to expand value set for "+describeReference(binding.getValueSet()))) { - if (binding.getStrength() == BindingStrength.REQUIRED) - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, codeInExpansion(vs, system, code), "Code {"+system+"}"+code+" is not in value set "+describeReference(binding.getValueSet())+" ("+vs.getUrl()+")"); - else if (binding.getStrength() == BindingStrength.EXTENSIBLE) - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, codeInExpansion(vs, system, code), "Code {"+system+"}"+code+" is not in value set "+describeReference(binding.getValueSet())+" ("+vs.getUrl()+")"); - else - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, codeInExpansion(vs, system, code), "Code {"+system+"}"+code+" is not in value set "+describeReference(binding.getValueSet())+" ("+vs.getUrl()+")"); - } - } catch (Exception e) { - if (e.getMessage() == null) - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Exception opening value set "+vs.getUrl()+" for "+describeReference(binding.getValueSet())+": --Null--"); - else - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Exception opening value set "+vs.getUrl()+" for "+describeReference(binding.getValueSet())+": "+e.getMessage()); - } - } - } else if (binding.hasValueSet()) { - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding by URI reference cannot be checked"); - } else { - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding for path " + path + " has no source, so can't be checked"); - } - } - } - } - } - - - private ValueSet resolveBindingReference(Type reference) throws EOperationOutcome, Exception { - if (reference instanceof UriType) - return context.fetchResource(ValueSet.class, ((UriType) reference).getValue().toString()); - else if (reference instanceof Reference) - return context.fetchResource(ValueSet.class, ((Reference) reference).getReference()); - else - return null; - } - - private boolean codeInExpansion(ValueSet vs, String system, String code) { - for (ValueSetExpansionContainsComponent c : vs.getExpansion().getContains()) { - if (code.equals(c.getCode()) && (system == null || system.equals(c.getSystem()))) - return true; - if (codeinExpansion(c, system, code)) - return true; - } - return false; - } - - private boolean codeinExpansion(ValueSetExpansionContainsComponent cnt, String system, String code) { - for (ValueSetExpansionContainsComponent c : cnt.getContains()) { - if (code.equals(c.getCode()) && system.equals(c.getSystem().toString())) - return true; - if (codeinExpansion(c, system, code)) - return true; - } - return false; - } - - private void checkCodeableConcept(List errors, String path, WrapperElement element, StructureDefinition profile, ElementDefinition theElementCntext) throws EOperationOutcome, Exception { - if (theElementCntext != null && theElementCntext.hasBinding()) { - ElementDefinitionBindingComponent binding = theElementCntext.getBinding(); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, binding != null, "Binding for "+path+" missing (cc)")) { - if (binding.hasValueSet() && binding.getValueSet() instanceof Reference) { - ValueSet unexpandedVs = resolveBindingReference(binding.getValueSet()); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, unexpandedVs != null, "ValueSet "+describeReference(binding.getValueSet())+" not found")) { - ValueSet vs; - try { - boolean found = false; - boolean any = false; - WrapperElement c = element.getFirstChild(); - while (c != null) { - if (c.getName().equals("coding")) { - any = true; - String system = c.getNamedChildValue("system"); - String code = c.getNamedChildValue("code"); - if (system != null && code != null) { - ValueSetExpansionOutcome exp = cache.getExpander().expand(unexpandedVs); - vs = exp != null ? exp.getValueset() : null; - if (vs == null) { - if (binding.getStrength() != BindingStrength.REQUIRED) { - ValidationResult validationResult = context.validateCode(system, code, null); - if (validationResult.isOk()) { - found = true; - } else { - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Unable to validate code \"{0}\" in code system \"{1}\"", code, system); - return; - } - } - } - if (found == false) { - if (!warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, vs != null, "Unable to expand value set for "+describeReference(binding.getValueSet()))) { - return; - } - } - found = found || codeInExpansion(vs, system, code); - } - } - c = c.getNextSibling(); - } - if (!any && binding.getStrength() == BindingStrength.REQUIRED) - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, found, "No code provided, and value set "+describeReference(binding.getValueSet())+" ("+unexpandedVs.getUrl()+") is required"); - if (any) - if (binding.getStrength() == BindingStrength.PREFERRED) - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, found, "None of the codes are in the example value set "+describeReference(binding.getValueSet())+" ("+unexpandedVs.getUrl()+")"); - else if (binding.getStrength() == BindingStrength.EXTENSIBLE) - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, found, "None of the codes are in the expected value set "+describeReference(binding.getValueSet())+" ("+unexpandedVs.getUrl()+")"); - - } catch (Exception e) { - if (e.getMessage() == null) { - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Exception opening value set "+unexpandedVs.getUrl()+" for "+describeReference(binding.getValueSet())+": --Null--"); -// } else if (!e.getMessage().contains("unable to find value set http://snomed.info/sct")) { -// hint(errors, IssueType.CODEINVALID, path, suppressLoincSnomedMessages, "Snomed value set - not validated"); -// } else if (!e.getMessage().contains("unable to find value set http://loinc.org")) { -// hint(errors, IssueType.CODEINVALID, path, suppressLoincSnomedMessages, "Loinc value set - not validated"); - } else - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Exception opening value set "+unexpandedVs.getUrl()+" for "+describeReference(binding.getValueSet())+": "+e.getMessage()); - } - } - } else if (binding.hasValueSet()) { - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding by URI reference cannot be checked"); - } else { - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding for path " + path + " has no source, so can't be checked"); - } - } - } - } - - private String describeReference(Type reference) { - if (reference == null) - return "null"; - if (reference instanceof UriType) - return ((UriType)reference).getValue(); - if (reference instanceof Reference) - return ((Reference)reference).getReference(); - return "??"; - } - - - private boolean checkCode(List errors, WrapperElement element, String path, String code, String system, String display) throws Exception { - if (context.supportsSystem(system)) { - ValidationResult s = context.validateCode(system, code, display); - if (s == null || s.isOk()) - return true; - if (s.getSeverity() == IssueSeverity.INFORMATION) - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, s == null, s.getMessage()); - else if (s.getSeverity() == IssueSeverity.WARNING) - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, s == null, s.getMessage()); - else - return rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, s == null, s.getMessage()); - return true; - } else if (system.startsWith("http://hl7.org/fhir")) { - if (system.equals("http://hl7.org/fhir/sid/icd-10")) - return true; // else don't check ICD-10 (for now) - else { - ValueSet vs = getValueSet(system); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, vs != null, "Unknown Code System "+system)) { - ConceptDefinitionComponent def = getCodeDefinition(vs, code); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, def != null, "Unknown Code ("+system+"#"+code+")")) - return warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, display == null || display.equals(def.getDisplay()), "Display should be '"+def.getDisplay()+"'"); - } - return false; - } - } else if (system.startsWith("http://loinc.org")) { - return true; - } else if (system.startsWith("http://unitsofmeasure.org")) { - return true; - } - else - return true; - } - - private ConceptDefinitionComponent getCodeDefinition(ConceptDefinitionComponent c, String code) { - if (code.equals(c.getCode())) - return c; - for (ConceptDefinitionComponent g : c.getConcept()) { - ConceptDefinitionComponent r = getCodeDefinition(g, code); - if (r != null) - return r; - } - return null; - } - - private ConceptDefinitionComponent getCodeDefinition(ValueSet vs, String code) { - for (ConceptDefinitionComponent c : vs.getCodeSystem().getConcept()) { - ConceptDefinitionComponent r = getCodeDefinition(c, code); - if (r != null) - return r; - } - return null; - } - - private ValueSet getValueSet(String system) throws Exception { - return context.fetchCodeSystem(system); - } - - - public class ProfileStructureIterator { - - private StructureDefinition profile; - private ElementDefinition elementDefn; - private List names = new ArrayList(); - private Map> children = new HashMap>(); - private int cursor; - - public ProfileStructureIterator(StructureDefinition profile, ElementDefinition elementDefn) { - this.profile = profile; - this.elementDefn = elementDefn; - loadMap(); - cursor = -1; + public ChildIterator(String path, WrapperElement element) { + parent = element; + basePath = path; } - private void loadMap() { - int i = profile.getSnapshot().getElement().indexOf(elementDefn) + 1; - String lead = elementDefn.getPath(); - while (i < profile.getSnapshot().getElement().size()) { - String name = profile.getSnapshot().getElement().get(i).getPath(); - if (name.length() <= lead.length()) - return; // cause we've got to the end of the possible matches - String tail = name.substring(lead.length()+1); - if (Utilities.isToken(tail) && name.substring(0, lead.length()).equals(lead)) { - List list = children.get(tail); - if (list == null) { - list = new ArrayList(); - names.add(tail); - children.put(tail, list); - } - list.add(profile.getSnapshot().getElement().get(i)); - } - i++; - } + public int count() { + WrapperElement n = child.getNextSibling(); + if (n != null && n.getName().equals(child.getName())) { + return lastCount + 1; + } else + return -1; } - public boolean more() { - cursor++; - return cursor < names.size(); - } - - public List current() { - return children.get(name()); + public WrapperElement element() { + return child; } public String name() { - return names.get(cursor); + return child.getName(); + } + + public boolean next() { + if (child == null) { + child = parent.getFirstChild(); + lastCount = 0; + } else { + String lastName = child.getName(); + child = child.getNextSibling(); + if (child != null && child.getName().equals(lastName)) + lastCount++; + else + lastCount = 0; + } + return child != null; + } + + public String path() { + WrapperElement n = child.getNextSibling(); + if (parent.isXml()) { + String sfx = ""; + if (n != null && n.getName().equals(child.getName())) { + sfx = "[" + Integer.toString(lastCount + 1) + "]"; + } + if (FormatUtilities.XHTML_NS.equals(child.getNamespace())) + return basePath + "/h:" + name() + sfx; + else + return basePath + "/f:" + name() + sfx; + } else { + String sfx = ""; + if (n != null && n.getName().equals(child.getName())) { + sfx = "/" + Integer.toString(lastCount + 1); + } + return basePath + "/" + name() + sfx; + } + } + } + + public class DOMWrapperElement extends WrapperElement { + + private int col; + private Element element; + private int line; + + public DOMWrapperElement(Element element) { + super(); + this.element = element; + XmlLocationData loc = (XmlLocationData) element.getUserData(XmlLocationData.LOCATION_DATA_KEY); + if (loc != null) { + line = loc.getStartLine(); + col = loc.getStartColumn(); + } else { + line = -1; + col = -1; + } + } + + @Override + public int col() { + return col; + } + + @Override + public String getAttribute(String name) { + return element.getAttribute(name); + } + + @Override + public WrapperElement getFirstChild() { + Element res = XMLUtil.getFirstChild(element); + return res == null ? null : new DOMWrapperElement(res); + } + + @Override + public String getName() { + return element.getLocalName(); + } + + @Override + public WrapperElement getNamedChild(String name) { + Element res = XMLUtil.getNamedChild(element, name); + return res == null ? null : new DOMWrapperElement(res); + } + + @Override + public void getNamedChildren(String name, List list) { + List el = new ArrayList(); + XMLUtil.getNamedChildren(element, name, el); + for (Element e : el) + list.add(new DOMWrapperElement(e)); + } + + @Override + public void getNamedChildrenWithWildcard(String name, List list) { + List el = new ArrayList(); + XMLUtil.getNamedChildrenWithWildcard(element, name, el); + for (Element e : el) + list.add(new DOMWrapperElement(e)); + } + + @Override + public String getNamedChildValue(String name) { + return XMLUtil.getNamedChildValue(element, name); + } + + @Override + public String getNamespace() { + return element.getNamespaceURI(); + } + + @Override + public WrapperElement getNextSibling() { + Element res = XMLUtil.getNextSibling(element); + return res == null ? null : new DOMWrapperElement(res); + } + + @Override + public String getResourceType() { + return element.getLocalName(); + } + + @Override + public String getText() { + return element.getTextContent(); + } + + @Override + public boolean hasAttribute(String name) { + return element.hasAttribute(name); + } + + @Override + public boolean hasNamespace(String ns) { + for (int i = 0; i < element.getAttributes().getLength(); i++) { + Node a = element.getAttributes().item(i); + if ((a.getNodeName().equals("xmlns") || a.getNodeName().startsWith("xmlns:")) && a.getNodeValue().equals(ns)) + return true; + } + return false; + } + + @Override + public boolean hasProcessingInstruction() { + Node node = element.getFirstChild(); + while (node != null) { + if (node.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) + return true; + node = node.getNextSibling(); + } + return false; + } + + @Override + public boolean isXml() { + return true; + } + + @Override + public int line() { + return line; } } - private void checkByProfile(List errors, String path, WrapperElement focus, StructureDefinition profile, ElementDefinition elementDefn) throws Exception { - // we have an element, and the structure that describes it. - // we know that's it's valid against the underlying spec - is it valid against this one? - // in the instance validator above, we assume that schema or schmeatron has taken care of cardinalities, but here, we have no such reliance. - // so the walking algorithm is different: we're going to walk the definitions - String type; - if (elementDefn.getPath().endsWith("[x]")) { - String tail = elementDefn.getPath().substring(elementDefn.getPath().lastIndexOf(".")+1, elementDefn.getPath().length()-3); - type = focus.getName().substring(tail.length()); - rule(errors, IssueType.STRUCTURE, focus.line(), focus.col(), path, typeAllowed(type, elementDefn.getType()), "The type '"+type+"' is not allowed at this point (must be one of '"+typeSummary(elementDefn)+")"); - } else { - if (elementDefn.getType().size() == 1) { - type = elementDefn.getType().size() == 0 ? null : elementDefn.getType().get(0).getCode(); - } else - type = null; - } - // constraints: - for (ElementDefinitionConstraintComponent c : elementDefn.getConstraint()) - checkConstraint(errors, path, focus, c); - if (elementDefn.hasBinding() && type != null) - checkBinding(errors, path, focus, profile, elementDefn, type); - - // type specific checking: - if (type != null && typeIsPrimitive(type)) { - checkPrimitiveByProfile(errors, path, focus, elementDefn); - } else { - if (elementDefn.hasFixed()) - checkFixedValue(errors, path, focus, elementDefn.getFixed(), ""); - - ProfileStructureIterator walker = new ProfileStructureIterator(profile, elementDefn); - while (walker.more()) { - // collect all the slices for the path - List childset = walker.current(); - // collect all the elements that match it by name - List children = new ArrayList(); - focus.getNamedChildrenWithWildcard(walker.name(), children); + public class ElementInfo { + + public int count; + public ElementDefinition definition; + private WrapperElement element; + private String name; + private String path; + + public ElementInfo(String name, WrapperElement element, String path, int count) { + this.name = name; + this.element = element; + this.path = path; + this.count = count; + } + + public int col() { + return element.col(); + } + + public int line() { + return element.line(); + } - if (children.size() == 0) { - // well, there's no children - should there be? - for (ElementDefinition defn : childset) { - if (!rule(errors, IssueType.REQUIRED, focus.line(), focus.col(), path, defn.getMin() == 0, "Required Element '"+walker.name()+"' missing")) - break; // no point complaining about missing ones after the first one - } - } else if (childset.size() == 1) { - // simple case: one possible definition, and one or more children. - rule(errors, IssueType.STRUCTURE, focus.line(), focus.col(), path, childset.get(0).getMax().equals("*") || Integer.parseInt(childset.get(0).getMax()) >= children.size(), - "Too many elements for '"+walker.name()+"'"); // todo: sort out structure - for (WrapperElement child : children) { - checkByProfile(errors, childset.get(0).getPath(), child, profile, childset.get(0)); - } - } else { - // ok, this is the full case - we have a list of definitions, and a list of candidates for meeting those definitions. - // we need to decide *if* that match a given definition - } - } - } } - private void checkBinding(List errors, String path, WrapperElement focus, StructureDefinition profile, ElementDefinition elementDefn, String type) throws EOperationOutcome, Exception { - ElementDefinitionBindingComponent bc = elementDefn.getBinding(); + public class JsonWrapperElement extends WrapperElement { + + private JsonElement _element; + private List children = new ArrayList(); + private JsonElement element; + private int index; + private String name; + private JsonWrapperElement parent; + private String path; + private String resourceType; + + public JsonWrapperElement(JsonElement element) { + super(); + this.name = null; + this.resourceType = ((JsonObject) element).get("resourceType").getAsString(); + this.element = element; + this.path = ""; + createChildren(); + } + + public JsonWrapperElement(String path, String name, JsonElement element, JsonElement _element, JsonWrapperElement parent, int index) { + super(); + this.path = path + "/" + name; + this.name = name; + this.element = element; + if (element instanceof JsonObject && ((JsonObject) element).has("resourceType")) + this.resourceType = ((JsonObject) element).get("resourceType").getAsString(); + this._element = _element; + this.parent = parent; + this.index = index; + createChildren(); + } + + @Override + public int col() { + // TODO Auto-generated method stub + return -1; + } + + private void createChildren() { + // System.out.println(" ..: "+path); + // we're going to make this look like the XML + if (element == null) + throw new Error("not done yet"); + + if (element instanceof JsonPrimitive) { + // we may have an element_ too + if (_element != null && _element instanceof JsonObject) + for (Entry t : ((JsonObject) _element).entrySet()) + processChild(t.getKey(), t.getValue()); + } else if (element instanceof JsonObject) { + for (Entry t : ((JsonObject) element).entrySet()) + if (!t.getKey().equals("resourceType")) { + processChild(t.getKey(), t.getValue()); + } + } else if (element instanceof JsonNull) { + // nothing to do + } else + throw new Error("unexpected condition: " + element.getClass().getName()); + } + + @Override + public String getAttribute(String name) { + if (name.equals("value")) { + if (element == null) + return null; + if (element instanceof JsonPrimitive) + return ((JsonPrimitive) element).getAsString(); + return null; + } + if (name.equals("xml:id")) { + WrapperElement c = getNamedChild("id"); + return c == null ? null : c.getAttribute("value"); + } + if (name.equals("url")) { + WrapperElement c = getNamedChild("url"); + return c == null ? null : c.getAttribute("value"); + } + throw new Error("not done yet: " + name); + } + + @Override + public WrapperElement getFirstChild() { + if (children.isEmpty()) + return null; + else + return children.get(0); + } + + @Override + public String getName() { + return name; + } + + @Override + public WrapperElement getNamedChild(String name) { + for (JsonWrapperElement j : children) + if (j.name.equals(name)) + return j; + return null; + } + + @Override + public void getNamedChildren(String name, List list) { + for (JsonWrapperElement j : children) + if (j.name.equals(name)) + list.add(j); + } + + @Override + public void getNamedChildrenWithWildcard(String name, List list) { + throw new Error("not done yet"); + } + + @Override + public String getNamedChildValue(String name) { + WrapperElement c = getNamedChild(name); + return c == null ? null : c.getAttribute("value"); + } + + @Override + public String getNamespace() { + // return element.getNamespaceURI(); + throw new Error("not done yet"); + } + + @Override + public WrapperElement getNextSibling() { + if (parent == null) + return null; + if (index >= parent.children.size() - 1) + return null; + return parent.children.get(index + 1); + } + + @Override + public String getResourceType() { + return resourceType; + } + + @Override + public String getText() { + throw new Error("not done yet"); + } + + @Override + public boolean hasAttribute(String name) { + if (name.equals("value")) { + if (element == null) + return false; + if (element instanceof JsonPrimitive) + return true; + return false; + } + if (name.equals("xml:id")) { + return getNamedChild("id") != null; + } + throw new Error("not done yet: " + name); + } + + @Override + public boolean hasNamespace(String ns) { + throw new Error("not done"); + } + + @Override + public boolean hasProcessingInstruction() { + return false; + } + + @Override + public boolean isXml() { + return false; + } + + @Override + public int line() { + return -1; + } + + private void processChild(String name, JsonElement e) throws Error { + if (name.startsWith("_")) { + name = name.substring(1); + if (((JsonObject) element).has(name)) + return; // it will get processed anyway + e = null; + } + JsonElement _e = element instanceof JsonObject ? ((JsonObject) element).get("_" + name) : null; + + if (e instanceof JsonPrimitive || (e == null && _e != null && !(_e instanceof JsonArray))) { + children.add(new JsonWrapperElement(path, name, e, _e, this, children.size())); + } else if (e instanceof JsonArray || (e == null && _e != null)) { + JsonArray array = (JsonArray) e; + JsonArray _array = (JsonArray) _e; + int max = array != null ? array.size() : 0; + if (_array != null && _array.size() > max) + max = _array.size(); + for (int i = 0; i < max; i++) { + JsonElement a = array == null || array.size() < i ? null : array.get(i); + JsonElement _a = _array == null || _array.size() < i ? null : _array.get(i); + children.add(new JsonWrapperElement(path, name, a, _a, this, children.size())); + } + } else if (e instanceof JsonObject) { + children.add(new JsonWrapperElement(path, name, e, null, this, children.size())); + } else + throw new Error("not done yet: " + e.getClass().getName()); + } - if (bc != null && bc.hasValueSet() && bc.getValueSet() instanceof Reference) { - String url = ((Reference) bc.getValueSet()).getReference(); - ValueSet vs = resolveValueSetReference(profile, (Reference) bc.getValueSet()); - if (vs == null) { - rule(errors, IssueType.STRUCTURE, focus.line(), focus.col(), path, false, "Cannot check binding on type '"+type+"' as the value set '"+url+"' could not be located"); - } else if (type.equals("code")) - checkBindingCode(errors, path, focus, vs); - else if (type.equals("Coding")) - checkBindingCoding(errors, path, focus, vs); - else if (type.equals("CodeableConcept")) - checkBindingCodeableConcept(errors, path, focus, vs); - else - rule(errors, IssueType.STRUCTURE, focus.line(), focus.col(), path, false, "Cannot check binding on type '"+type+"'"); - } } - private ValueSet resolveValueSetReference(StructureDefinition profile, Reference reference) throws EOperationOutcome, Exception { - if (reference.getReference().startsWith("#")) { - for (Resource r : profile.getContained()) { - if (r instanceof ValueSet && r.getId().equals(reference.getReference().substring(1))) - return (ValueSet) r; - } - return null; - } else - return resolveBindingReference(reference); - + private class NodeStack { + private ElementDefinition definition; + private WrapperElement element; + private ElementDefinition extension; + private String literalPath; // xpath format + private List logicalPaths; // dotted format, various entry points + private NodeStack parent; + private ElementDefinition type; + private boolean xml; + + public NodeStack(boolean xml) { + this.xml = xml; + } + + public String addToLiteralPath(String... path) { + StringBuilder b = new StringBuilder(); + b.append(getLiteralPath()); + if (xml) { + for (String p : path) { + if (p.startsWith(":")) { + b.append("["); + b.append(p.substring(1)); + b.append("]"); + } else { + b.append("/f:"); + b.append(p); + } + } + } else { + for (String p : path) { + b.append("/"); + if (p.startsWith(":")) { + b.append(p.substring(1)); + } else { + b.append(p); + } + } + } + return b.toString(); + } + + private ElementDefinition getDefinition() { + return definition; + } + + private WrapperElement getElement() { + return element; + } + + private String getLiteralPath() { + return literalPath == null ? "" : literalPath; + } + + private List getLogicalPaths() { + return logicalPaths == null ? new ArrayList() : logicalPaths; + } + + private ElementDefinition getType() { + return type; + } + + private NodeStack push(WrapperElement element, int count, ElementDefinition definition, ElementDefinition type) { + NodeStack res = new NodeStack(element.isXml()); + res.parent = this; + res.element = element; + res.definition = definition; + if (element.isXml()) { + res.literalPath = getLiteralPath() + (element.getNamespace().equals(FormatUtilities.XHTML_NS) ? "/h:" : "/f:") + element.getName(); + if (count > -1) + res.literalPath = res.literalPath + "[" + Integer.toString(count) + "]"; + } else { + if (element.getName() == null) + res.literalPath = ""; + else + res.literalPath = getLiteralPath() + "/" + element.getName(); + if (count > -1) + res.literalPath = res.literalPath + "/" + Integer.toString(count); + } + res.logicalPaths = new ArrayList(); + if (type != null) { + // type will be bull if we on a stitching point of a contained resource, or if.... + res.type = type; + String t = tail(definition.getPath()); + for (String lp : getLogicalPaths()) { + res.logicalPaths.add(lp + "." + t); + if (t.endsWith("[x]")) + res.logicalPaths.add(lp + "." + t.substring(0, t.length() - 3) + type.getPath()); + } + res.logicalPaths.add(type.getPath()); + } else if (definition != null) { + for (String lp : getLogicalPaths()) + res.logicalPaths.add(lp + "." + element.getName()); + } else + res.logicalPaths.addAll(getLogicalPaths()); + // CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); + // for (String lp : res.logicalPaths) + // b.append(lp); + // System.out.println(res.literalPath+" : "+b.toString()); + return res; + } + + private void setType(ElementDefinition type) { + this.type = type; + } } - private void checkBindingCode(List errors, String path, WrapperElement focus, ValueSet vs) { - // rule(errors, "exception", path, false, "checkBindingCode not done yet"); + public abstract class WrapperElement { + public abstract int col(); + + public abstract String getAttribute(String name); + + public abstract WrapperElement getFirstChild(); + + public abstract String getName(); + + public abstract WrapperElement getNamedChild(String name); + + public abstract void getNamedChildren(String name, List list); + + public abstract void getNamedChildrenWithWildcard(String name, List list); + + public abstract String getNamedChildValue(String name); + + public abstract String getNamespace(); + + public abstract WrapperElement getNextSibling(); + + public abstract String getResourceType(); + + public abstract String getText(); + + public abstract boolean hasAttribute(String name); + + public abstract boolean hasNamespace(String string); + + public abstract boolean hasProcessingInstruction(); + + public abstract boolean isXml(); + + public abstract int line(); } - private void checkBindingCoding(List errors, String path, WrapperElement focus, ValueSet vs) { - // rule(errors, "exception", path, false, "checkBindingCoding not done yet"); - } - - private void checkBindingCodeableConcept(List errors, String path, WrapperElement focus, ValueSet vs) { - // rule(errors, "exception", path, false, "checkBindingCodeableConcept not done yet"); - } - - private String typeSummary(ElementDefinition elementDefn) { - StringBuilder b = new StringBuilder(); - for (TypeRefComponent t : elementDefn.getType()) { - b.append("|"+t.getCode()); - } - return b.toString().substring(1); - } - - private boolean typeAllowed(String t, List types) { - for (TypeRefComponent type : types) { - if (t.equals(Utilities.capitalize(type.getCode()))) - return true; - if (t.equals("Resource") && Utilities.capitalize(type.getCode()).equals("Reference")) - return true; - } - return false; - } - - private void checkConstraint(List errors, String path, WrapperElement focus, ElementDefinitionConstraintComponent c) throws Exception { - -// try -// { -// XPathFactory xpf = new net.sf.saxon.xpath.XPathFactoryImpl(); -// NamespaceContext context = new NamespaceContextMap("f", "http://hl7.org/fhir", "h", "http://www.w3.org/1999/xhtml"); -// -// XPath xpath = xpf.newXPath(); -// xpath.setNamespaceContext(context); -// Boolean ok = (Boolean) xpath.evaluate(c.getXpath(), focus, XPathConstants.BOOLEAN); -// if (ok == null || !ok) { -// if (c.getSeverity() == ConstraintSeverity.warning) -// warning(errors, "invariant", path, false, c.getHuman()); -// else -// rule(errors, "invariant", path, false, c.getHuman()); -// } -// } -// catch (XPathExpressionException e) { -// rule(errors, "invariant", path, false, "error executing invariant: "+e.getMessage()); -// } - } - - private void checkPrimitiveByProfile(List errors, String path, WrapperElement focus, ElementDefinition elementDefn) { - // two things to check - length, and fixed value - String value = focus.getAttribute("value"); - if (elementDefn.hasMaxLengthElement()) { - rule(errors, IssueType.TOOLONG, focus.line(), focus.col(), path, value.length() <= elementDefn.getMaxLength(), "The value '"+value+"' exceeds the allow length limit of "+Integer.toString(elementDefn.getMaxLength())); - } - if (elementDefn.hasFixed()) { - checkFixedValue(errors, path, focus, elementDefn.getFixed(), ""); - } - } - - private void checkFixedValue(List errors, String path, WrapperElement focus, org.hl7.fhir.instance.model.Element fixed, String propName) { - if (fixed == null && focus == null) - ; // this is all good - else if (fixed == null && focus != null) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, false, "Unexpected element "+focus.getName()); - else if (fixed != null && focus == null) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, false, "Mising element "+propName); - else { - String value = focus.getAttribute("value"); - if (fixed instanceof org.hl7.fhir.instance.model.BooleanType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.BooleanType) fixed).asStringValue(), value), "Value is '"+value+"' but must be '"+((org.hl7.fhir.instance.model.BooleanType) fixed).asStringValue()+"'"); - else if (fixed instanceof org.hl7.fhir.instance.model.IntegerType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.IntegerType) fixed).asStringValue(), value), "Value is '"+value+"' but must be '"+((org.hl7.fhir.instance.model.IntegerType) fixed).asStringValue()+"'"); - else if (fixed instanceof org.hl7.fhir.instance.model.DecimalType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.DecimalType) fixed).asStringValue(), value), "Value is '"+value+"' but must be '"+((org.hl7.fhir.instance.model.DecimalType) fixed).asStringValue()+"'"); - else if (fixed instanceof org.hl7.fhir.instance.model.Base64BinaryType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.Base64BinaryType) fixed).asStringValue(), value), "Value is '"+value+"' but must be '"+((org.hl7.fhir.instance.model.Base64BinaryType) fixed).asStringValue()+"'"); - else if (fixed instanceof org.hl7.fhir.instance.model.InstantType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.InstantType) fixed).getValue().toString(), value), "Value is '"+value+"' but must be '"+((org.hl7.fhir.instance.model.InstantType) fixed).asStringValue()+"'"); - else if (fixed instanceof org.hl7.fhir.instance.model.StringType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.StringType) fixed).getValue(), value), "Value is '"+value+"' but must be '"+((org.hl7.fhir.instance.model.StringType) fixed).getValue()+"'"); - else if (fixed instanceof org.hl7.fhir.instance.model.UriType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.UriType) fixed).getValue(), value), "Value is '"+value+"' but must be '"+((org.hl7.fhir.instance.model.UriType) fixed).getValue()+"'"); - else if (fixed instanceof org.hl7.fhir.instance.model.DateType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.DateType) fixed).getValue().toString(), value), "Value is '"+value+"' but must be '"+((org.hl7.fhir.instance.model.DateType) fixed).getValue()+"'"); - else if (fixed instanceof org.hl7.fhir.instance.model.DateTimeType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.DateTimeType) fixed).getValue().toString(), value), "Value is '"+value+"' but must be '"+((org.hl7.fhir.instance.model.DateTimeType) fixed).getValue()+"'"); - else if (fixed instanceof org.hl7.fhir.instance.model.OidType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.OidType) fixed).getValue(), value), "Value is '"+value+"' but must be '"+((org.hl7.fhir.instance.model.OidType) fixed).getValue()+"'"); - else if (fixed instanceof org.hl7.fhir.instance.model.UuidType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.UuidType) fixed).getValue(), value), "Value is '"+value+"' but must be '"+((org.hl7.fhir.instance.model.UuidType) fixed).getValue()+"'"); - else if (fixed instanceof org.hl7.fhir.instance.model.CodeType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.CodeType) fixed).getValue(), value), "Value is '"+value+"' but must be '"+((org.hl7.fhir.instance.model.CodeType) fixed).getValue()+"'"); - else if (fixed instanceof org.hl7.fhir.instance.model.IdType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.IdType) fixed).getValue(), value), "Value is '"+value+"' but must be '"+((org.hl7.fhir.instance.model.IdType) fixed).getValue()+"'"); - else if (fixed instanceof Quantity) - checkQuantity(errors, path, focus, (Quantity) fixed); - else if (fixed instanceof Address) - checkAddress(errors, path, focus, (Address) fixed); - else if (fixed instanceof ContactPoint) - checkContactPoint(errors, path, focus, (ContactPoint) fixed); - else if (fixed instanceof Attachment) - checkAttachment(errors, path, focus, (Attachment) fixed); - else if (fixed instanceof Identifier) - checkIdentifier(errors, path, focus, (Identifier) fixed); - else if (fixed instanceof Coding) - checkCoding(errors, path, focus, (Coding) fixed); - else if (fixed instanceof HumanName) - checkHumanName(errors, path, focus, (HumanName) fixed); - else if (fixed instanceof CodeableConcept) - checkCodeableConcept(errors, path, focus, (CodeableConcept) fixed); - else if (fixed instanceof Timing) - checkTiming(errors, path, focus, (Timing) fixed); - else if (fixed instanceof Period) - checkPeriod(errors, path, focus, (Period) fixed); - else if (fixed instanceof Range) - checkRange(errors, path, focus, (Range) fixed); - else if (fixed instanceof Ratio) - checkRatio(errors, path, focus, (Ratio) fixed); - else if (fixed instanceof SampledData) - checkSampledData(errors, path, focus, (SampledData) fixed); - - else - rule(errors, IssueType.EXCEPTION, focus.line(), focus.col(), path, false, "Unhandled fixed value type "+fixed.getClass().getName()); - List extensions = new ArrayList(); - focus.getNamedChildren("extension", extensions); - if (fixed.getExtension().size() == 0) { - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, extensions.size() == 0, "No extensions allowed"); - } else if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, extensions.size() == fixed.getExtension().size(), "Extensions count mismatch: expected "+Integer.toString(fixed.getExtension().size())+" but found "+Integer.toString(extensions.size()))) { - for (Extension e : fixed.getExtension()) { - WrapperElement ex = getExtensionByUrl(extensions, e.getUrl()); - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, ex != null, "Extension count mismatch: unable to find extension: "+e.getUrl())) { - checkFixedValue(errors, path, ex.getFirstChild().getNextSibling(), e.getValue(), "extension.value"); - } - } - } - } - } - - private void checkAddress(List errors, String path, WrapperElement focus, Address fixed) { - checkFixedValue(errors, path+".use", focus.getNamedChild("use"), fixed.getUseElement(), "use"); - checkFixedValue(errors, path+".text", focus.getNamedChild("text"), fixed.getTextElement(), "text"); - checkFixedValue(errors, path+".city", focus.getNamedChild("city"), fixed.getCityElement(), "city"); - checkFixedValue(errors, path+".state", focus.getNamedChild("state"), fixed.getStateElement(), "state"); - checkFixedValue(errors, path+".country", focus.getNamedChild("country"), fixed.getCountryElement(), "country"); - checkFixedValue(errors, path+".zip", focus.getNamedChild("zip"), fixed.getPostalCodeElement(), "postalCode"); - - List lines = new ArrayList(); - focus.getNamedChildren( "line", lines); - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, lines.size() == fixed.getLine().size(), "Expected "+Integer.toString(fixed.getLine().size())+" but found "+Integer.toString(lines.size())+" line elements")) { - for (int i = 0; i < lines.size(); i++) - checkFixedValue(errors, path+".coding", lines.get(i), fixed.getLine().get(i), "coding"); - } - } - - private void checkContactPoint(List errors, String path, WrapperElement focus, ContactPoint fixed) { - checkFixedValue(errors, path+".system", focus.getNamedChild("system"), fixed.getSystemElement(), "system"); - checkFixedValue(errors, path+".value", focus.getNamedChild("value"), fixed.getValueElement(), "value"); - checkFixedValue(errors, path+".use", focus.getNamedChild("use"), fixed.getUseElement(), "use"); - checkFixedValue(errors, path+".period", focus.getNamedChild("period"), fixed.getPeriod(), "period"); - - } - - private void checkAttachment(List errors, String path, WrapperElement focus, Attachment fixed) { - checkFixedValue(errors, path+".contentType", focus.getNamedChild("contentType"), fixed.getContentTypeElement(), "contentType"); - checkFixedValue(errors, path+".language", focus.getNamedChild("language"), fixed.getLanguageElement(), "language"); - checkFixedValue(errors, path+".data", focus.getNamedChild("data"), fixed.getDataElement(), "data"); - checkFixedValue(errors, path+".url", focus.getNamedChild("url"), fixed.getUrlElement(), "url"); - checkFixedValue(errors, path+".size", focus.getNamedChild("size"), fixed.getSizeElement(), "size"); - checkFixedValue(errors, path+".hash", focus.getNamedChild("hash"), fixed.getHashElement(), "hash"); - checkFixedValue(errors, path+".title", focus.getNamedChild("title"), fixed.getTitleElement(), "title"); - } - - private void checkIdentifier(List errors, String path, WrapperElement focus, Identifier fixed) { - checkFixedValue(errors, path+".use", focus.getNamedChild("use"), fixed.getUseElement(), "use"); - checkFixedValue(errors, path+".label", focus.getNamedChild("type"), fixed.getType(), "type"); - checkFixedValue(errors, path+".system", focus.getNamedChild("system"), fixed.getSystemElement(), "system"); - checkFixedValue(errors, path+".value", focus.getNamedChild("value"), fixed.getValueElement(), "value"); - checkFixedValue(errors, path+".period", focus.getNamedChild("period"), fixed.getPeriod(), "period"); - checkFixedValue(errors, path+".assigner", focus.getNamedChild("assigner"), fixed.getAssigner(), "assigner"); - } - - private void checkCoding(List errors, String path, WrapperElement focus, Coding fixed) { - checkFixedValue(errors, path+".system", focus.getNamedChild("system"), fixed.getSystemElement(), "system"); - checkFixedValue(errors, path+".code", focus.getNamedChild("code"), fixed.getCodeElement(), "code"); - checkFixedValue(errors, path+".display", focus.getNamedChild("display"), fixed.getDisplayElement(), "display"); - checkFixedValue(errors, path+".userSelected", focus.getNamedChild("userSelected"), fixed.getUserSelectedElement(), "userSelected"); - } - - private void checkHumanName(List errors, String path, WrapperElement focus, HumanName fixed) { - checkFixedValue(errors, path+".use", focus.getNamedChild("use"), fixed.getUseElement(), "use"); - checkFixedValue(errors, path+".text", focus.getNamedChild("text"), fixed.getTextElement(), "text"); - checkFixedValue(errors, path+".period", focus.getNamedChild("period"), fixed.getPeriod(), "period"); - - List parts = new ArrayList(); - focus.getNamedChildren( "family", parts); - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getFamily().size(), "Expected "+Integer.toString(fixed.getFamily().size())+" but found "+Integer.toString(parts.size())+" family elements")) { - for (int i = 0; i < parts.size(); i++) - checkFixedValue(errors, path+".family", parts.get(i), fixed.getFamily().get(i), "family"); - } - focus.getNamedChildren( "given", parts); - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getFamily().size(), "Expected "+Integer.toString(fixed.getFamily().size())+" but found "+Integer.toString(parts.size())+" given elements")) { - for (int i = 0; i < parts.size(); i++) - checkFixedValue(errors, path+".given", parts.get(i), fixed.getFamily().get(i), "given"); - } - focus.getNamedChildren( "prefix", parts); - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getFamily().size(), "Expected "+Integer.toString(fixed.getFamily().size())+" but found "+Integer.toString(parts.size())+" prefix elements")) { - for (int i = 0; i < parts.size(); i++) - checkFixedValue(errors, path+".prefix", parts.get(i), fixed.getFamily().get(i), "prefix"); - } - focus.getNamedChildren( "suffix", parts); - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getFamily().size(), "Expected "+Integer.toString(fixed.getFamily().size())+" but found "+Integer.toString(parts.size())+" suffix elements")) { - for (int i = 0; i < parts.size(); i++) - checkFixedValue(errors, path+".suffix", parts.get(i), fixed.getFamily().get(i), "suffix"); - } - } - - private void checkCodeableConcept(List errors, String path, WrapperElement focus, CodeableConcept fixed) { - checkFixedValue(errors, path+".text", focus.getNamedChild("text"), fixed.getTextElement(), "text"); - List codings = new ArrayList(); - focus.getNamedChildren( "coding", codings); - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, codings.size() == fixed.getCoding().size(), "Expected "+Integer.toString(fixed.getCoding().size())+" but found "+Integer.toString(codings.size())+" coding elements")) { - for (int i = 0; i < codings.size(); i++) - checkFixedValue(errors, path+".coding", codings.get(i), fixed.getCoding().get(i), "coding"); - } - } - - private void checkTiming(List errors, String path, WrapperElement focus, Timing fixed) { - checkFixedValue(errors, path+".repeat", focus.getNamedChild("repeat"), fixed.getRepeat(), "value"); - - List events = new ArrayList(); - focus.getNamedChildren( "event", events); - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, events.size() == fixed.getEvent().size(), "Expected "+Integer.toString(fixed.getEvent().size())+" but found "+Integer.toString(events.size())+" event elements")) { - for (int i = 0; i < events.size(); i++) - checkFixedValue(errors, path+".event", events.get(i), fixed.getEvent().get(i), "event"); - } - } - - private void checkPeriod(List errors, String path, WrapperElement focus, Period fixed) { - checkFixedValue(errors, path+".start", focus.getNamedChild("start"), fixed.getStartElement(), "start"); - checkFixedValue(errors, path+".end", focus.getNamedChild("end"), fixed.getEndElement(), "end"); - } - - private void checkRange(List errors, String path, WrapperElement focus, Range fixed) { - checkFixedValue(errors, path+".low", focus.getNamedChild("low"), fixed.getLow(), "low"); - checkFixedValue(errors, path+".high", focus.getNamedChild("high"), fixed.getHigh(), "high"); - - } - - private void checkRatio(List errors, String path, WrapperElement focus, Ratio fixed) { - checkFixedValue(errors, path+".numerator", focus.getNamedChild("numerator"), fixed.getNumerator(), "numerator"); - checkFixedValue(errors, path+".denominator", focus.getNamedChild("denominator"), fixed.getDenominator(), "denominator"); - } - - private void checkSampledData(List errors, String path, WrapperElement focus, SampledData fixed) { - checkFixedValue(errors, path+".origin", focus.getNamedChild("origin"), fixed.getOrigin(), "origin"); - checkFixedValue(errors, path+".period", focus.getNamedChild("period"), fixed.getPeriodElement(), "period"); - checkFixedValue(errors, path+".factor", focus.getNamedChild("factor"), fixed.getFactorElement(), "factor"); - checkFixedValue(errors, path+".lowerLimit", focus.getNamedChild("lowerLimit"), fixed.getLowerLimitElement(), "lowerLimit"); - checkFixedValue(errors, path+".upperLimit", focus.getNamedChild("upperLimit"), fixed.getUpperLimitElement(), "upperLimit"); - checkFixedValue(errors, path+".dimensions", focus.getNamedChild("dimensions"), fixed.getDimensionsElement(), "dimensions"); - checkFixedValue(errors, path+".data", focus.getNamedChild("data"), fixed.getDataElement(), "data"); - } - - private void checkQuantity(List errors, String path, WrapperElement focus, Quantity fixed) { - checkFixedValue(errors, path+".value", focus.getNamedChild("value"), fixed.getValueElement(), "value"); - checkFixedValue(errors, path+".comparator", focus.getNamedChild("comparator"), fixed.getComparatorElement(), "comparator"); - checkFixedValue(errors, path+".units", focus.getNamedChild("unit"), fixed.getUnitElement(), "units"); - checkFixedValue(errors, path+".system", focus.getNamedChild("system"), fixed.getSystemElement(), "system"); - checkFixedValue(errors, path+".code", focus.getNamedChild("code"), fixed.getCodeElement(), "code"); - } - - private boolean check(String v1, String v2) { - return v1 == null ? Utilities.noString(v1) : v1.equals(v2); - } - - private WrapperElement getExtensionByUrl(List extensions, String urlSimple) { - for (WrapperElement e : extensions) { - if (urlSimple.equals(e.getNamedChildValue("url"))) - return e; - } - return null; - } - - - - } - diff --git a/pom.xml b/pom.xml index 0d5ae0b18c4..457f67f6f1e 100644 --- a/pom.xml +++ b/pom.xml @@ -196,6 +196,9 @@ + + yyyy-MM-dd'T'HH:mm:ss'Z' + 1.1