diff --git a/examples/src/main/java/example/ValidatorExamples.java b/examples/src/main/java/example/ValidatorExamples.java index 7f853a7f13f..c8c03d961f5 100644 --- a/examples/src/main/java/example/ValidatorExamples.java +++ b/examples/src/main/java/example/ValidatorExamples.java @@ -263,7 +263,13 @@ public class ValidatorExamples { // TODO: implement return null; } - }; + + @Override + public StructureDefinition generateSnapshot(StructureDefinition theInput, String theUrl, String theName) { + // TODO: implement + return null; + } + }; /* * ValidationSupportChain strings multiple instances of IValidationSupport together. The diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ValidateUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ValidateUtil.java index 8d234d690bb..7d8a1eedc19 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ValidateUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ValidateUtil.java @@ -75,4 +75,16 @@ public class ValidateUtil { } } + public static void exactlyOneNotNullOrThrowInvalidRequestException(Object[] theObjects, String theMessage) { + int count = 0; + for (Object next : theObjects) { + if (next != null) { + count++; + } + } + if (count != 1) { + throw new InvalidRequestException(theMessage); + } + } + } diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/LoadingValidationSupportDstu3.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/LoadingValidationSupportDstu3.java index b6545d2886b..86c64ffe0ef 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/LoadingValidationSupportDstu3.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/LoadingValidationSupportDstu3.java @@ -95,6 +95,11 @@ public class LoadingValidationSupportDstu3 implements IValidationSupport { return null; } + @Override + public StructureDefinition generateSnapshot(StructureDefinition theInput, String theUrl, String theName) { + return null; + } + @Override public List fetchAllStructureDefinitions(FhirContext theContext) { return Collections.emptyList(); diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/LoadingValidationSupportR4.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/LoadingValidationSupportR4.java index 458363b091d..2e5bbf88aa9 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/LoadingValidationSupportR4.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/LoadingValidationSupportR4.java @@ -92,6 +92,11 @@ public class LoadingValidationSupportR4 implements org.hl7.fhir.r4.hapi.ctx.IVal return false; } + @Override + public StructureDefinition generateSnapshot(StructureDefinition theInput, String theUrl, String theProfileName) { + return null; + } + @Override public CodeValidationResult validateCode(FhirContext theContext, String theCodeSystem, String theCode, String theDisplay) { return null; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoStructureDefinitionDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoStructureDefinitionDstu2.java new file mode 100644 index 00000000000..51914a8a398 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoStructureDefinitionDstu2.java @@ -0,0 +1,32 @@ +package ca.uhn.fhir.jpa.dao; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2019 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.model.dstu2.resource.StructureDefinition; + +public class FhirResourceDaoStructureDefinitionDstu2 extends FhirResourceDaoDstu2 implements IFhirResourceDaoStructureDefinition { + @Override + public StructureDefinition generateSnapshot(StructureDefinition theInput, String theUrl, String theName) { + // FIXME: implement + return null; + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDaoStructureDefinition.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDaoStructureDefinition.java new file mode 100644 index 00000000000..6cd6a7b79da --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDaoStructureDefinition.java @@ -0,0 +1,29 @@ +package ca.uhn.fhir.jpa.dao; + +import org.hl7.fhir.instance.model.api.IBaseResource; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2019 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +public interface IFhirResourceDaoStructureDefinition extends IFhirResourceDao { + + T generateSnapshot(T theInput, String theUrl, String theName); + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoStructureDefinitionDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoStructureDefinitionDstu3.java new file mode 100644 index 00000000000..abb4d95e172 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoStructureDefinitionDstu3.java @@ -0,0 +1,41 @@ +package ca.uhn.fhir.jpa.dao.dstu3; + +import ca.uhn.fhir.jpa.dao.IFhirResourceDaoStructureDefinition; +import org.apache.commons.lang3.Validate; +import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport; +import org.hl7.fhir.dstu3.model.StructureDefinition; +import org.springframework.beans.factory.annotation.Autowired; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2019 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +public class FhirResourceDaoStructureDefinitionDstu3 extends FhirResourceDaoDstu3 implements IFhirResourceDaoStructureDefinition { + + @Autowired + private IValidationSupport myValidationSupport; + + @Override + public StructureDefinition generateSnapshot(StructureDefinition theInput, String theUrl, String theName) { + StructureDefinition output = myValidationSupport.generateSnapshot(theInput, theUrl, theName); + Validate.notNull(output); + return output; + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/JpaValidationSupportDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/JpaValidationSupportDstu3.java index 918b37dc795..f8c5fb944ca 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/JpaValidationSupportDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/JpaValidationSupportDstu3.java @@ -128,7 +128,11 @@ public class JpaValidationSupportDstu3 implements IJpaValidationSupportDstu3, Ap } } else if ("StructureDefinition".equals(resourceName)) { if (theUri.startsWith("http://hl7.org/fhir/StructureDefinition/")) { - return null; + // Don't allow the core FHIR definitions to be overwritten + String typeName = theUri.substring("http://hl7.org/fhir/StructureDefinition/".length()); + if (myDstu3Ctx.getElementDefinition(typeName) != null) { + return null; + } } SearchParameterMap params = new SearchParameterMap(); params.setLoadSynchronousUpTo(1); @@ -189,4 +193,9 @@ public class JpaValidationSupportDstu3 implements IJpaValidationSupportDstu3, Ap return null; } + @Override + public StructureDefinition generateSnapshot(StructureDefinition theInput, String theUrl, String theName) { + return null; + } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoStructureDefinitionR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoStructureDefinitionR4.java new file mode 100644 index 00000000000..9219788dffa --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoStructureDefinitionR4.java @@ -0,0 +1,41 @@ +package ca.uhn.fhir.jpa.dao.r4; + +import ca.uhn.fhir.jpa.dao.IFhirResourceDaoStructureDefinition; +import org.apache.commons.lang3.Validate; +import org.hl7.fhir.r4.hapi.ctx.IValidationSupport; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.springframework.beans.factory.annotation.Autowired; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2019 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +public class FhirResourceDaoStructureDefinitionR4 extends FhirResourceDaoR4 implements IFhirResourceDaoStructureDefinition { + + @Autowired + private IValidationSupport myValidationSupport; + + @Override + public StructureDefinition generateSnapshot(StructureDefinition theInput, String theUrl, String theName) { + StructureDefinition output = myValidationSupport.generateSnapshot(theInput, theUrl, theName); + Validate.notNull(output); + return output; + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/IJpaValidationSupportR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/IJpaValidationSupportR4.java index 937f811aaf1..4888226fd42 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/IJpaValidationSupportR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/IJpaValidationSupportR4.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.jpa.dao.r4; import org.hl7.fhir.r4.hapi.ctx.IValidationSupport; +import org.hl7.fhir.r4.model.StructureDefinition; /* * #%L @@ -23,5 +24,5 @@ import org.hl7.fhir.r4.hapi.ctx.IValidationSupport; */ public interface IJpaValidationSupportR4 extends IValidationSupport { - + // nothing yet } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/JpaValidationSupportR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/JpaValidationSupportR4.java index 4ea6ae14ad8..1f9c483565d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/JpaValidationSupportR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/JpaValidationSupportR4.java @@ -1,16 +1,23 @@ package ca.uhn.fhir.jpa.dao.r4; -import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.*; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.UriParam; +import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; +import org.apache.commons.lang3.Validate; +import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.conformance.ProfileUtilities; +import org.hl7.fhir.r4.context.IWorkerContext; +import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext; import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent; import org.hl7.fhir.r4.terminologies.ValueSetExpander; +import org.hl7.fhir.utilities.validation.ValidationMessage; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -19,6 +26,7 @@ import org.springframework.context.ApplicationContextAware; import javax.annotation.PostConstruct; import javax.transaction.Transactional; import javax.transaction.Transactional.TxType; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -121,7 +129,9 @@ public class JpaValidationSupportR4 implements IJpaValidationSupportR4, Applicat search = myValueSetDao.search(params); } } else if ("StructureDefinition".equals(resourceName)) { - if (theUri.startsWith("http://hl7.org/fhir/StructureDefinition/")) { + // Don't allow the core FHIR definitions to be overwritten + String typeName = theUri.substring("http://hl7.org/fhir/StructureDefinition/".length()); + if (myR4Ctx.getElementDefinition(typeName) != null) { return null; } SearchParameterMap params = new SearchParameterMap(); @@ -183,4 +193,9 @@ public class JpaValidationSupportR4 implements IJpaValidationSupportR4, Applicat return null; } + @Override + public StructureDefinition generateSnapshot(StructureDefinition theInput, String theUrl, String theProfileName) { + return null; + } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/StopWatch.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderStructureDefinitionDstu2.java similarity index 68% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/StopWatch.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderStructureDefinitionDstu2.java index a07b6c464ab..c7c419d4bfa 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/StopWatch.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderStructureDefinitionDstu2.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.util; +package ca.uhn.fhir.jpa.provider; /* * #%L @@ -10,7 +10,7 @@ package ca.uhn.fhir.jpa.util; * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -20,10 +20,10 @@ package ca.uhn.fhir.jpa.util; * #L% */ -/** - * @deprecated Use {@link ca.uhn.fhir.util.StopWatch} instead - */ -@Deprecated -public class StopWatch extends ca.uhn.fhir.util.StopWatch { - // this just exists since existing code may depend on it +import ca.uhn.fhir.model.dstu2.resource.StructureDefinition; + +public class BaseJpaResourceProviderStructureDefinitionDstu2 extends JpaResourceProviderDstu2 { + + // nothing yet + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderStructureDefinitionDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderStructureDefinitionDstu3.java new file mode 100644 index 00000000000..92a95821268 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderStructureDefinitionDstu3.java @@ -0,0 +1,81 @@ +package ca.uhn.fhir.jpa.provider.dstu3; + +import ca.uhn.fhir.jpa.dao.IFhirResourceDaoStructureDefinition; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.util.JpaConstants; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.param.UriParam; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.util.ValidateUtil; +import org.hl7.fhir.dstu3.model.IdType; +import org.hl7.fhir.dstu3.model.StringType; +import org.hl7.fhir.dstu3.model.StructureDefinition; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2019 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +public class BaseJpaResourceProviderStructureDefinitionDstu3 extends JpaResourceProviderDstu3 { + + /** + * $snapshot operation + */ + @Operation(name=JpaConstants.OPERATION_SNAPSHOT, idempotent = true) + public StructureDefinition snapshot( + @IdParam(optional = true) IdType theId, + @OperationParam(name = "definition") StructureDefinition theStructureDefinition, + @OperationParam(name = "url") StringType theUrl, + RequestDetails theRequestDetails) { + + ValidateUtil.exactlyOneNotNullOrThrowInvalidRequestException( + new Object[]{ theId, theStructureDefinition, theUrl }, + "Must supply either an ID or a StructureDefinition or a URL (but not more than one of these things)" + ); + + StructureDefinition sd; + if (theId == null && theStructureDefinition != null && theUrl == null) { + sd = theStructureDefinition; + } else if (theId != null && theStructureDefinition == null) { + sd = getDao().read(theId, theRequestDetails); + } else { + SearchParameterMap map = new SearchParameterMap(); + map.setLoadSynchronousUpTo(2); + map.add(StructureDefinition.SP_URL, new UriParam(theUrl.getValue())); + IBundleProvider outcome = getDao().search(map, theRequestDetails); + if (outcome.size() == 0) { + throw new ResourceNotFoundException("No StructureDefiniton found with url = '" + theUrl.getValue() + "'"); + } + sd = (StructureDefinition) outcome.getResources(0, 1).get(0); + } + + return getDao().generateSnapshot(sd, null, null); + } + + @Override + public IFhirResourceDaoStructureDefinition getDao() { + return (IFhirResourceDaoStructureDefinition) super.getDao(); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderStructureDefinitionR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderStructureDefinitionR4.java new file mode 100644 index 00000000000..150d63839ec --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderStructureDefinitionR4.java @@ -0,0 +1,79 @@ +package ca.uhn.fhir.jpa.provider.r4; + +import ca.uhn.fhir.jpa.dao.IFhirResourceDaoStructureDefinition; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.util.JpaConstants; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.param.UriParam; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.util.ValidateUtil; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.StructureDefinition; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2019 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +public class BaseJpaResourceProviderStructureDefinitionR4 extends JpaResourceProviderR4 { + + /** + * $snapshot operation + */ + @Operation(name=JpaConstants.OPERATION_SNAPSHOT, idempotent = true) + public StructureDefinition snapshot( + @IdParam(optional = true) IdType theId, + @OperationParam(name = "definition") StructureDefinition theStructureDefinition, + @OperationParam(name = "url") StringType theUrl, + RequestDetails theRequestDetails) { + + ValidateUtil.exactlyOneNotNullOrThrowInvalidRequestException( + new Object[]{ theId, theStructureDefinition, theUrl }, + "Must supply either an ID or a StructureDefinition or a URL (but not more than one of these things)" + ); + + StructureDefinition sd; + if (theId == null && theStructureDefinition != null && theUrl == null) { + sd = theStructureDefinition; + } else if (theId != null && theStructureDefinition == null) { + sd = getDao().read(theId, theRequestDetails); + } else { + SearchParameterMap map = new SearchParameterMap(); + map.setLoadSynchronousUpTo(2); + map.add(StructureDefinition.SP_URL, new UriParam(theUrl.getValue())); + IBundleProvider outcome = getDao().search(map, theRequestDetails); + if (outcome.size() == 0) { + throw new ResourceNotFoundException("No StructureDefiniton found with url = '" + theUrl.getValue() + "'"); + } + sd = (StructureDefinition) outcome.getResources(0, 1).get(0); + } + + return getDao().generateSnapshot(sd, null, null); + } + + @Override + public IFhirResourceDaoStructureDefinition getDao() { + return (IFhirResourceDaoStructureDefinition) super.getDao(); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/HapiTerminologySvcDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/HapiTerminologySvcDstu3.java index d74eee61edc..99701c94060 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/HapiTerminologySvcDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/HapiTerminologySvcDstu3.java @@ -297,5 +297,10 @@ public class HapiTerminologySvcDstu3 extends BaseHapiTerminologySvcImpl implemen return new CodeValidationResult(IssueSeverity.ERROR, "Unknown code {" + theCodeSystem + "}" + theCode); } + @Override + public StructureDefinition generateSnapshot(StructureDefinition theInput, String theUrl, String theName) { + return null; + } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/HapiTerminologySvcR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/HapiTerminologySvcR4.java index 4b224d4a4d2..ed11049c2a4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/HapiTerminologySvcR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/HapiTerminologySvcR4.java @@ -225,6 +225,11 @@ public class HapiTerminologySvcR4 extends BaseHapiTerminologySvcImpl implements return myTerminologySvc.supportsSystem(theSystem); } + @Override + public StructureDefinition generateSnapshot(StructureDefinition theInput, String theUrl, String theProfileName) { + return null; + } + @CoverageIgnore @Override public CodeValidationResult validateCode(FhirContext theContext, String theCodeSystem, String theCode, String theDisplay) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/JpaConstants.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/JpaConstants.java index bcda8e5ce08..23ddd3c4714 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/JpaConstants.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/JpaConstants.java @@ -142,4 +142,9 @@ public class JpaConstants { * Operation name for the "$subsumes" operation */ public static final String OPERATION_SUBSUMES = "$subsumes"; + + /** + * Operation name for the "$snapshot" operation + */ + public static final String OPERATION_SNAPSHOT = "$snapshot"; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChainDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChainDstu3.java index d3c812ebaa6..27f8239d779 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChainDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChainDstu3.java @@ -20,31 +20,52 @@ package ca.uhn.fhir.jpa.validation; * #L% */ -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; - +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.term.IHapiTerminologySvcDstu3; import org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport; import org.hl7.fhir.dstu3.hapi.validation.ValidationSupportChain; +import org.hl7.fhir.dstu3.hapi.validation.SnapshotGeneratingValidationSupport; +import org.hl7.fhir.dstu3.model.StructureDefinition; +import org.hl7.fhir.instance.model.api.IBaseResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; -import ca.uhn.fhir.jpa.term.IHapiTerminologySvcDstu3; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; public class JpaValidationSupportChainDstu3 extends ValidationSupportChain { - private DefaultProfileValidationSupport myDefaultProfileValidationSupport = new DefaultProfileValidationSupport(); - @Autowired @Qualifier("myJpaValidationSupportDstu3") public ca.uhn.fhir.jpa.dao.dstu3.IJpaValidationSupportDstu3 myJpaValidationSupportDstu3; - + private DefaultProfileValidationSupport myDefaultProfileValidationSupport = new DefaultProfileValidationSupport(); @Autowired private IHapiTerminologySvcDstu3 myTerminologyService; - + @Autowired + private FhirContext myFhirContext; + public JpaValidationSupportChainDstu3() { super(); } - + + @SuppressWarnings("unchecked") + @Override + public T fetchResource(FhirContext theContext, Class theClass, String theUri) { + if (theClass.equals(StructureDefinition.class)) { + return (T) fetchStructureDefinition(theContext, theUri); + } + return super.fetchResource(theContext, theClass, theUri); + } + + @Override + public StructureDefinition fetchStructureDefinition(FhirContext theCtx, String theUrl) { + StructureDefinition retVal = super.fetchStructureDefinition(theCtx, theUrl); + if (retVal != null && !retVal.hasSnapshot()) { + retVal = generateSnapshot(retVal, theUrl, null); + } + return retVal; + } + public void flush() { myDefaultProfileValidationSupport.flush(); } @@ -54,12 +75,13 @@ public class JpaValidationSupportChainDstu3 extends ValidationSupportChain { addValidationSupport(myDefaultProfileValidationSupport); addValidationSupport(myJpaValidationSupportDstu3); addValidationSupport(myTerminologyService); + addValidationSupport(new SnapshotGeneratingValidationSupport(myFhirContext, this)); } - + @PreDestroy public void preDestroy() { flush(); } - - + + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChainR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChainR4.java index edfad432064..b4d7efdfdda 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChainR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChainR4.java @@ -23,8 +23,12 @@ package ca.uhn.fhir.jpa.validation; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; +import ca.uhn.fhir.context.FhirContext; +import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.hapi.ctx.DefaultProfileValidationSupport; +import org.hl7.fhir.r4.hapi.validation.SnapshotGeneratingValidationSupport; import org.hl7.fhir.r4.hapi.validation.ValidationSupportChain; +import org.hl7.fhir.r4.model.StructureDefinition; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -33,7 +37,10 @@ import ca.uhn.fhir.jpa.term.IHapiTerminologySvcR4; public class JpaValidationSupportChainR4 extends ValidationSupportChain { private DefaultProfileValidationSupport myDefaultProfileValidationSupport = new DefaultProfileValidationSupport(); - + + @Autowired + private FhirContext myFhirContext; + @Autowired @Qualifier("myJpaValidationSupportR4") public ca.uhn.fhir.jpa.dao.r4.IJpaValidationSupportR4 myJpaValidationSupportR4; @@ -49,11 +56,31 @@ public class JpaValidationSupportChainR4 extends ValidationSupportChain { myDefaultProfileValidationSupport.flush(); } + @SuppressWarnings("unchecked") + @Override + public T fetchResource(FhirContext theContext, Class theClass, String theUri) { + if (theClass.equals(StructureDefinition.class)) { + return (T) fetchStructureDefinition(theContext, theUri); + } + return super.fetchResource(theContext, theClass, theUri); + } + + @Override + public StructureDefinition fetchStructureDefinition(FhirContext theCtx, String theUrl) { + StructureDefinition retVal = super.fetchStructureDefinition(theCtx, theUrl); + if (retVal != null && !retVal.hasSnapshot()) { + retVal = generateSnapshot(retVal, theUrl, null); + } + return retVal; + } + + @PostConstruct public void postConstruct() { addValidationSupport(myDefaultProfileValidationSupport); addValidationSupport(myJpaValidationSupportR4); addValidationSupport(myTerminologyService); + addValidationSupport(new SnapshotGeneratingValidationSupport(myFhirContext, this)); } @PreDestroy diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java index f82016be338..6311e1c4cd5 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java @@ -221,7 +221,7 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest { protected IStaleSearchDeletingSvc myStaleSearchDeletingSvc; @Autowired @Qualifier("myStructureDefinitionDaoDstu3") - protected IFhirResourceDao myStructureDefinitionDao; + protected IFhirResourceDaoStructureDefinition myStructureDefinitionDao; @Autowired @Qualifier("mySubscriptionDaoDstu3") protected IFhirResourceDaoSubscription mySubscriptionDao; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3StructureDefinitionTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3StructureDefinitionTest.java new file mode 100644 index 00000000000..bd0a49d2cd0 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3StructureDefinitionTest.java @@ -0,0 +1,39 @@ +package ca.uhn.fhir.jpa.dao.dstu3; + +import ca.uhn.fhir.util.TestUtil; +import org.hl7.fhir.dstu3.model.StructureDefinition; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Test; + +import java.io.IOException; + +import static org.junit.Assert.assertEquals; + +@SuppressWarnings({"unchecked", "deprecation"}) +public class FhirResourceDaoDstu3StructureDefinitionTest extends BaseJpaDstu3Test { + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoDstu3StructureDefinitionTest.class); + + @After + public final void after() { + } + + @Test + public void testGenerateSnapshot() throws IOException { + StructureDefinition sd = loadResourceFromClasspath(StructureDefinition.class, "/dstu3/profile-differential-patient-dstu3.json"); + assertEquals(0, sd.getSnapshot().getElement().size()); + + StructureDefinition output = myStructureDefinitionDao.generateSnapshot(sd, "http://foo", "THE BEST PROFILE"); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(output)); + + assertEquals(54, output.getSnapshot().getElement().size()); + } + + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java index 69ee0636fa3..dbd62093545 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java @@ -426,6 +426,23 @@ public class FhirResourceDaoDstu3ValidateTest extends BaseJpaDstu3Test { } } + @Test + public void testValidateUsingDifferentialProfile() throws IOException { + StructureDefinition sd = loadResourceFromClasspath(StructureDefinition.class, "/dstu3/profile-differential-patient-dstu3.json"); + myStructureDefinitionDao.create(sd); + + Patient p = new Patient(); + p.getMeta().addProfile("http://hl7.org/fhir/StructureDefinition/MyPatient421"); + p.setActive(true); + + String raw = myFhirCtx.newJsonParser().encodeResourceToString(p); + MethodOutcome outcome = myPatientDao.validate(p, null, raw, EncodingEnum.JSON, null, null, mySrd); + + String encoded = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome.getOperationOutcome()); + ourLog.info("OO: {}", encoded); + assertThat(encoded, containsString("No issues detected")); + } + @AfterClass public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java index 877d0601247..c53347bfb69 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java @@ -255,7 +255,7 @@ public abstract class BaseJpaR4Test extends BaseJpaTest { protected IStaleSearchDeletingSvc myStaleSearchDeletingSvc; @Autowired @Qualifier("myStructureDefinitionDaoR4") - protected IFhirResourceDao myStructureDefinitionDao; + protected IFhirResourceDaoStructureDefinition myStructureDefinitionDao; @Autowired @Qualifier("myConsentDaoR4") protected IFhirResourceDao myConsentDao; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4StructureDefinitionTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4StructureDefinitionTest.java new file mode 100644 index 00000000000..8784300b6bc --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4StructureDefinitionTest.java @@ -0,0 +1,55 @@ +package ca.uhn.fhir.jpa.dao.r4; + +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.SortOrderEnum; +import ca.uhn.fhir.rest.api.SortSpec; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.DateParam; +import ca.uhn.fhir.rest.param.DateRangeParam; +import ca.uhn.fhir.rest.param.ParamPrefixEnum; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.util.TestUtil; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Test; + +import java.io.IOException; +import java.util.List; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +@SuppressWarnings({"unchecked", "deprecation"}) +public class FhirResourceDaoR4StructureDefinitionTest extends BaseJpaR4Test { + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4StructureDefinitionTest.class); + + @After + public final void after() { + } + + @Test + public void testGenerateSnapshot() throws IOException { + StructureDefinition sd = loadResourceFromClasspath(StructureDefinition.class, "/r4/profile-differential-patient-r4.json"); + assertEquals(0, sd.getSnapshot().getElement().size()); + + StructureDefinition output = myStructureDefinitionDao.generateSnapshot(sd, "http://foo", "THE BEST PROFILE"); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(output)); + + assertEquals(51, output.getSnapshot().getElement().size()); + } + + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java index 2f205ef9269..3c5da884078 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java @@ -446,6 +446,26 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test { } } + @Test + public void testValidateUsingDifferentialProfile() throws IOException { + StructureDefinition sd = loadResourceFromClasspath(StructureDefinition.class, "/r4/profile-differential-patient-r4.json"); + myStructureDefinitionDao.create(sd); + + Patient p = new Patient(); + p.getText().setStatus(Narrative.NarrativeStatus.GENERATED); + p.getText().getDiv().setValue("
hello
"); + p.getMeta().addProfile("http://example.com/fhir/StructureDefinition/patient-1a-extensions"); + p.setActive(true); + + String raw = myFhirCtx.newJsonParser().encodeResourceToString(p); + MethodOutcome outcome = myPatientDao.validate(p, null, raw, EncodingEnum.JSON, null, null, mySrd); + + String encoded = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome.getOperationOutcome()); + ourLog.info("OO: {}", encoded); + assertThat(encoded, containsString("No issues detected")); + } + + @AfterClass public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3StructureDefinitionTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3StructureDefinitionTest.java new file mode 100644 index 00000000000..bc64c37986f --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3StructureDefinitionTest.java @@ -0,0 +1,101 @@ +package ca.uhn.fhir.jpa.provider.dstu3; + +import ca.uhn.fhir.jpa.util.JpaConstants; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.util.TestUtil; +import org.hl7.fhir.dstu3.model.IdType; +import org.hl7.fhir.dstu3.model.Parameters; +import org.hl7.fhir.dstu3.model.StringType; +import org.hl7.fhir.dstu3.model.StructureDefinition; +import org.hl7.fhir.instance.model.api.IIdType; +import org.junit.AfterClass; +import org.junit.Test; + +import java.io.IOException; + +import static org.junit.Assert.assertEquals; + +public class ResourceProviderDstu3StructureDefinitionTest extends BaseResourceProviderDstu3Test { + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + @Test + public void testSnapshotWithResourceParameter() throws IOException { + StructureDefinition sd = loadResourceFromClasspath(StructureDefinition.class, "/dstu3/profile-differential-patient-dstu3.json"); + + StructureDefinition response = ourClient + .operation() + .onType(StructureDefinition.class) + .named(JpaConstants.OPERATION_SNAPSHOT) + .withParameter(Parameters.class, "definition", sd) + .returnResourceType(StructureDefinition.class) + .execute(); + assertEquals(54, response.getSnapshot().getElement().size()); + } + + + @Test + public void testSnapshotWithId() throws IOException { + StructureDefinition sd = loadResourceFromClasspath(StructureDefinition.class, "/dstu3/profile-differential-patient-dstu3.json"); + IIdType id = ourClient.create().resource(sd).execute().getId().toUnqualifiedVersionless(); + + StructureDefinition response = ourClient + .operation() + .onInstance(id) + .named(JpaConstants.OPERATION_SNAPSHOT) + .withNoParameters(Parameters.class) + .returnResourceType(StructureDefinition.class) + .execute(); + assertEquals(54, response.getSnapshot().getElement().size()); + } + + + @Test + public void testSnapshotWithUrl() throws IOException { + StructureDefinition sd = loadResourceFromClasspath(StructureDefinition.class, "/dstu3/profile-differential-patient-dstu3.json"); + IIdType id = ourClient.create().resource(sd).execute().getId().toUnqualifiedVersionless(); + + StructureDefinition response = ourClient + .operation() + .onType(StructureDefinition.class) + .named(JpaConstants.OPERATION_SNAPSHOT) + .withParameter(Parameters.class, "url", new StringType("http://hl7.org/fhir/StructureDefinition/MyPatient421")) + .returnResourceType(StructureDefinition.class) + .execute(); + assertEquals(54, response.getSnapshot().getElement().size()); + } + + @Test + public void testSnapshotWithUrlAndId() { + try { + ourClient + .operation() + .onInstance(new IdType("StructureDefinition/123")) + .named(JpaConstants.OPERATION_SNAPSHOT) + .withParameter(Parameters.class, "url", new StringType("http://hl7.org/fhir/StructureDefinition/MyPatient421")) + .returnResourceType(StructureDefinition.class) + .execute(); + } catch (InvalidRequestException e) { + assertEquals("HTTP 400 Bad Request: Must supply either an ID or a StructureDefinition or a URL (but not more than one of these things)", e.getMessage()); + } + } + + @Test + public void testSnapshotWithInvalidUrl() { + try { + ourClient + .operation() + .onType(StructureDefinition.class) + .named(JpaConstants.OPERATION_SNAPSHOT) + .withParameter(Parameters.class, "url", new StringType("http://hl7.org/fhir/StructureDefinition/FOO")) + .returnResourceType(StructureDefinition.class) + .execute(); + } catch (ResourceNotFoundException e) { + assertEquals("HTTP 404 Not Found: No StructureDefiniton found with url = 'http://hl7.org/fhir/StructureDefinition/FOO'", e.getMessage()); + } + } +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4StructureDefinitionTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4StructureDefinitionTest.java new file mode 100644 index 00000000000..ce11e9bc582 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4StructureDefinitionTest.java @@ -0,0 +1,101 @@ +package ca.uhn.fhir.jpa.provider.r4; + +import ca.uhn.fhir.jpa.util.JpaConstants; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.util.TestUtil; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.instance.model.api.IIdType; +import org.junit.AfterClass; +import org.junit.Test; + +import java.io.IOException; + +import static org.junit.Assert.assertEquals; + +public class ResourceProviderR4StructureDefinitionTest extends BaseResourceProviderR4Test { + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + @Test + public void testSnapshotWithResourceParameter() throws IOException { + StructureDefinition sd = loadResourceFromClasspath(StructureDefinition.class, "/r4/profile-differential-patient-r4.json"); + + StructureDefinition response = ourClient + .operation() + .onType(StructureDefinition.class) + .named(JpaConstants.OPERATION_SNAPSHOT) + .withParameter(Parameters.class, "definition", sd) + .returnResourceType(StructureDefinition.class) + .execute(); + assertEquals(51, response.getSnapshot().getElement().size()); + } + + + @Test + public void testSnapshotWithId() throws IOException { + StructureDefinition sd = loadResourceFromClasspath(StructureDefinition.class, "/r4/profile-differential-patient-r4.json"); + IIdType id = ourClient.create().resource(sd).execute().getId().toUnqualifiedVersionless(); + + StructureDefinition response = ourClient + .operation() + .onInstance(id) + .named(JpaConstants.OPERATION_SNAPSHOT) + .withNoParameters(Parameters.class) + .returnResourceType(StructureDefinition.class) + .execute(); + assertEquals(51, response.getSnapshot().getElement().size()); + } + + + @Test + public void testSnapshotWithUrl() throws IOException { + StructureDefinition sd = loadResourceFromClasspath(StructureDefinition.class, "/r4/profile-differential-patient-r4.json"); + IIdType id = ourClient.create().resource(sd).execute().getId().toUnqualifiedVersionless(); + + StructureDefinition response = ourClient + .operation() + .onType(StructureDefinition.class) + .named(JpaConstants.OPERATION_SNAPSHOT) + .withParameter(Parameters.class, "url", new StringType("http://example.com/fhir/StructureDefinition/patient-1a-extensions")) + .returnResourceType(StructureDefinition.class) + .execute(); + assertEquals(51, response.getSnapshot().getElement().size()); + } + + @Test + public void testSnapshotWithUrlAndId() { + try { + ourClient + .operation() + .onInstance(new IdType("StructureDefinition/123")) + .named(JpaConstants.OPERATION_SNAPSHOT) + .withParameter(Parameters.class, "url", new StringType("http://example.com/fhir/StructureDefinition/patient-1a-extensions")) + .returnResourceType(StructureDefinition.class) + .execute(); + } catch (InvalidRequestException e) { + assertEquals("HTTP 400 Bad Request: Must supply either an ID or a StructureDefinition or a URL (but not more than one of these things)", e.getMessage()); + } + } + + @Test + public void testSnapshotWithInvalidUrl() { + try { + ourClient + .operation() + .onType(StructureDefinition.class) + .named(JpaConstants.OPERATION_SNAPSHOT) + .withParameter(Parameters.class, "url", new StringType("http://hl7.org/fhir/StructureDefinition/FOO")) + .returnResourceType(StructureDefinition.class) + .execute(); + } catch (ResourceNotFoundException e) { + assertEquals("HTTP 404 Not Found: No StructureDefiniton found with url = 'http://hl7.org/fhir/StructureDefinition/FOO'", e.getMessage()); + } + } +} diff --git a/hapi-fhir-jpaserver-base/src/test/resources/dstu3/profile-differential-patient-dstu3.json b/hapi-fhir-jpaserver-base/src/test/resources/dstu3/profile-differential-patient-dstu3.json new file mode 100644 index 00000000000..72f361533e7 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/resources/dstu3/profile-differential-patient-dstu3.json @@ -0,0 +1,64 @@ +{ + "resourceType":"StructureDefinition", + "meta":{ + "lastUpdated":"2017-08-26T18:27:24.959+01:00" + }, + "url":"http://hl7.org/fhir/StructureDefinition/MyPatient421", + "name":"MyPatient", + "status":"draft", + "date":"2017-08-25T14:34:21.08+01:00", + "fhirVersion":"3.0.1", + "kind":"resource", + "abstract":false, + "type":"Patient", + "baseDefinition":"http://hl7.org/fhir/StructureDefinition/Patient", + "derivation":"constraint", + "differential":{ + "element": [ + { + "id":"Patient.id", + "path":"Patient.id", + "definition":"The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes." + }, + { + "id":"Patient.extension", + "path":"Patient.extension", + "slicing":{ + "discriminator": [ + { + "type":"value", + "path":"url" + } + ], + "rules":"open" + } + }, + { + "id":"Patient.extension:us-core-race", + "path":"Patient.extension", + "sliceName":"us-core-race", + "type": [ + { + "code":"Extension", + "profile":"http://hl7.org/fhir/StructureDefinition/us-core-race" + } + ] + }, + { + "id":"Patient.extension:us-core-religion", + "path":"Patient.extension", + "sliceName":"us-core-religion", + "short":"Optional Extensions Element", + "definition":"Optional Extension Element - found in all resources.", + "min":"0", + "max":"*", + "type": [ + { + "code":"Extension", + "profile":"http://hl7.org/fhir/StructureDefinition/us-core-religion" + } + ] + } + ] + } +} diff --git a/hapi-fhir-jpaserver-base/src/test/resources/r4/profile-differential-patient-r4.json b/hapi-fhir-jpaserver-base/src/test/resources/r4/profile-differential-patient-r4.json new file mode 100644 index 00000000000..022b19ba9c1 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/resources/r4/profile-differential-patient-r4.json @@ -0,0 +1,80 @@ +{ + "resourceType":"StructureDefinition", + "url":"http://example.com/fhir/StructureDefinition/patient-1a-extensions", + "version":"0.1", + "name":"PatientWithExtensions", + "title":"Patient Profile 1 - 1a Extensions", + "status":"active", + "experimental":false, + "date":"2019", + "publisher":"Chris Grenz", + "contact": [ + { + "telecom": [ + { + "system":"email", + "value":"chris.grenz@thoughtworks.com" + } + ] + } + ], + "description":"Profile of Patient with extensions", + "copyright":"(c)2019 Chris Grenz into public domain", + "fhirVersion":"4.0.0", + "kind":"resource", + "abstract":false, + "type":"Patient", + "baseDefinition":"http://hl7.org/fhir/StructureDefinition/Patient", + "derivation":"constraint", + "differential":{ + "element": [ + { + "id":"Patient", + "path":"Patient" + }, + { + "id":"Patient.extension", + "path":"Patient.extension" + }, + { + "id":"Patient.extension:doNotCall", + "path":"Patient.extension", + "sliceName":"doNotCall", + "type": [ + { + "code":"Extension", + "profile": [ + "http://example.com/fhir/StructureDefinition/patient-donotcall" + ] + } + ] + }, + { + "id":"Patient.extension:legalCase", + "path":"Patient.extension", + "sliceName":"legalCase", + "type": [ + { + "code":"Extension", + "profile": [ + "http://example.com/fhir/StructureDefinition/patient-legalcase" + ] + } + ] + }, + { + "id":"Patient.extension:legalCase.value[x]:valueBoolean.extension:leadCounsel", + "path":"Patient.extension.valueBoolean.extension", + "sliceName":"leadCounsel", + "type": [ + { + "code":"Extension", + "profile": [ + "http://example.com/fhir/StructureDefinition/patient-legalcase-leadcounsel" + ] + } + ] + } + ] + } +} diff --git a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/ctx/DefaultProfileValidationSupport.java b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/ctx/DefaultProfileValidationSupport.java index 406ecd67218..70783346d7b 100644 --- a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/ctx/DefaultProfileValidationSupport.java +++ b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/ctx/DefaultProfileValidationSupport.java @@ -293,4 +293,9 @@ public class DefaultProfileValidationSupport implements IValidationSupport { return new CodeValidationResult(IssueSeverity.WARNING, "Unknown code: " + theCodeSystem + " / " + theCode); } + @Override + public StructureDefinition generateSnapshot(StructureDefinition theInput, String theUrl, String theName) { + return null; + } + } diff --git a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/ctx/IValidationSupport.java b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/ctx/IValidationSupport.java index 0cb23892bfc..5946f57dfbe 100644 --- a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/ctx/IValidationSupport.java +++ b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/ctx/IValidationSupport.java @@ -99,7 +99,16 @@ public interface IValidationSupport @Override CodeValidationResult validateCode(FhirContext theContext, String theCodeSystem, String theCode, String theDisplay); - class CodeValidationResult extends IContextValidationSupport.CodeValidationResult { + /** + * Generate a snapshot from the given differential profile. + * + * @param theInput + * @param theUrl + * @return Returns null if this module does not know how to handle this request + */ + StructureDefinition generateSnapshot(StructureDefinition theInput, String theUrl, String theName); + + class CodeValidationResult extends IContextValidationSupport.CodeValidationResult { public CodeValidationResult(ConceptDefinitionComponent theNext) { super(theNext); diff --git a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/ctx/DefaultProfileValidationSupport.java b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/ctx/DefaultProfileValidationSupport.java index f50726b5613..30a6e2a5ce8 100644 --- a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/ctx/DefaultProfileValidationSupport.java +++ b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/ctx/DefaultProfileValidationSupport.java @@ -174,6 +174,11 @@ public class DefaultProfileValidationSupport implements IValidationSupport { return cs != null && cs.getContent() != CodeSystemContentMode.NOTPRESENT; } + @Override + public StructureDefinition generateSnapshot(StructureDefinition theInput, String theUrl, String theProfileName) { + return null; + } + private void loadCodeSystems(FhirContext theContext, Map theCodeSystems, Map theValueSets, String theClasspath) { ourLog.info("Loading CodeSystem/ValueSet from classpath: {}", theClasspath); InputStream inputStream = DefaultProfileValidationSupport.class.getResourceAsStream(theClasspath); diff --git a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/ctx/IValidationSupport.java b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/ctx/IValidationSupport.java index d0c31bddec2..705e6b399a1 100644 --- a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/ctx/IValidationSupport.java +++ b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/ctx/IValidationSupport.java @@ -14,104 +14,104 @@ import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; import java.util.List; public interface IValidationSupport - extends ca.uhn.fhir.context.support.IContextValidationSupport { + extends ca.uhn.fhir.context.support.IContextValidationSupport { - /** - * Expands the given portion of a ValueSet - * - * @param theInclude - * The portion to include - * @return The expansion - */ - @Override - ValueSetExpander.ValueSetExpansionOutcome expandValueSet(FhirContext theContext, ConceptSetComponent theInclude); + /** + * Expands the given portion of a ValueSet + * + * @param theInclude The portion to include + * @return The expansion + */ + @Override + ValueSetExpander.ValueSetExpansionOutcome expandValueSet(FhirContext theContext, ConceptSetComponent theInclude); - /** - * Load and return all possible structure definitions - */ - @Override - List fetchAllStructureDefinitions(FhirContext theContext); + /** + * Load and return all possible structure definitions + */ + @Override + List fetchAllStructureDefinitions(FhirContext theContext); - /** - * Fetch a code system by Uri - * - * @param uri - * Canonical Uri of the code system - * @return The valueset (must not be null, but can be an empty ValueSet) - */ - @Override - CodeSystem fetchCodeSystem(FhirContext theContext, String uri); + /** + * Fetch a code system by Uri + * + * @param uri Canonical Uri of the code system + * @return The valueset (must not be null, but can be an empty ValueSet) + */ + @Override + CodeSystem fetchCodeSystem(FhirContext theContext, String uri); /** * Fetch a valueset by Uri * - * @param uri - * Canonical Uri of the ValueSet + * @param uri Canonical Uri of the ValueSet * @return The valueset (must not be null, but can be an empty ValueSet) */ ValueSet fetchValueSet(FhirContext theContext, String uri); - /** - * Loads a resource needed by the validation (a StructureDefinition, or a - * ValueSet) - * - * @param theContext - * The HAPI FHIR Context object current in use by the validator - * @param theClass - * The type of the resource to load - * @param theUri - * The resource URI - * @return Returns the resource, or null if no resource with the - * given URI can be found - */ - @Override - T fetchResource(FhirContext theContext, Class theClass, String theUri); + /** + * Loads a resource needed by the validation (a StructureDefinition, or a + * ValueSet) + * + * @param theContext The HAPI FHIR Context object current in use by the validator + * @param theClass The type of the resource to load + * @param theUri The resource URI + * @return Returns the resource, or null if no resource with the + * given URI can be found + */ + @Override + T fetchResource(FhirContext theContext, Class theClass, String theUri); - @Override - StructureDefinition fetchStructureDefinition(FhirContext theCtx, String theUrl); + @Override + StructureDefinition fetchStructureDefinition(FhirContext theCtx, String theUrl); - /** - * Returns true if codes in the given code system can be expanded - * or validated - * - * @param theSystem - * The URI for the code system, e.g. "http://loinc.org" - * @return Returns true if codes in the given code system can be - * validated - */ - @Override - boolean isCodeSystemSupported(FhirContext theContext, String theSystem); + /** + * Returns true if codes in the given code system can be expanded + * or validated + * + * @param theSystem The URI for the code system, e.g. "http://loinc.org" + * @return Returns true if codes in the given code system can be + * validated + */ + @Override + boolean isCodeSystemSupported(FhirContext theContext, String theSystem); - /** - * Validates that the given code exists and if possible returns a display - * name. This method is called to check codes which are found in "example" - * binding fields (e.g. Observation.code in the default profile. - * - * @param theCodeSystem - * The code system, e.g. "http://loinc.org" - * @param theCode - * The code, e.g. "1234-5" - * @param theDisplay - * The display name, if it should also be validated - * @return Returns a validation result object - */ - @Override - CodeValidationResult validateCode(FhirContext theContext, String theCodeSystem, String theCode, String theDisplay); + /** + * Generate a snapshot from the given differential profile. + * + * @param theInput + * @param theUrl + * @param theProfileName + * @return Returns null if this module does not know how to handle this request + */ + StructureDefinition generateSnapshot(StructureDefinition theInput, String theUrl, String theProfileName); - class CodeValidationResult extends IContextValidationSupport.CodeValidationResult { + /** + * Validates that the given code exists and if possible returns a display + * name. This method is called to check codes which are found in "example" + * binding fields (e.g. Observation.code in the default profile. + * + * @param theCodeSystem The code system, e.g. "http://loinc.org" + * @param theCode The code, e.g. "1234-5" + * @param theDisplay The display name, if it should also be validated + * @return Returns a validation result object + */ + @Override + CodeValidationResult validateCode(FhirContext theContext, String theCodeSystem, String theCode, String theDisplay); - public CodeValidationResult(ConceptDefinitionComponent theNext) { - super(theNext); - } + class CodeValidationResult extends IContextValidationSupport.CodeValidationResult { - public CodeValidationResult(IssueSeverity theSeverity, String theMessage) { - super(theSeverity, theMessage); - } + public CodeValidationResult(ConceptDefinitionComponent theNext) { + super(theNext); + } - public CodeValidationResult(IssueSeverity severity, String message, ConceptDefinitionComponent definition) { - super(severity, message, definition); - } + public CodeValidationResult(IssueSeverity theSeverity, String theMessage) { + super(theSeverity, theMessage); + } - } + public CodeValidationResult(IssueSeverity severity, String message, ConceptDefinitionComponent definition) { + super(severity, message, definition); + } + + } } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/CachingValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/CachingValidationSupport.java index 3d95895bd98..8b6799fc654 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/CachingValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/CachingValidationSupport.java @@ -69,4 +69,9 @@ public class CachingValidationSupport implements IValidationSupport { public CodeValidationResult validateCode(FhirContext theContext, String theCodeSystem, String theCode, String theDisplay) { return myWrap.validateCode(theContext, theCodeSystem, theCode, theDisplay); } + + @Override + public StructureDefinition generateSnapshot(StructureDefinition theInput, String theUrl, String theName) { + return myWrap.generateSnapshot(theInput, theUrl, theName); + } } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/PrePopulatedValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/PrePopulatedValidationSupport.java index 8cd61f35972..956d46db324 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/PrePopulatedValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/PrePopulatedValidationSupport.java @@ -142,7 +142,7 @@ public class PrePopulatedValidationSupport implements IValidationSupport { @Override public List fetchAllStructureDefinitions(FhirContext theContext) { - return new ArrayList(myStructureDefinitions.values()); + return new ArrayList<>(myStructureDefinitions.values()); } @Override @@ -185,4 +185,9 @@ public class PrePopulatedValidationSupport implements IValidationSupport { return null; } + @Override + public StructureDefinition generateSnapshot(StructureDefinition theInput, String theUrl, String theName) { + return null; + } + } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/SnapshotGeneratingValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/SnapshotGeneratingValidationSupport.java new file mode 100644 index 00000000000..33603ce8722 --- /dev/null +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/SnapshotGeneratingValidationSupport.java @@ -0,0 +1,139 @@ +package org.hl7.fhir.dstu3.hapi.validation; + +import ca.uhn.fhir.context.*; +import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; +import org.apache.commons.lang3.Validate; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.dstu3.conformance.ProfileUtilities; +import org.hl7.fhir.dstu3.context.IWorkerContext; +import org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext; +import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport; +import org.hl7.fhir.dstu3.model.CodeSystem; +import org.hl7.fhir.dstu3.model.ElementDefinition; +import org.hl7.fhir.dstu3.model.StructureDefinition; +import org.hl7.fhir.dstu3.model.ValueSet; +import org.hl7.fhir.utilities.validation.ValidationMessage; + +import java.util.ArrayList; +import java.util.List; + +/** + * Simple validation support module that handles profile snapshot generation. This is + * separate from other funcrtions since it needs a link to a validation support + * module itself, and it is useful to be able to pass a chain in. + */ +public class SnapshotGeneratingValidationSupport implements IValidationSupport { + private final FhirContext myCtx; + private final IValidationSupport myValidationSupport; + + public SnapshotGeneratingValidationSupport(FhirContext theCtx, IValidationSupport theValidationSupport) { + Validate.notNull(theCtx); + Validate.notNull(theValidationSupport); + myCtx = theCtx; + myValidationSupport = theValidationSupport; + } + + @Override + public ValueSet.ValueSetExpansionComponent expandValueSet(FhirContext theContext, ValueSet.ConceptSetComponent theInclude) { + return null; + } + + @Override + public List fetchAllConformanceResources(FhirContext theContext) { + return null; + } + + @Override + public List fetchAllStructureDefinitions(FhirContext theContext) { + return null; + } + + @Override + public CodeSystem fetchCodeSystem(FhirContext theContext, String uri) { + return null; + } + + @Override + public ValueSet fetchValueSet(FhirContext theContext, String uri) { + return null; + } + + @Override + public T fetchResource(FhirContext theContext, Class theClass, String theUri) { + return null; + } + + @Override + public StructureDefinition fetchStructureDefinition(FhirContext theCtx, String theUrl) { + return null; + } + + @Override + public boolean isCodeSystemSupported(FhirContext theContext, String theSystem) { + return false; + } + + @Override + public StructureDefinition generateSnapshot(StructureDefinition theInput, String theUrl, String theProfileName) { + IWorkerContext context = new HapiWorkerContext(myCtx, myValidationSupport); + ProfileUtilities.ProfileKnowledgeProvider profileKnowledgeProvider = new MyProfileKnowledgeWorker(); + ArrayList messages = new ArrayList<>(); + + StructureDefinition base = myValidationSupport.fetchStructureDefinition(myCtx, theInput.getBaseDefinition()); + if (base == null) { + throw new PreconditionFailedException("Unknown base definition: " + theInput.getBaseDefinition()); + } + + new ProfileUtilities(context, messages, profileKnowledgeProvider).generateSnapshot(base, theInput, theUrl, theProfileName); + + return theInput; + } + + @Override + public CodeValidationResult validateCode(FhirContext theContext, String theCodeSystem, String theCode, String theDisplay) { + return null; + } + + private class MyProfileKnowledgeWorker implements ProfileUtilities.ProfileKnowledgeProvider { + @Override + public boolean isDatatype(String typeSimple) { + BaseRuntimeElementDefinition def = myCtx.getElementDefinition(typeSimple); + Validate.notNull(typeSimple); + return (def instanceof RuntimePrimitiveDatatypeDefinition) || (def instanceof RuntimeCompositeDatatypeDefinition); + } + + @Override + public boolean isResource(String typeSimple) { + BaseRuntimeElementDefinition def = myCtx.getElementDefinition(typeSimple); + Validate.notNull(typeSimple); + return def instanceof RuntimeResourceDefinition; + } + + @Override + public boolean hasLinkFor(String typeSimple) { + return false; + } + + @Override + public String getLinkFor(String corePath, String typeSimple) { + return null; + } + + @Override + public BindingResolution resolveBinding(StructureDefinition def, ElementDefinition.ElementDefinitionBindingComponent binding, String path) throws FHIRException { + return null; + } + + @Override + public String getLinkForProfile(StructureDefinition profile, String url) { + return null; + } + + @Override + public boolean prependLinks() { + return false; + } + } + +} diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/ValidationSupportChain.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/ValidationSupportChain.java index 2dbc1fe8baa..5ca537433bb 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/ValidationSupportChain.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/ValidationSupportChain.java @@ -81,11 +81,14 @@ public class ValidationSupportChain implements IValidationSupport { @Override public List fetchAllStructureDefinitions(FhirContext theContext) { ArrayList retVal = new ArrayList(); - Set urls = new HashSet(); + Set urls = new HashSet<>(); for (IValidationSupport nextSupport : myChain) { - for (StructureDefinition next : nextSupport.fetchAllStructureDefinitions(theContext)) { - if (isBlank(next.getUrl()) || urls.add(next.getUrl())) { - retVal.add(next); + List list = nextSupport.fetchAllStructureDefinitions(theContext); + if (list != null) { + for (StructureDefinition next : list) { + if (isBlank(next.getUrl()) || urls.add(next.getUrl())) { + retVal.add(next); + } } } } @@ -167,5 +170,16 @@ public class ValidationSupportChain implements IValidationSupport { return myChain.get(0).validateCode(theCtx, theCodeSystem, theCode, theDisplay); } + @Override + public StructureDefinition generateSnapshot(StructureDefinition theInput, String theUrl, String theProfileName) { + StructureDefinition outcome = null; + for (org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport next : myChain) { + outcome = next.generateSnapshot(theInput, theUrl, theProfileName); + if (outcome != null) { + break; + } + } + return outcome; + } } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/BaseValidator.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/BaseValidator.java deleted file mode 100644 index 8711cceafd9..00000000000 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/BaseValidator.java +++ /dev/null @@ -1,446 +0,0 @@ -package org.hl7.fhir.dstu3.validation; - -/* -Copyright (c) 2011+, HL7, Inc -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name of HL7 nor the names of its contributors may be used to - endorse or promote products derived from this software without specific - prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, -INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. - -*/ - -import java.text.MessageFormat; -import java.util.List; - -import org.apache.commons.lang3.StringUtils; -import org.hl7.fhir.utilities.validation.ValidationMessage; -import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; -import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; -import org.hl7.fhir.utilities.validation.ValidationMessage.Source; - -public class BaseValidator { - - protected Source source; - - /** - * Test a rule and add a {@link IssueSeverity#FATAL} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - protected boolean fail(List errors, IssueType type, int line, int col, String path, boolean thePass, String msg) { - if (!thePass) { - errors.add(new ValidationMessage(source, type, line, col, path, msg, IssueSeverity.FATAL)); - } - return thePass; - } - - /** - * Test a rule and add a {@link IssueSeverity#FATAL} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - protected boolean fail(List errors, IssueType type, List pathParts, boolean thePass, String msg) { - if (!thePass) { - String path = toPath(pathParts); - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.FATAL)); - } - return thePass; - } - - /** - * Test a rule and add a {@link IssueSeverity#FATAL} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - protected boolean fail(List errors, IssueType type, List pathParts, boolean thePass, String theMessage, Object... theMessageArguments) { - if (!thePass) { - String path = toPath(pathParts); - errors.add(new ValidationMessage(source, type, -1, -1, path, formatMessage(theMessage, theMessageArguments), IssueSeverity.FATAL)); - } - return thePass; - } - - /** - * Test a rule and add a {@link IssueSeverity#FATAL} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - protected boolean fail(List errors, IssueType type, String path, boolean thePass, String msg) { - if (!thePass) { - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.FATAL)); - } - return thePass; - } - - - private String formatMessage(String theMessage, Object... theMessageArguments) { - String message; - if (theMessageArguments != null && theMessageArguments.length > 0) { - message = MessageFormat.format(theMessage, theMessageArguments); - } else { - message = theMessage; - } - return message; - } - - protected boolean grammarWord(String w) { - return w.equals("and") || w.equals("or") || w.equals("a") || w.equals("the") || w.equals("for") || w.equals("this") || w.equals("that") || w.equals("of"); - } - - /** - * Test a rule and add a {@link IssueSeverity#INFORMATION} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - protected boolean hint(List errors, IssueType type, int line, int col, String path, boolean thePass, String msg) { - if (!thePass) { - errors.add(new ValidationMessage(source, type, line, col, path, msg, IssueSeverity.INFORMATION)); - } - return thePass; - } - - /** - * Test a rule and add a {@link IssueSeverity#INFORMATION} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - protected boolean hint(List errors, IssueType type, int line, int col, String path, boolean thePass, String theMessage, Object... theMessageArguments) { - if (!thePass) { - String message = formatMessage(theMessage, theMessageArguments); - errors.add(new ValidationMessage(source, type, line, col, path, message, IssueSeverity.INFORMATION)); - } - return thePass; - } - - /** - * Test a rule and add a {@link IssueSeverity#INFORMATION} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - protected boolean hint(List errors, IssueType type, List pathParts, boolean thePass, String theMessage, Object... theMessageArguments) { - if (!thePass) { - String path = toPath(pathParts); - String message = formatMessage(theMessage, theMessageArguments); - errors.add(new ValidationMessage(source, type, -1, -1, path, message, IssueSeverity.INFORMATION)); - } - return thePass; - } - - /** - * Test a rule and add a {@link IssueSeverity#INFORMATION} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - protected boolean hint(List errors, IssueType type, String path, boolean thePass, String msg) { - if (!thePass) { - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.INFORMATION)); - } - return thePass; - } - - /** - * Test a rule and add a {@link IssueSeverity#ERROR} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - protected boolean rule(List errors, IssueType type, int line, int col, String path, boolean thePass, String theMessage, Object... theMessageArguments) { - if (!thePass) { - String message = formatMessage(theMessage, theMessageArguments); - errors.add(new ValidationMessage(source, type, line, col, path, message, IssueSeverity.ERROR)); - } - return thePass; - } - - /** - * Test a rule and add a {@link IssueSeverity#ERROR} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - protected boolean rule(List errors, IssueType type, List pathParts, boolean thePass, String msg) { - if (!thePass) { - String path = toPath(pathParts); - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.ERROR)); - } - return thePass; - } - - /** - * Test a rule and add a {@link IssueSeverity#ERROR} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - protected boolean rule(List errors, IssueType type, List pathParts, boolean thePass, String theMessage, Object... theMessageArguments) { - if (!thePass) { - String path = toPath(pathParts); - String message = formatMessage(theMessage, theMessageArguments); - errors.add(new ValidationMessage(source, type, -1, -1, path, message, IssueSeverity.ERROR)); - } - return thePass; - } - - /** - * Test a rule and add a {@link IssueSeverity#ERROR} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - protected boolean rule(List errors, IssueType type, String path, boolean thePass, String msg) { - if (!thePass) { - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.ERROR)); - } - return thePass; - } - - static public boolean rule(List errors, Source source, IssueType type, String path, boolean thePass, String msg) { - if (!thePass) { - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.ERROR)); - } - return thePass; - } - - /** - * Test a rule and add a {@link IssueSeverity#ERROR} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - protected boolean rule(List errors, IssueType type, String path, boolean thePass, String msg, String html) { - if (!thePass) { - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, html, IssueSeverity.ERROR)); - } - return thePass; - } - - protected String splitByCamelCase(String s) { - StringBuilder b = new StringBuilder(); - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - if (Character.isUpperCase(c) && !(i == 0 || Character.isUpperCase(s.charAt(i-1)))) - b.append(' '); - b.append(c); - } - return b.toString(); - } - - protected String stripPunctuation(String s, boolean numbers) { - StringBuilder b = new StringBuilder(); - for (char c : s.toCharArray()) { - int t = Character.getType(c); - if (t == Character.UPPERCASE_LETTER || t == Character.LOWERCASE_LETTER || t == Character.TITLECASE_LETTER || t == Character.MODIFIER_LETTER || t == Character.OTHER_LETTER || (t == Character.DECIMAL_DIGIT_NUMBER && numbers) || (t == Character.LETTER_NUMBER && numbers) || c == ' ') - b.append(c); - } - return b.toString(); - } - - private String toPath(List pathParts) { - if (pathParts == null || pathParts.isEmpty()) { - return ""; - } - return "//" + StringUtils.join(pathParts, '/'); - } - - /** - * Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - protected boolean warning(List errors, IssueType type, int line, int col, String path, boolean thePass, String msg, Object... theMessageArguments) { - if (!thePass) { - msg = formatMessage(msg, theMessageArguments); - errors.add(new ValidationMessage(source, type, line, col, path, msg, IssueSeverity.WARNING)); - } - return thePass; - - } - - protected boolean warningOrError(boolean isError, List errors, IssueType type, int line, int col, String path, boolean thePass, String msg, Object... theMessageArguments) { - if (!thePass) { - msg = formatMessage(msg, theMessageArguments); - errors.add(new ValidationMessage(source, type, line, col, path, msg, isError ? IssueSeverity.ERROR : IssueSeverity.WARNING)); - } - return thePass; - - } - - /** - * Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - protected boolean warning(List errors, IssueType type, List pathParts, boolean thePass, String theMessage, Object... theMessageArguments) { - if (!thePass) { - String path = toPath(pathParts); - String message = formatMessage(theMessage, theMessageArguments); - errors.add(new ValidationMessage(source, type, -1, -1, path, message, IssueSeverity.WARNING)); - } - return thePass; - } - - /** - * Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - protected boolean warning(List errors, IssueType type, String path, boolean thePass, String msg) { - if (!thePass) { - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.WARNING)); - } - return thePass; - } - - /** - * Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - protected boolean warning(List errors, IssueType type, String path, boolean thePass, String msg, String html) { - if (!thePass) { - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, html, IssueSeverity.WARNING)); - } - return thePass; - } - - /** - * Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - protected boolean warning(List errors, IssueType type, String path, boolean thePass, String msg, String html, Object... theMessageArguments) { - if (!thePass) { - msg = formatMessage(msg, theMessageArguments); - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, html, IssueSeverity.WARNING)); - } - return thePass; - } - - //--------- - /** - * Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - protected boolean suppressedwarning(List errors, IssueType type, int line, int col, String path, boolean thePass, String msg, Object... theMessageArguments) { - if (!thePass) { - msg = formatMessage(msg, theMessageArguments); - errors.add(new ValidationMessage(source, type, line, col, path, msg, IssueSeverity.INFORMATION)); - } - return thePass; - - } - - /** - * Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - protected boolean suppressedwarning(List errors, IssueType type, List pathParts, boolean thePass, String theMessage, Object... theMessageArguments) { - if (!thePass) { - String path = toPath(pathParts); - String message = formatMessage(theMessage, theMessageArguments); - errors.add(new ValidationMessage(source, type, -1, -1, path, message, IssueSeverity.INFORMATION)); - } - return thePass; - } - - /** - * Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - protected boolean suppressedwarning(List errors, IssueType type, String path, boolean thePass, String msg) { - if (!thePass) { - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.INFORMATION)); - } - return thePass; - } - - /** - * Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - protected boolean suppressedwarning(List errors, IssueType type, String path, boolean thePass, String msg, String html) { - if (!thePass) { - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, html, IssueSeverity.INFORMATION)); - } - return thePass; - } - - /** - * Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - protected boolean suppressedwarning(List errors, IssueType type, String path, boolean thePass, String msg, String html, Object... theMessageArguments) { - if (!thePass) { - msg = formatMessage(msg, theMessageArguments); - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, html, IssueSeverity.INFORMATION)); - } - return thePass; - } - -} diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/ExtensionLocatorService.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/ExtensionLocatorService.java deleted file mode 100644 index 67787ab6d16..00000000000 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/ExtensionLocatorService.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.hl7.fhir.dstu3.validation; - -import org.hl7.fhir.dstu3.model.StructureDefinition; -import org.hl7.fhir.exceptions.DefinitionException; - - - -/** - * This interface is used to provide extension location services for the validator - * - * when it encounters an extension, it asks this server to locate it, or tell it - * whether to ignore the extension, or mark it as invalid - * - * @author Grahame - * - */ -public interface ExtensionLocatorService { - - public enum Status { - Located, NotAllowed, Unknown - } - - public class ExtensionLocationResponse { - private Status status; - private StructureDefinition definition; - private String message; - private String url; - - public ExtensionLocationResponse(String url, Status status, StructureDefinition definition, String message) { - super(); - this.url = url; - this.status = status; - this.definition = definition; - this.message = message; - } - - public Status getStatus() { - return status; - } - - public StructureDefinition getDefinition() { - return definition; - } - - public String getMessage() { - return message; - } - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - /** - * This routine is used when walking into a complex extension. - * the non-tail part of the relative URL matches the end of the - * exiting URL - * @param url - the relative URL - * @return - * @throws DefinitionException - * @ - */ - public ExtensionLocationResponse clone(String url) throws DefinitionException { - if (!this.url.endsWith(url.substring(0, url.lastIndexOf(".")))) - throw new DefinitionException("the relative URL "+url+" cannot be used in the context "+this.url); - - return new ExtensionLocationResponse(this.url+"."+url.substring(url.lastIndexOf(".")+1), status, definition, message); - } - - } - - public ExtensionLocationResponse locateExtension(String uri); -} diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/GraphicalValidator.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/GraphicalValidator.java deleted file mode 100644 index 1fc5ae7c013..00000000000 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/GraphicalValidator.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.hl7.fhir.dstu3.validation; - -import java.awt.EventQueue; -import java.io.IOException; - -public class GraphicalValidator { - - public ValidatorFrame frame; - - /** - * Launch the application. - */ - public static void main(String[] args) { - EventQueue.invokeLater(new Runnable() { - public void run() { - try { - GraphicalValidator window = new GraphicalValidator(); - window.frame.setVisible(true); - } catch (Exception e) { - e.printStackTrace(); - } - } - }); - } - - /** - * Create the application. - * @throws IOException - */ - public GraphicalValidator() throws IOException { - initialize(); - } - - /** - * Initialize the contents of the frame. - * @throws IOException - */ - private void initialize() throws IOException { - frame = new ValidatorFrame(); - } - -} diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/InstanceValidator.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/InstanceValidator.java deleted file mode 100644 index 8fc9f6a0489..00000000000 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/InstanceValidator.java +++ /dev/null @@ -1,3735 +0,0 @@ -package org.hl7.fhir.dstu3.validation; - -import static org.apache.commons.lang3.StringUtils.isBlank; -import static org.apache.commons.lang3.StringUtils.isNotBlank; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.apache.commons.lang3.StringUtils; - -import org.hl7.fhir.dstu3.conformance.ProfileUtilities; -import org.hl7.fhir.dstu3.context.IWorkerContext; -import org.hl7.fhir.dstu3.context.IWorkerContext.ValidationResult; -import org.hl7.fhir.dstu3.elementmodel.Element; -import org.hl7.fhir.dstu3.elementmodel.Element.SpecialElement; -import org.hl7.fhir.dstu3.elementmodel.JsonParser; -import org.hl7.fhir.dstu3.elementmodel.Manager; -import org.hl7.fhir.dstu3.elementmodel.Manager.FhirFormat; -import org.hl7.fhir.dstu3.elementmodel.ObjectConverter; -import org.hl7.fhir.dstu3.elementmodel.ParserBase; -import org.hl7.fhir.dstu3.elementmodel.ParserBase.ValidationPolicy; -import org.hl7.fhir.dstu3.elementmodel.XmlParser; -import org.hl7.fhir.dstu3.formats.FormatUtilities; -import org.hl7.fhir.dstu3.model.Address; -import org.hl7.fhir.dstu3.model.Attachment; -import org.hl7.fhir.dstu3.model.BooleanType; -import org.hl7.fhir.dstu3.model.Bundle; -import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent; -import org.hl7.fhir.dstu3.model.CodeSystem; -import org.hl7.fhir.dstu3.model.CodeSystem.ConceptDefinitionComponent; -import org.hl7.fhir.dstu3.model.CodeableConcept; -import org.hl7.fhir.dstu3.model.Coding; -import org.hl7.fhir.dstu3.model.ContactPoint; -import org.hl7.fhir.dstu3.model.DateType; -import org.hl7.fhir.dstu3.model.DecimalType; -import org.hl7.fhir.dstu3.model.DomainResource; -import org.hl7.fhir.dstu3.model.ElementDefinition; -import org.hl7.fhir.dstu3.model.ElementDefinition.AggregationMode; -import org.hl7.fhir.dstu3.model.ElementDefinition.ConstraintSeverity; -import org.hl7.fhir.dstu3.model.ElementDefinition.DiscriminatorType; -import org.hl7.fhir.dstu3.model.ElementDefinition.ElementDefinitionBindingComponent; -import org.hl7.fhir.dstu3.model.ElementDefinition.ElementDefinitionConstraintComponent; -import org.hl7.fhir.dstu3.model.ElementDefinition.ElementDefinitionSlicingDiscriminatorComponent; -import org.hl7.fhir.dstu3.model.ElementDefinition.PropertyRepresentation; -import org.hl7.fhir.dstu3.model.ElementDefinition.TypeRefComponent; -import org.hl7.fhir.dstu3.model.Enumeration; -import org.hl7.fhir.dstu3.model.Enumerations.BindingStrength; -import org.hl7.fhir.dstu3.model.ExpressionNode; -import org.hl7.fhir.dstu3.model.Extension; -import org.hl7.fhir.dstu3.model.HumanName; -import org.hl7.fhir.dstu3.model.Identifier; -import org.hl7.fhir.dstu3.model.IntegerType; -import org.hl7.fhir.dstu3.model.Period; -import org.hl7.fhir.dstu3.model.Quantity; -import org.hl7.fhir.dstu3.model.Questionnaire; -import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemComponent; -import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemOptionComponent; -import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemType; -import org.hl7.fhir.dstu3.model.Range; -import org.hl7.fhir.dstu3.model.Ratio; -import org.hl7.fhir.dstu3.model.Reference; -import org.hl7.fhir.dstu3.model.Resource; -import org.hl7.fhir.dstu3.model.SampledData; -import org.hl7.fhir.dstu3.model.StringType; -import org.hl7.fhir.dstu3.model.StructureDefinition; -import org.hl7.fhir.dstu3.model.StructureDefinition.ExtensionContext; -import org.hl7.fhir.dstu3.model.StructureDefinition.StructureDefinitionKind; -import org.hl7.fhir.dstu3.model.StructureDefinition.StructureDefinitionSnapshotComponent; -import org.hl7.fhir.dstu3.model.StructureDefinition.TypeDerivationRule; -import org.hl7.fhir.dstu3.model.TimeType; -import org.hl7.fhir.dstu3.model.Timing; -import org.hl7.fhir.dstu3.model.Type; -import org.hl7.fhir.dstu3.model.UriType; -import org.hl7.fhir.dstu3.model.ValueSet; -import org.hl7.fhir.dstu3.model.ValueSet.ValueSetExpansionContainsComponent; -import org.hl7.fhir.dstu3.utils.FHIRLexer.FHIRLexerException; -import org.hl7.fhir.dstu3.utils.FHIRPathEngine; -import org.hl7.fhir.dstu3.utils.FHIRPathEngine.IEvaluationContext; -import org.hl7.fhir.dstu3.utils.IResourceValidator; -import org.hl7.fhir.dstu3.utils.ToolingExtensions; -import org.hl7.fhir.dstu3.utils.ValidationProfileSet; -import org.hl7.fhir.dstu3.utils.ValidationProfileSet.ProfileRegistration; -import org.hl7.fhir.exceptions.DefinitionException; -import org.hl7.fhir.exceptions.FHIRException; -import org.hl7.fhir.exceptions.FHIRFormatError; -import org.hl7.fhir.exceptions.PathEngineException; -import org.hl7.fhir.exceptions.TerminologyServiceException; -import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; -import org.hl7.fhir.utilities.Utilities; -import org.hl7.fhir.utilities.validation.ValidationMessage; -import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; -import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; -import org.hl7.fhir.utilities.validation.ValidationMessage.Source; -import org.hl7.fhir.utilities.xhtml.NodeType; -import org.hl7.fhir.utilities.xhtml.XhtmlNode; -import org.w3c.dom.Document; -import org.w3c.dom.Node; - -import com.google.gson.Gson; -import com.google.gson.JsonObject; - -import ca.uhn.fhir.util.ObjectUtil; - - -/** - * Thinking of using this in a java program? Don't! - * You should use on of the wrappers instead. Either in HAPI, or use ValidationEngine - * - * @author Grahame Grieve - * - */ -/* - * todo: - * check urn's don't start oid: or uuid: - */ -public class InstanceValidator extends BaseValidator implements IResourceValidator { - - - private IWorkerContext context; - private FHIRPathEngine fpe; - - // configuration items - private CheckDisplayOption checkDisplay; - private boolean anyExtensionsAllowed; - private boolean errorForUnknownProfiles; - private boolean noInvariantChecks; - private boolean noTerminologyChecks; - private BestPracticeWarningLevel bpWarnings; - - private List extensionDomains = new ArrayList(); - - private IdStatus resourceIdRule; - private boolean allowXsiLocation; - - // used during the build process to keep the overall volume of messages down - private boolean suppressLoincSnomedMessages; - - private Bundle logical; - - // time tracking - private long overall = 0; - private long txTime = 0; - private long sdTime = 0; - private long loadTime = 0; - private long fpeTime = 0; - - private boolean noBindingMsgSuppressed; - private HashMap resourceProfilesMap; - private IValidatorResourceFetcher fetcher; - long time = 0; - - /* - * Keeps track of whether a particular profile has been checked or not yet - */ - private class ProfileUsage { - private StructureDefinition profile; - private boolean checked; - - public ProfileUsage(StructureDefinition profile) { - this.profile = profile; - this.checked = false; - } - - public boolean isChecked() { - return checked; - } - - public void setChecked() { - this.checked = true; - } - - public StructureDefinition getProfile() { - return profile; - } - } - - /* - * Keeps track of all profiles associated with a resource element and whether the resource has been checked against those profiles yet - */ - public class ResourceProfiles { - private Element resource; - private Element owner; - private NodeStack stack; - private HashMap profiles; - private boolean processed; - - public ResourceProfiles(Element resource, NodeStack stack) { - this.resource = resource; - if (this.resource.getName().equals("contained")) - this.owner = stack.parent.element; - else - this.owner = resource; - this.stack = stack; - this.profiles = new HashMap(); - this.processed = false; - } - - public boolean isProcessed() { - return processed; - } - - public void setProcessed() { - processed = true; - } - - public NodeStack getStack() { - return stack; - } - - public Element getOwner() { - return owner; - } - - public boolean hasProfiles() { - return !profiles.isEmpty(); - } - - public void addProfiles(List errors, ValidationProfileSet profiles, String path, Element element) throws FHIRException { - for (ProfileRegistration profile : profiles.getCanonical()) - addProfile(errors, profile.getProfile(), profile.isError(), path, element); - } - - public boolean addProfile(List errors, String profile, boolean error, String path, Element element) { - String effectiveProfile = profile; - String version = null; - if (profile.contains("|")) { - effectiveProfile = profile.substring(0, profile.indexOf('|')); - version = profile.substring(profile.indexOf('|')+1); - } - StructureDefinition sd = context.fetchResource(StructureDefinition.class, effectiveProfile); - if (warningOrError(error, errors, IssueType.INVALID, element.line(), element.col(), path, sd != null, "StructureDefinition reference \"{0}\" could not be resolved", profile)) { - if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, version==null || (sd.getVersion()!=null && sd.getVersion().equals(version)), - "Referenced version " + version + " does not match found version " + sd.getVersion() + " for profile " + sd.getUrl(), profile)) { - if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, sd.hasSnapshot(), - "StructureDefinition has no snapshot - validation is against the snapshot, so it must be provided")) { - if (!profiles.containsKey(sd)) { - profiles.put(sd, new ProfileUsage(sd)); - addAncestorProfiles(sd); - return true; - } - } - } - } - return false; - } - - public void addAncestorProfiles(StructureDefinition sd) { - if (sd.getDerivation().equals(StructureDefinition.TypeDerivationRule.CONSTRAINT)) { - StructureDefinition parentSd = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition()); - if (parentSd != null && !profiles.containsKey(parentSd)) { - ProfileUsage pu = new ProfileUsage(parentSd); - pu.setChecked(); // We're going to check the child, so no need to check the parent - profiles.put(parentSd, pu); - } - } - } - - public List uncheckedProfiles() { - List uncheckedProfiles = new ArrayList(); - for (ProfileUsage profileUsage : profiles.values()) { - if (!profileUsage.isChecked()) - uncheckedProfiles.add(profileUsage); - } - return uncheckedProfiles; - } - - public boolean hasUncheckedProfiles() { - return !uncheckedProfiles().isEmpty(); - } - - public void checkProfile(StructureDefinition profile) { - ProfileUsage profileUsage = profiles.get(profile); - if (profileUsage==null) - throw new Error("Can't check profile that hasn't been added: " + profile.getUrl()); - else - profileUsage.setChecked(); - } - } - - public InstanceValidator(IWorkerContext theContext, IEvaluationContext hostServices) { - super(); - this.context = theContext; - fpe = new FHIRPathEngine(context); - fpe.setHostServices(hostServices); - source = Source.InstanceValidator; - } - - - @Override - public boolean isNoInvariantChecks() { - return noInvariantChecks; - } - - @Override - public IResourceValidator setNoInvariantChecks(boolean value) { - this.noInvariantChecks = value; - return this; - } - - @Override - public IValidatorResourceFetcher getFetcher() { - return this.fetcher; - } - - @Override - public IResourceValidator setFetcher(IValidatorResourceFetcher value) { - this.fetcher = value; - return this; - } - - private boolean allowUnknownExtension(String url) { - if (url.contains("example.org") || url.contains("acme.com") || url.contains("nema.org") || url.startsWith("http://hl7.org/fhir/tools/StructureDefinition/") || url.equals("http://hl7.org/fhir/StructureDefinition/structuredefinition-expression")) - // Added structuredefinition-expression explicitly because it wasn't defined in the version of the spec it needs to be used with - return true; - for (String s : extensionDomains) - if (url.startsWith(s)) - return true; - return anyExtensionsAllowed; - } - - private boolean isKnownExtension(String url) { - // Added structuredefinition-expression explicitly because it wasn't defined in the version of the spec it needs to be used with - if (url.contains("example.org") || url.contains("acme.com") || url.contains("nema.org") || url.startsWith("http://hl7.org/fhir/tools/StructureDefinition/") || url.equals("http://hl7.org/fhir/StructureDefinition/structuredefinition-expression")) - return true; - for (String s : extensionDomains) - if (url.startsWith(s)) - return true; - return false; - } - - 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); - break; - case Warning: - warning(errors, invalid, line, col, literalPath, test, message); - break; - case Hint: - hint(errors, invalid, line, col, literalPath, test, message); - break; - default: // do nothing - } - } - } - - @Override - public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List errors, InputStream stream, FhirFormat format) throws FHIRException, IOException { - return validate(appContext, errors, stream, format, new ValidationProfileSet()); - } - - @Override - public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List errors, InputStream stream, FhirFormat format, String profile) throws FHIRException, IOException { - return validate(appContext, errors, stream, format, new ValidationProfileSet(profile, true)); - } - - @Override - public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List errors, InputStream stream, FhirFormat format, StructureDefinition profile) throws FHIRException, IOException { - return validate(appContext, errors, stream, format, new ValidationProfileSet(profile)); - } - - @Override - public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List errors, InputStream stream, FhirFormat format, ValidationProfileSet profiles) throws FHIRException, IOException { - ParserBase parser = Manager.makeParser(context, format); - if (parser instanceof XmlParser) - ((XmlParser) parser).setAllowXsiLocation(allowXsiLocation); - parser.setupValidation(ValidationPolicy.EVERYTHING, errors); - long t = System.nanoTime(); - Element e = parser.parse(stream); - loadTime = System.nanoTime() - t; - if (e != null) - validate(appContext, errors, e, profiles); - return e; - } - - @Override - public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List errors, Resource resource) throws FHIRException, IOException { - return validate(appContext, errors, resource, new ValidationProfileSet()); - } - - @Override - public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List errors, Resource resource, String profile) throws FHIRException, IOException { - return validate(appContext, errors, resource, new ValidationProfileSet(profile, true)); - } - - @Override - public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List errors, Resource resource, StructureDefinition profile) throws FHIRException, IOException { - return validate(appContext, errors, resource, new ValidationProfileSet(profile)); - } - - @Override - public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List errors, Resource resource, ValidationProfileSet profiles) throws FHIRException, IOException { - long t = System.nanoTime(); - Element e = new ObjectConverter(context).convert(resource); - loadTime = System.nanoTime() - t; - validate(appContext, errors, e, profiles); - return e; - } - - @Override - public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List errors, org.w3c.dom.Element element) throws FHIRException, IOException { - return validate(appContext, errors, element, new ValidationProfileSet()); - } - - @Override - public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List errors, org.w3c.dom.Element element, String profile) throws FHIRException, IOException { - return validate(appContext, errors, element, new ValidationProfileSet(profile, true)); - } - - @Override - public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List errors, org.w3c.dom.Element element, StructureDefinition profile) throws FHIRException, IOException { - return validate(appContext, errors, element, new ValidationProfileSet(profile)); - } - - @Override - public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List errors, org.w3c.dom.Element element, ValidationProfileSet profiles) throws FHIRException, IOException { - XmlParser parser = new XmlParser(context); - parser.setupValidation(ValidationPolicy.EVERYTHING, errors); - long t = System.nanoTime(); - Element e = parser.parse(element); - loadTime = System.nanoTime() - t; - if (e != null) - validate(appContext, errors, e, profiles); - return e; - } - - @Override - public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List errors, Document document) throws FHIRException, IOException { - return validate(appContext, errors, document, new ValidationProfileSet()); - } - - @Override - public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List errors, Document document, String profile) throws FHIRException, IOException { - return validate(appContext, errors, document, new ValidationProfileSet(profile, true)); - } - - @Override - public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List errors, Document document, StructureDefinition profile) throws FHIRException, IOException { - return validate(appContext, errors, document, new ValidationProfileSet(profile)); - } - - @Override - public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List errors, Document document, ValidationProfileSet profiles) throws FHIRException, IOException { - XmlParser parser = new XmlParser(context); - parser.setupValidation(ValidationPolicy.EVERYTHING, errors); - long t = System.nanoTime(); - Element e = parser.parse(document); - loadTime = System.nanoTime() - t; - if (e != null) - validate(appContext, errors, e, profiles); - return e; - } - - @Override - public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List errors, JsonObject object) throws FHIRException, IOException { - return validate(appContext, errors, object, new ValidationProfileSet()); - } - - @Override - public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List errors, JsonObject object, String profile) throws FHIRException, IOException { - return validate(appContext, errors, object, new ValidationProfileSet(profile, true)); - } - - @Override - public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List errors, JsonObject object, StructureDefinition profile) throws FHIRException, IOException { - return validate(appContext, errors, object, new ValidationProfileSet(profile)); - } - - @Override - public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List errors, JsonObject object, ValidationProfileSet profiles) throws FHIRException, IOException { - JsonParser parser = new JsonParser(context); - parser.setupValidation(ValidationPolicy.EVERYTHING, errors); - long t = System.nanoTime(); - Element e = parser.parse(object); - loadTime = System.nanoTime() - t; - if (e != null) - validate(appContext, errors, e, profiles); - return e; - } - - @Override - public void validate(Object appContext, List errors, Element element) throws FHIRException, IOException { - ValidationProfileSet profileSet = new ValidationProfileSet(); - validate(appContext, errors, element, profileSet); - } - - private void validateRemainder(Object appContext, List errors) throws IOException, FHIRException { - boolean processedResource; - do { - processedResource = false; - Set keys = new HashSet(); - keys.addAll(resourceProfilesMap.keySet()); - for (Element resource : keys) { - ResourceProfiles rp = resourceProfilesMap.get(resource); - if (rp.hasUncheckedProfiles()) { - processedResource = true; - start(appContext, errors, rp.getOwner(), resource, null, rp.getStack()); - } - } - } while (processedResource); - } - - @Override - public void validate(Object appContext, List errors, Element element, String profile) throws FHIRException, IOException { - validate(appContext, errors, element, new ValidationProfileSet(profile, true)); - } - - @Override - public void validate(Object appContext, List errors, Element element, StructureDefinition profile) throws FHIRException, IOException { - validate(appContext, errors, element, new ValidationProfileSet(profile)); - } - - @Override - public void validate(Object appContext, List errors, Element element, ValidationProfileSet profiles) throws FHIRException, IOException { - // this is the main entry point; all the other entry points end up here coming here... - long t = System.nanoTime(); - boolean isRoot = false; - if (resourceProfilesMap == null) { - resourceProfilesMap = new HashMap(); - isRoot = true; - } - validateResource(appContext, errors, element, element, null, profiles, resourceIdRule, new NodeStack(element)); - if (isRoot) { - validateRemainder(appContext, errors); - resourceProfilesMap = null; - } - overall = System.nanoTime() - t; - } - - - private boolean check(String v1, String v2) { - return v1 == null ? Utilities.noString(v1) : v1.equals(v2); - } - - private void checkAddress(List errors, String path, Element focus, Address fixed) { - checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), "use", focus); - checkFixedValue(errors, path + ".text", focus.getNamedChild("text"), fixed.getTextElement(), "text", focus); - checkFixedValue(errors, path + ".city", focus.getNamedChild("city"), fixed.getCityElement(), "city", focus); - checkFixedValue(errors, path + ".state", focus.getNamedChild("state"), fixed.getStateElement(), "state", focus); - checkFixedValue(errors, path + ".country", focus.getNamedChild("country"), fixed.getCountryElement(), "country", focus); - checkFixedValue(errors, path + ".zip", focus.getNamedChild("zip"), fixed.getPostalCodeElement(), "postalCode", focus); - - 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", focus); - } - } - - private void checkAttachment(List errors, String path, Element focus, Attachment fixed) { - checkFixedValue(errors, path + ".contentType", focus.getNamedChild("contentType"), fixed.getContentTypeElement(), "contentType", focus); - checkFixedValue(errors, path + ".language", focus.getNamedChild("language"), fixed.getLanguageElement(), "language", focus); - checkFixedValue(errors, path + ".data", focus.getNamedChild("data"), fixed.getDataElement(), "data", focus); - checkFixedValue(errors, path + ".url", focus.getNamedChild("url"), fixed.getUrlElement(), "url", focus); - checkFixedValue(errors, path + ".size", focus.getNamedChild("size"), fixed.getSizeElement(), "size", focus); - checkFixedValue(errors, path + ".hash", focus.getNamedChild("hash"), fixed.getHashElement(), "hash", focus); - checkFixedValue(errors, path + ".title", focus.getNamedChild("title"), fixed.getTitleElement(), "title", focus); - } - - // public API - - private boolean checkCode(List errors, Element element, String path, String code, String system, String display) throws TerminologyServiceException { - long t = System.nanoTime(); - boolean ss = context.supportsSystem(system); - txTime = txTime + (System.nanoTime() - t); - if (ss) { - t = System.nanoTime(); - ValidationResult s = context.validateCode(system, code, display); - txTime = txTime + (System.nanoTime() - t); - 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 { - CodeSystem cs = getCodeSystem(system); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, cs != null, "Unknown Code System " + system)) { - ConceptDefinitionComponent def = getCodeDefinition(cs, 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, Element focus, CodeableConcept fixed) { - checkFixedValue(errors, path + ".text", focus.getNamedChild("text"), fixed.getTextElement(), "text", focus); - 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", focus); - } - } - - private void checkCodeableConcept(List errors, String path, Element element, StructureDefinition profile, ElementDefinition theElementCntext) { - if (!noTerminologyChecks && 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 valueset = resolveBindingReference(profile, binding.getValueSet(), profile.getUrl()); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null, "ValueSet " + describeReference(binding.getValueSet()) + " not found")) { - try { - CodeableConcept cc = readAsCodeableConcept(element); - if (!cc.hasCoding()) { - if (binding.getStrength() == BindingStrength.REQUIRED) - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "No code provided, and a code is required from the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl()); - else if (binding.getStrength() == BindingStrength.EXTENSIBLE) - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "No code provided, and a code should be provided from the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl()); - } else { - long t = System.nanoTime(); - - // Check whether the codes are appropriate for the type of binding we have - boolean bindingsOk = true; - if (binding.getStrength() != BindingStrength.EXAMPLE) { - boolean atLeastOneSystemIsSupported = false; - for (Coding nextCoding : cc.getCoding()) { - String nextSystem = nextCoding.getSystem(); - if (isNotBlank(nextSystem) && context.supportsSystem(nextSystem)) { - atLeastOneSystemIsSupported = true; - break; - } - } - - if (!atLeastOneSystemIsSupported && binding.getStrength() == BindingStrength.EXAMPLE) { - // ignore this since we can't validate but it doesn't matter.. - } else { - ValidationResult vr = context.validateCode(cc, valueset); - if (!vr.isOk()) { - bindingsOk = false; - if (vr.getErrorClass() != null && vr.getErrorClass().isInfrastructure()) { - if (binding.getStrength() == BindingStrength.REQUIRED) - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet()) + " and a code from this value set is required (class = "+vr.getErrorClass().toString()+")"); - else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { - if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) - checkMaxValueSet(errors, path, element, profile, (Reference) binding.getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet").get(0).getValue(), cc); - else - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet()) + " and a code should come from this value set unless it has no suitable code (class = "+vr.getErrorClass().toString()+")"); - } else if (binding.getStrength() == BindingStrength.PREFERRED) - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet()) + " and a code is recommended to come from this value set (class = "+vr.getErrorClass().toString()+")"); - } else { - if (binding.getStrength() == BindingStrength.REQUIRED) - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "None of the codes provided are in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl()+", and a code from this value set is required) (codes = "+ccSummary(cc)+")"); - else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { - if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) - checkMaxValueSet(errors, path, element, profile, (Reference) binding.getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet").get(0).getValue(), cc); - else - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "None of the codes provided are in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", and a code should come from this value set unless it has no suitable code) (codes = "+ccSummary(cc)+")"); - } else if (binding.getStrength() == BindingStrength.PREFERRED) - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "None of the codes provided are in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", and a code is recommended to come from this value set) (codes = "+ccSummary(cc)+")"); - } - } else if (vr.getMessage()!=null) - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, vr.getMessage()); - } - // Then, for any codes that are in code systems we are able - // to validate, we'll validate that the codes actually exist - if (bindingsOk) { - for (Coding nextCoding : cc.getCoding()) { - String nextCode = nextCoding.getCode(); - String nextSystem = nextCoding.getSystem(); - if (isNotBlank(nextCode) && isNotBlank(nextSystem) && context.supportsSystem(nextSystem)) { - ValidationResult vr = context.validateCode(nextSystem, nextCode, null); - if (!vr.isOk()) { - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Code {0} is not a valid code in code system {1}", nextCode, nextSystem); - } - } - } - } - txTime = txTime + (System.nanoTime() - t); - } - } - } catch (Exception e) { - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Error "+e.getMessage()+" validating CodeableConcept"); - } - } - } else if (binding.hasValueSet()) { - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding by URI reference cannot be checked"); - } else if (!noBindingMsgSuppressed) { - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding for path " + path + " has no source, so can't be checked"); - } - } - } - } - - private void checkMaxValueSet(List errors, String path, Element element, StructureDefinition profile, Reference maxVSUrl, CodeableConcept cc) { - // TODO Auto-generated method stub - ValueSet valueset = resolveBindingReference(profile, maxVSUrl, profile.getUrl()); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null, "ValueSet " + describeReference(maxVSUrl) + " not found")) { - try { - long t = System.nanoTime(); - ValidationResult vr = context.validateCode(cc, valueset); - txTime = txTime + (System.nanoTime() - t); - if (!vr.isOk()) { - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "None of the codes provided are in the maximum value set " + describeReference(maxVSUrl) + " (" + valueset.getUrl()+", and a code from this value set is required) (codes = "+ccSummary(cc)+")"); - } - } catch (Exception e) { - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Error "+e.getMessage()+" validating CodeableConcept using maxValueSet"); - } - } - } - - private void checkMaxValueSet(List errors, String path, Element element, StructureDefinition profile, Reference maxVSUrl, Coding c) { - // TODO Auto-generated method stub - ValueSet valueset = resolveBindingReference(profile, maxVSUrl, profile.getUrl()); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null, "ValueSet " + describeReference(maxVSUrl) + " not found")) { - try { - long t = System.nanoTime(); - ValidationResult vr = context.validateCode(c, valueset); - txTime = txTime + (System.nanoTime() - t); - if (!vr.isOk()) { - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "The code provided is not in the maximum value set " + describeReference(maxVSUrl) + " (" + valueset.getUrl()+", and a code from this value set is required) (code = "+c.getSystem()+"#"+c.getCode()+")"); - } - } catch (Exception e) { - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Error "+e.getMessage()+" validating CodeableConcept using maxValueSet"); - } - } - } - - private void checkMaxValueSet(List errors, String path, Element element, StructureDefinition profile, Reference maxVSUrl, String value) { - // TODO Auto-generated method stub - ValueSet valueset = resolveBindingReference(profile, maxVSUrl, profile.getUrl()); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null, "ValueSet " + describeReference(maxVSUrl) + " not found")) { - try { - long t = System.nanoTime(); - ValidationResult vr = context.validateCode(null, value, null, valueset); - txTime = txTime + (System.nanoTime() - t); - if (!vr.isOk()) { - if (vr.getErrorClass() != null && vr.getErrorClass().isInfrastructure()) - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "The code provided could not be validated against the maximum value set " + describeReference(maxVSUrl) + " (" + valueset.getUrl()+"), (error = "+vr.getMessage()+")"); - else - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "The code provided is not in the maximum value set " + describeReference(maxVSUrl) + " (" + valueset.getUrl()+"), and a code from this value set is required) (code = "+value+"), (error = "+vr.getMessage()+")"); - } - } catch (Exception e) { - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Error "+e.getMessage()+" validating CodeableConcept using maxValueSet"); - } - } - } - - private String ccSummary(CodeableConcept cc) { - CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); - for (Coding c : cc.getCoding()) - b.append(c.getSystem()+"#"+c.getCode()); - return b.toString(); - } - - private CodeableConcept readAsCodeableConcept(Element element) { - CodeableConcept cc = new CodeableConcept(); - List list = new ArrayList(); - element.getNamedChildren("coding", list); - for (Element item : list) - cc.addCoding(readAsCoding(item)); - cc.setText(element.getNamedChildValue("text")); - return cc; - } - - private Coding readAsCoding(Element item) { - Coding c = new Coding(); - c.setSystem(item.getNamedChildValue("system")); - c.setVersion(item.getNamedChildValue("version")); - c.setCode(item.getNamedChildValue("code")); - c.setDisplay(item.getNamedChildValue("display")); - return c; - } - - private void checkCoding(List errors, String path, Element focus, Coding fixed) { - checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), "system", focus); - checkFixedValue(errors, path + ".code", focus.getNamedChild("code"), fixed.getCodeElement(), "code", focus); - checkFixedValue(errors, path + ".display", focus.getNamedChild("display"), fixed.getDisplayElement(), "display", focus); - checkFixedValue(errors, path + ".userSelected", focus.getNamedChild("userSelected"), fixed.getUserSelectedElement(), "userSelected", focus); - } - - private void checkCoding(List errors, String path, Element element, StructureDefinition profile, ElementDefinition theElementCntext, boolean inCodeableConcept) { - 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 && !noTerminologyChecks) { - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, !isValueSet(system), "The Coding references a value set, not a code system (\""+system+"\")"); - try { - if (checkCode(errors, element, path, code, system, display)) - 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")) { - if (binding.hasValueSet() && binding.getValueSet() instanceof Reference) { - ValueSet valueset = resolveBindingReference(profile, binding.getValueSet(), profile.getUrl()); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null, "ValueSet " + describeReference(binding.getValueSet()) + " not found")) { - try { - Coding c = readAsCoding(element); - long t = System.nanoTime(); - ValidationResult vr = context.validateCode(c, valueset); - txTime = txTime + (System.nanoTime() - t); - if (!vr.isOk()) { - if (vr.IsNoService()) - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "The value provided could not be validated in the absence of a terminology server"); - else if (vr.getErrorClass() != null && !vr.getErrorClass().isInfrastructure()) { - if (binding.getStrength() == BindingStrength.REQUIRED) - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl()+", and a code from this value set is required)"); - else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { - if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) - checkMaxValueSet(errors, path, element, profile, (Reference) binding.getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet").get(0).getValue(), c); - else - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", and a code should come from this value set unless it has no suitable code)"); - } else if (binding.getStrength() == BindingStrength.PREFERRED) - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", and a code is recommended to come from this value set)"); - } else if (binding.getStrength() == BindingStrength.REQUIRED) - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "The Coding provided is not in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", and a code is required from this value set)"+(vr.getMessage() != null ? " (error message = "+vr.getMessage()+")" : "")); - else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { - if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) - checkMaxValueSet(errors, path, element, profile, (Reference) binding.getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet").get(0).getValue(), c); - else - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "The Coding provided is not in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", and a code should come from this value set unless it has no suitable code)"+(vr.getMessage() != null ? " (error message = "+vr.getMessage()+")" : "")); - } else if (binding.getStrength() == BindingStrength.PREFERRED) - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "The Coding provided is not in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", and a code is recommended to come from this value set)"+(vr.getMessage() != null ? " (error message = "+vr.getMessage()+")" : "")); - } - } catch (Exception e) { - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Error "+e.getMessage()+" validating Coding"); - } - } - } else if (binding.hasValueSet()) { - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding by URI reference cannot be checked"); - } else if (!inCodeableConcept && !noBindingMsgSuppressed) { - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding for path " + path + " has no source, so can't be checked"); - } - } - } - } catch (Exception e) { - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Error "+e.getMessage()+" validating Coding"); - } - } - } - - private boolean isValueSet(String url) { - try { - ValueSet vs = context.fetchResourceWithException(ValueSet.class, url); - return vs != null; - } catch (Exception e) { - return false; - } - } - - private void checkContactPoint(List errors, String path, Element focus, ContactPoint fixed) { - checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), "system", focus); - checkFixedValue(errors, path + ".value", focus.getNamedChild("value"), fixed.getValueElement(), "value", focus); - checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), "use", focus); - checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriod(), "period", focus); - - } - - protected void checkDeclaredProfiles(ResourceProfiles resourceProfiles, List errors, Element resource, Element element, NodeStack stack) throws FHIRException { - Element meta = element.getNamedChild("meta"); - if (meta != null) { - List profiles = new ArrayList(); - meta.getNamedChildren("profile", profiles); - int i = 0; - for (Element profile : profiles) { - String ref = profile.primitiveValue(); - 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")) { - long t = System.nanoTime(); - resourceProfiles.addProfile(errors, ref, errorForUnknownProfiles, p, element); - i++; - } - } - } - } - - private StructureDefinition checkExtension(Object appContext, List errors, String path, Element resource, Element element, ElementDefinition def, StructureDefinition profile, NodeStack stack) throws FHIRException, IOException { - String url = element.getNamedChildValue("url"); - boolean isModifier = element.getName().equals("modifierExtension"); - - long t = System.nanoTime(); - StructureDefinition ex = context.fetchResource(StructureDefinition.class, url); - sdTime = sdTime + (System.nanoTime() - t); - if (ex == null) { - if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, allowUnknownExtension(url), "The extension " + url + " is unknown, and not allowed here")) - hint(errors, IssueType.STRUCTURE, element.line(), element.col(), path, isKnownExtension(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)"); - - // check the type of the extension: - Set allowedTypes = listExtensionTypes(ex); - String actualType = getExtensionType(element); - if (actualType == null) - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", allowedTypes.isEmpty(), "The Extension '" + url + "' definition is for a complex extension, so it cannot contain a value"); - else - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", allowedTypes.contains(actualType), "The Extension '" + url + "' definition allows for the types "+allowedTypes.toString()+" but found type "+actualType); - - // 3. is the content of the extension valid? - validateElement(appContext, errors, ex, ex.getSnapshot().getElement().get(0), null, null, resource, element, "Extension", stack, false); - - } - return ex; - } - - private String getExtensionType(Element element) { - for (Element e : element.getChildren()) { - if (e.getName().startsWith("value")) { - String tn = e.getName().substring(5); - String ltn = Utilities.uncapitalize(tn); - if (isPrimitiveType(ltn)) - return ltn; - else - return tn; - } - } - return null; - } - - private Set listExtensionTypes(StructureDefinition ex) { - ElementDefinition vd = null; - for (ElementDefinition ed : ex.getSnapshot().getElement()) { - if (ed.getPath().startsWith("Extension.value")) { - vd = ed; - break; - } - } - Set res = new HashSet(); - if (vd != null && !"0".equals(vd.getMax())) { - for (TypeRefComponent tr : vd.getType()) { - res.add(tr.getCode()); - } - } - return res; - } - - private boolean checkExtensionContext(List errors, Element 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.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, Element focus, org.hl7.fhir.dstu3.model.Element fixed, String propName, Element parent) { - if ((fixed == null || fixed.isEmpty()) && 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 && !fixed.isEmpty() && focus == null) - rule(errors, IssueType.VALUE, parent == null ? -1 : parent.line(), parent == null ? -1 : parent.col(), path, false, "Missing element '" + propName+"'"); - else { - String value = focus.primitiveValue(); - if (fixed instanceof org.hl7.fhir.dstu3.model.BooleanType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.BooleanType) fixed).asStringValue(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.model.BooleanType) fixed).asStringValue() + "'"); - else if (fixed instanceof org.hl7.fhir.dstu3.model.IntegerType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.IntegerType) fixed).asStringValue(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.model.IntegerType) fixed).asStringValue() + "'"); - else if (fixed instanceof org.hl7.fhir.dstu3.model.DecimalType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.DecimalType) fixed).asStringValue(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.model.DecimalType) fixed).asStringValue() + "'"); - else if (fixed instanceof org.hl7.fhir.dstu3.model.Base64BinaryType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.Base64BinaryType) fixed).asStringValue(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.model.Base64BinaryType) fixed).asStringValue() + "'"); - else if (fixed instanceof org.hl7.fhir.dstu3.model.InstantType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.InstantType) fixed).getValue().toString(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.model.InstantType) fixed).asStringValue() + "'"); - else if (fixed instanceof org.hl7.fhir.dstu3.model.StringType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.StringType) fixed).getValue(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.model.StringType) fixed).getValue() + "'"); - else if (fixed instanceof org.hl7.fhir.dstu3.model.UriType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.UriType) fixed).getValue(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.model.UriType) fixed).getValue() + "'"); - else if (fixed instanceof org.hl7.fhir.dstu3.model.DateType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.DateType) fixed).getValue().toString(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.model.DateType) fixed).getValue() + "'"); - else if (fixed instanceof org.hl7.fhir.dstu3.model.DateTimeType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.DateTimeType) fixed).getValue().toString(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.model.DateTimeType) fixed).getValue() + "'"); - else if (fixed instanceof org.hl7.fhir.dstu3.model.OidType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.OidType) fixed).getValue(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.model.OidType) fixed).getValue() + "'"); - else if (fixed instanceof org.hl7.fhir.dstu3.model.UuidType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.UuidType) fixed).getValue(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.model.UuidType) fixed).getValue() + "'"); - else if (fixed instanceof org.hl7.fhir.dstu3.model.CodeType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.CodeType) fixed).getValue(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.model.CodeType) fixed).getValue() + "'"); - else if (fixed instanceof org.hl7.fhir.dstu3.model.IdType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.IdType) fixed).getValue(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.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, as the specified fixed value doesn't contain any extensions"); - } 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()) { - Element 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.getNamedChild("extension").getNamedChild("value"), e.getValue(), "extension.value", ex.getNamedChild("extension")); - } - } - } - } - } - - private void checkHumanName(List errors, String path, Element focus, HumanName fixed) { - checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), "use", focus); - checkFixedValue(errors, path + ".text", focus.getNamedChild("text"), fixed.getTextElement(), "text", focus); - checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriod(), "period", focus); - - List parts = new ArrayList(); - focus.getNamedChildren("family", parts); - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() > 0 == fixed.hasFamily(), - "Expected " + (fixed.hasFamily() ? "1" : "0") + " but found " + Integer.toString(parts.size()) + " family elements")) { - for (int i = 0; i < parts.size(); i++) - checkFixedValue(errors, path + ".family", parts.get(i), fixed.getFamilyElement(), "family", focus); - } - 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); - } - 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); - } - 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", focus); - } - } - - private void checkIdentifier(List errors, String path, Element 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, Element focus, Identifier fixed) { - checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), "use", focus); - checkFixedValue(errors, path + ".type", focus.getNamedChild("type"), fixed.getType(), "type", focus); - checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), "system", focus); - checkFixedValue(errors, path + ".value", focus.getNamedChild("value"), fixed.getValueElement(), "value", focus); - checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriod(), "period", focus); - checkFixedValue(errors, path + ".assigner", focus.getNamedChild("assigner"), fixed.getAssigner(), "assigner", focus); - } - - private void checkPeriod(List errors, String path, Element focus, Period fixed) { - checkFixedValue(errors, path + ".start", focus.getNamedChild("start"), fixed.getStartElement(), "start", focus); - checkFixedValue(errors, path + ".end", focus.getNamedChild("end"), fixed.getEndElement(), "end", focus); - } - - private void checkPrimitive(Object appContext, List errors, String path, String type, ElementDefinition context, Element e, StructureDefinition profile) throws FHIRException, IOException { - if (isBlank(e.primitiveValue())) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.hasChildren(), "primitive types must have a value or must have child extensions"); - return; - } - String regex = context.getExtensionString(ToolingExtensions.EXT_REGEX); - if (regex!=null) - rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue().matches(regex), "Element value '" + e.primitiveValue() + "' does not meet regex '" + regex + "'"); - - if (type.equals("boolean")) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, "true".equals(e.primitiveValue()) || "false".equals(e.primitiveValue()), "boolean values must be 'true' or 'false'"); - } - if (type.equals("uri")) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, !e.primitiveValue().startsWith("oid:"), "URI values cannot start with oid:"); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, !e.primitiveValue().startsWith("uuid:"), "URI values cannot start with uuid:"); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue().equals(e.primitiveValue().trim()), "URI values cannot have leading or trailing whitespace"); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxLength() || context.getMaxLength()==0 || e.primitiveValue().length() <= context.getMaxLength(), "value is longer than permitted maximum length of " + context.getMaxLength()); - - // now, do we check the URI target? - if (fetcher != null) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, fetcher.resolveURL(appContext, path, e.primitiveValue()), "URL value '"+e.primitiveValue()+"' does not resolve"); - } - } - if (type.equalsIgnoreCase("string") && e.hasPrimitiveValue()) { - if (rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue() == null || e.primitiveValue().length() > 0, "@value cannot be empty")) { - warning(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue() == null || e.primitiveValue().trim().equals(e.primitiveValue()), "value should not start or finish with whitespace"); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxLength() || context.getMaxLength()==0 || e.primitiveValue().length() <= context.getMaxLength(), "value is longer than permitted maximum length of " + context.getMaxLength()); - } - } - if (type.equals("dateTime")) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, yearIsValid(e.primitiveValue()), "The value '" + e.primitiveValue() + "' does not have a valid year"); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, - e.primitiveValue() - .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.primitiveValue()) || hasTimeZone(e.primitiveValue()), "if a date has a time, it must have a timezone"); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxLength() || context.getMaxLength()==0 || e.primitiveValue().length() <= context.getMaxLength(), "value is longer than permitted maximum length of " + context.getMaxLength()); - } - if (type.equals("date")) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, yearIsValid(e.primitiveValue()), "The value '" + e.primitiveValue() + "' does not have a valid year"); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue().matches("-?[0-9]{4}(-(0[1-9]|1[0-2])(-(0[0-9]|[1-2][0-9]|3[0-1]))?)?"), - "Not a valid date"); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxLength() || context.getMaxLength()==0 || e.primitiveValue().length() <= context.getMaxLength(), "value is longer than permitted maximum value of " + context.getMaxLength()); - } - if (type.equals("integer")) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxValueIntegerType() || !context.getMaxValueIntegerType().hasValue() || (context.getMaxValueIntegerType().getValue() >= new Integer(e.getValue()).intValue()), "value is greater than permitted maximum value of " + (context.hasMaxValueIntegerType() ? context.getMaxValueIntegerType() : "")); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMinValueIntegerType() || !context.getMinValueIntegerType().hasValue() || (context.getMinValueIntegerType().getValue() <= new Integer(e.getValue()).intValue()), "value is less than permitted minimum value of " + (context.hasMinValueIntegerType() ? context.getMinValueIntegerType() : "")); - } - if (type.equals("instant")) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, - e.primitiveValue().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.primitiveValue() + "' is not valid (by regex)"); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, yearIsValid(e.primitiveValue()), "The value '" + e.primitiveValue() + "' does not have a valid year"); - } - - if (type.equals("code") && e.primitiveValue() != null) { - // 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.primitiveValue()), "The code '" + e.primitiveValue() + "' is not valid (whitespace rules)"); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxLength() || context.getMaxLength()==0 || e.primitiveValue().length() <= context.getMaxLength(), "value is longer than permitted maximum length of " + context.getMaxLength()); - } - - if (context.hasBinding() && e.primitiveValue() != null) { - checkPrimitiveBinding(errors, path, type, context, e, profile); - } - - if (type.equals("xhtml")) { - XhtmlNode xhtml = e.getXhtml(); - if (xhtml != null) { // if it is null, this is an error already noted in the parsers - // check that the namespace is there and correct. - String ns = xhtml.getNsDecl(); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, FormatUtilities.XHTML_NS.equals(ns), "Wrong namespace on the XHTML ('"+ns+"')"); - // check that inner namespaces are all correct - checkInnerNS(errors, e, path, xhtml.getChildNodes()); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, "div".equals(xhtml.getName()), "Wrong name on the XHTML ('"+ns+"') - must start with div"); - // check that no illegal elements and attributes have been used - checkInnerNames(errors, e, path, xhtml.getChildNodes()); - } - } - - if (context.hasFixed()) - checkFixedValue(errors,path,e, context.getFixed(), context.getSliceName(), null); - - // for nothing to check - } - - private void checkInnerNames(List errors, Element e, String path, List list) { - for (XhtmlNode node : list) { - if (node.getNodeType() == NodeType.Element) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, Utilities.existsInList(node.getName(), - "p", "br", "div", "h1", "h2", "h3", "h4", "h5", "h6", "a", "span", "b", "em", "i", "strong", - "small", "big", "tt", "small", "dfn", "q", "var", "abbr", "acronym", "cite", "blockquote", "hr", "address", "bdo", "kbd", "q", "sub", "sup", - "ul", "ol", "li", "dl", "dt", "dd", "pre", "table", "caption", "colgroup", "col", "thead", "tr", "tfoot", "tbody", "th", "td", - "code", "samp", "img", "map", "area" - - ), "Illegal element name in the XHTML ('"+node.getName()+"')"); - for (String an : node.getAttributes().keySet()) { - boolean ok = an.startsWith("xmlns") || Utilities.existsInList(an, - "title", "style", "class", "id", "lang", "xml:lang", "dir", "accesskey", "tabindex", - // tables - "span", "width", "align", "valign", "char", "charoff", "abbr", "axis", "headers", "scope", "rowspan", "colspan") || - - Utilities.existsInList(node.getName()+"."+an, "a.href", "a.name", "img.src", "img.border", "div.xmlns", "blockquote.cite", "q.cite", - "a.charset", "a.type", "a.name", "a.href", "a.hreflang", "a.rel", "a.rev", "a.shape", "a.coords", "img.src", - "img.alt", "img.longdesc", "img.height", "img.width", "img.usemap", "img.ismap", "map.name", "area.shape", - "area.coords", "area.href", "area.nohref", "area.alt", "table.summary", "table.width", "table.border", - "table.frame", "table.rules", "table.cellspacing", "table.cellpadding", "pre.space" - ); - if (!ok) - rule(errors, IssueType.INVALID, e.line(), e.col(), path, false, "Illegal attribute name in the XHTML ('"+an+"' on '"+node.getName()+"')"); - } - checkInnerNames(errors, e, path, node.getChildNodes()); - } - } - } - - private void checkInnerNS(List errors, Element e, String path, List list) { - for (XhtmlNode node : list) { - if (node.getNodeType() == NodeType.Element) { - String ns = node.getNsDecl(); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, ns == null || FormatUtilities.XHTML_NS.equals(ns), "Wrong namespace on the XHTML ('"+ns+"')"); - checkInnerNS(errors, e, path, node.getChildNodes()); - } - } - } - - private void checkPrimitiveBinding(List errors, String path, String type, ElementDefinition elementContext, Element element, StructureDefinition profile) { - // We ignore bindings that aren't on string, uri or code - if (!element.hasPrimitiveValue() || !("code".equals(type) || "string".equals(type) || "uri".equals(type))) { - return; - } - if (noTerminologyChecks) - return; - - String value = element.primitiveValue(); - // System.out.println("check "+value+" in "+path); - - // firstly, resolve the value set - ElementDefinitionBindingComponent binding = elementContext.getBinding(); - if (binding.hasValueSet() && binding.getValueSet() instanceof Reference) { - ValueSet vs = resolveBindingReference(profile, binding.getValueSet(), profile.getUrl()); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, vs != null, "ValueSet {0} not found", describeReference(binding.getValueSet()))) { - long t = System.nanoTime(); - ValidationResult vr = context.validateCode(null, value, null, vs); - txTime = txTime + (System.nanoTime() - t); - if (vr != null && !vr.isOk()) { - if (vr.IsNoService()) - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "The value provided ('"+value+"') could not be validated in the absence of a terminology server"); - else if (binding.getStrength() == BindingStrength.REQUIRED) - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "The value provided ('"+value+"') is not in the value set " + describeReference(binding.getValueSet()) + " (" + vs.getUrl() + ", and a code is required from this value set)"+(vr.getMessage() != null ? " (error message = "+vr.getMessage()+")" : "")); - else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { - if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) - checkMaxValueSet(errors, path, element, profile, (Reference) binding.getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet").get(0).getValue(), value); - else - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "The value provided ('"+value+"') is not in the value set " + describeReference(binding.getValueSet()) + " (" + vs.getUrl() + ", and a code should come from this value set unless it has no suitable code)"+(vr.getMessage() != null ? " (error message = "+vr.getMessage()+")" : "")); - } else if (binding.getStrength() == BindingStrength.PREFERRED) - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "The value provided ('"+value+"') is not in the value set " + describeReference(binding.getValueSet()) + " (" + vs.getUrl() + ", and a code is recommended to come from this value set)"+(vr.getMessage() != null ? " (error message = "+vr.getMessage()+")" : "")); - } - } - } else if (!noBindingMsgSuppressed) - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, !type.equals("code"), "Binding has no source, so can't be checked"); - } - - private void checkQuantity(List errors, String path, Element focus, Quantity fixed) { - checkFixedValue(errors, path + ".value", focus.getNamedChild("value"), fixed.getValueElement(), "value", focus); - checkFixedValue(errors, path + ".comparator", focus.getNamedChild("comparator"), fixed.getComparatorElement(), "comparator", focus); - checkFixedValue(errors, path + ".units", focus.getNamedChild("unit"), fixed.getUnitElement(), "units", focus); - checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), "system", focus); - checkFixedValue(errors, path + ".code", focus.getNamedChild("code"), fixed.getCodeElement(), "code", focus); - } - - // implementation - - private void checkRange(List errors, String path, Element focus, Range fixed) { - checkFixedValue(errors, path + ".low", focus.getNamedChild("low"), fixed.getLow(), "low", focus); - checkFixedValue(errors, path + ".high", focus.getNamedChild("high"), fixed.getHigh(), "high", focus); - - } - - private void checkRatio(List errors, String path, Element focus, Ratio fixed) { - checkFixedValue(errors, path + ".numerator", focus.getNamedChild("numerator"), fixed.getNumerator(), "numerator", focus); - checkFixedValue(errors, path + ".denominator", focus.getNamedChild("denominator"), fixed.getDenominator(), "denominator", focus); - } - - private Reference readAsReference(Element item) { - Reference r = new Reference(); - r.setDisplay(item.getNamedChildValue("display")); - r.setReference(item.getNamedChildValue("reference")); - List identifier = item.getChildrenByName("identifier"); - if (identifier.isEmpty() == false) { - r.setIdentifier(readAsIdentifier(identifier.get(0))); - } - return r; - } - private Identifier readAsIdentifier(Element item) { - Identifier r = new Identifier(); - r.setSystem(item.getNamedChildValue("system")); - r.setValue(item.getNamedChildValue("value")); - return r; - } - - private void checkReference(Object appContext, List errors, String path, Element element, StructureDefinition profile, ElementDefinition container, String parentType, NodeStack stack) throws FHIRException, IOException { - Reference reference = readAsReference(element); - - String ref = reference.getReference(); - if (Utilities.noString(ref)) { - if (Utilities.noString(reference.getIdentifier().getSystem()) && Utilities.noString(reference.getIdentifier().getValue())) { - warning(errors, IssueType.STRUCTURE, element.line(), element.col(), path, !Utilities.noString(element.getNamedChildValue("display")), "A Reference without an actual reference or identifier should have a display"); - } - return; - } - - Element we = localResolve(ref, stack, errors, path); - String refType; - if (ref.startsWith("#")) { - refType = "contained"; - } else { - if (we == null) { - refType = "remote"; - } else { - refType = "bundle"; - } - } - String ft; - if (we != null) - ft = we.getType(); - else - ft = tryParse(ref); - ReferenceValidationPolicy pol = refType.equals("contained") ? ReferenceValidationPolicy.CHECK_VALID : fetcher == null ? ReferenceValidationPolicy.IGNORE : fetcher.validationPolicy(appContext, path, ref); - - if (pol.checkExists()) { - if (we == null) { - if (fetcher == null) - throw new FHIRException("Resource resolution services not provided"); - we = fetcher.fetch(appContext, ref); - } - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, we != null, "Unable to resolve resource '"+ref+"'"); - } - - if (we != null && pol.checkType()) { - if (warning(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.hasTargetProfile() || type.getTargetProfile().equals("http://hl7.org/fhir/StructureDefinition/Resource")) - ok = true; - else { - String pr = type.getTargetProfile(); - - String bt = getBaseType(profile, pr); - StructureDefinition sd = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + bt); - 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); - if (ok && we!=null && pol.checkValid()) { - doResourceProfile(appContext, we, pr, errors, stack.push(we, -1, null, null), path, element); - } - } else - ok = true; // suppress following check - if (ok && type.hasAggregation()) { - boolean modeOk; - for (Enumeration mode : type.getAggregation()) { - if (mode.getValue().equals(AggregationMode.CONTAINED) && refType.equals("contained")) - ok = true; - else if (mode.getValue().equals(AggregationMode.BUNDLED) && refType.equals("bundled")) - ok = true; - else if (mode.getValue().equals(AggregationMode.REFERENCED) && (refType.equals("bundled")||refType.equals("remote"))) - ok = true; - } - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, ok, "Reference is " + refType + " which isn't supported by the specified aggregation mode(s) for the reference"); - } - } - } - 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() + ")"); - } - } - if (pol == ReferenceValidationPolicy.CHECK_VALID) { - // todo.... - } - } - - private void doResourceProfile(Object appContext, Element resource, String profile, List errors, NodeStack stack, String path, Element element) throws FHIRException, IOException { - ResourceProfiles resourceProfiles = addResourceProfile(errors, resource, profile, path, element, stack); - if (resourceProfiles.isProcessed()) { - start(appContext, errors, resource, resource, null, stack); - } - } - - private ResourceProfiles getResourceProfiles(Element resource, NodeStack stack) { - ResourceProfiles resourceProfiles = resourceProfilesMap.get(resource); - if (resourceProfiles==null) { - resourceProfiles = new ResourceProfiles(resource, stack); - resourceProfilesMap.put(resource, resourceProfiles); - } - return resourceProfiles; - } - - private ResourceProfiles addResourceProfile(List errors, Element resource, String profile, String path, Element element, NodeStack stack) { - ResourceProfiles resourceProfiles = getResourceProfiles(resource, stack); - resourceProfiles.addProfile(errors, profile, errorForUnknownProfiles, path, element); - return resourceProfiles; - } - - private String checkResourceType(String type) { - long t = System.nanoTime(); - try { - if (context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + type) != null) - return type; - else - return null; - } finally { - sdTime = sdTime + (System.nanoTime() - t); - } - } - - private void checkSampledData(List errors, String path, Element focus, SampledData fixed) { - checkFixedValue(errors, path + ".origin", focus.getNamedChild("origin"), fixed.getOrigin(), "origin", focus); - checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriodElement(), "period", focus); - checkFixedValue(errors, path + ".factor", focus.getNamedChild("factor"), fixed.getFactorElement(), "factor", focus); - checkFixedValue(errors, path + ".lowerLimit", focus.getNamedChild("lowerLimit"), fixed.getLowerLimitElement(), "lowerLimit", focus); - checkFixedValue(errors, path + ".upperLimit", focus.getNamedChild("upperLimit"), fixed.getUpperLimitElement(), "upperLimit", focus); - checkFixedValue(errors, path + ".dimensions", focus.getNamedChild("dimensions"), fixed.getDimensionsElement(), "dimensions", focus); - checkFixedValue(errors, path + ".data", focus.getNamedChild("data"), fixed.getDataElement(), "data", focus); - } - - private void checkTiming(List errors, String path, Element focus, Timing fixed) { - checkFixedValue(errors, path + ".repeat", focus.getNamedChild("repeat"), fixed.getRepeat(), "value", focus); - - 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", focus); - } - } - - 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(); - } - - protected ElementDefinition findElement(StructureDefinition profile, String name) { - for (ElementDefinition c : profile.getSnapshot().getElement()) { - if (c.getPath().equals(name)) { - return c; - } - } - return null; - } - - public BestPracticeWarningLevel getBasePracticeWarningLevel() { - return bpWarnings; - } - - private String getBaseType(StructureDefinition profile, String pr) { - StructureDefinition p = resolveProfile(profile, pr); - if (p == null) - return null; - else - return p.getType(); - } - - @Override - public CheckDisplayOption getCheckDisplay() { - return checkDisplay; - } - - // private String findProfileTag(Element element) { - // String uri = null; - // List list = new ArrayList(); - // element.getNamedChildren("category", list); - // for (Element 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(CodeSystem cs, String code) { - for (ConceptDefinitionComponent c : cs.getConcept()) { - ConceptDefinitionComponent r = getCodeDefinition(c, code); - if (r != null) - return r; - } - return null; - } - - private Element getContainedById(Element container, String id) { - List contained = new ArrayList(); - container.getNamedChildren("contained", contained); - for (Element we : contained) { - if (id.equals(we.getNamedChildValue("id"))) - return we; - } - return null; - } - - public IWorkerContext getContext() { - return context; - } - - private ElementDefinition getCriteriaForDiscriminator(String path, ElementDefinition element, String discriminator, StructureDefinition profile) throws DefinitionException { - StructureDefinition sd = profile; - ElementDefinition ed = element; - - if ("value".equals(discriminator) && element.hasFixed()) - return element; - - List nodes = new ArrayList(); - Matcher matcher = Pattern.compile("\\.([a-zA-Z0-0]+(\\([^\\(^\\)]*\\))?)").matcher("."+discriminator); - while (matcher.find()) { - if (!matcher.group(1).startsWith("@")) - nodes.add(matcher.group(1)); - } - - for (String fullnode : nodes) { - String node = fullnode.contains("(") ? fullnode.substring(0, fullnode.indexOf('(')) : fullnode; - String qualifier = fullnode.contains("(") ? fullnode.substring(fullnode.indexOf('(') + 2, fullnode.length()-2) : null; - if (qualifier!=null && !node.equals("extension")) - throw new DefinitionException("Function specified other than 'extension()': " + discriminator); - // get the children of element - List childDefinitions; - childDefinitions = ProfileUtilities.getChildMap(sd, ed); - // if that's empty, get the children of the type - if (childDefinitions.isEmpty()) { - if (ed.getType().size() == 0) - throw new DefinitionException("Error in profile for " + path + " no children, no type"); - if (ed.getType().size() > 1) - throw new DefinitionException("Error in profile for " + path + " multiple types defined in slice discriminator"); - if (ed.hasSlicing()) { - List slices = ProfileUtilities.getSliceList(profile, ed); - boolean found = false; - for (ElementDefinition sed: slices) { - childDefinitions = ProfileUtilities.getChildMap(sd, sed); - for (ElementDefinition t : childDefinitions) { - if (tailMatches(t, node)) { - found = true; - if (qualifier!=null) { - List childNodes = ProfileUtilities.getChildList(profile, t); - if (childNodes.isEmpty()) { - if (t.getType().size()==1 && t.getType().get(0).hasProfile() && t.getType().get(0).getProfile().equals(qualifier)) { - found = true; - break; - } - - } else { - for (ElementDefinition c : childNodes) { - if (c.getPath().endsWith(".url") && c.hasFixed() && c.getFixed() instanceof UriType && ((UriType)c.getFixed()).equals(qualifier)) { - found = true; - break; - } - } - } - } - if (found) { - ed = t; - break; - } - } - } - } - if (found) - continue; - } - if (ed.getType().get(0).hasProfile() && ed.getType().get(0).getCode().equals("Reference") && node.equals("reference")) { - long t1 = System.nanoTime(); - sd = context.fetchResource(StructureDefinition.class, ed.getType().get(0).getTargetProfile()); - sdTime = sdTime + (System.nanoTime() - t1); - // we've skipped the reference. - ed = sd.getSnapshot().getElementFirstRep(); - continue; - } else if (ed.getType().get(0).hasProfile()) { - long t2 = System.nanoTime(); - sd = context.fetchResource(StructureDefinition.class, ed.getType().get(0).getProfile()); - sdTime = sdTime + (System.nanoTime() - t2); - } else { - long t2 = System.nanoTime(); - sd = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + ed.getType().get(0).getCode()); - sdTime = sdTime + (System.nanoTime() - t2); - } - if (sd == null) - throw new DefinitionException("Error in profile for " + path + ": unable to find type for "+ed.getId()); - childDefinitions = ProfileUtilities.getChildMap(sd, sd.getSnapshot().getElementFirstRep()); - } - // find a single match. (no slicing!) - boolean found = false; - for (ElementDefinition t : childDefinitions) { - if (tailMatches(t, node)) { - found = true; - if (qualifier!=null) { - found = false; - List childNodes = ProfileUtilities.getChildList(profile, t); - if (childNodes.isEmpty()) { - if (t.getType().size()==1 && t.getType().get(0).hasProfile() && t.getType().get(0).getProfile().equals(qualifier)) { - found = true; - ed = t; - break; - } - - } else { - for (ElementDefinition c : childNodes) { - if (c.getPath().endsWith(".url") && c.hasFixed() && c.getFixed() instanceof UriType && ((UriType)c.getFixed()).equals(qualifier)) { - found = true; - ed = t; - break; - } - } - } - } - if (found) { - ed = t; - break; - } - } - } - if (!found) - throw new DefinitionException("Unable to find definition for discriminator "+discriminator+" from "+path+" in "+profile.getUrl()); - } - return ed; - // List snapshot = null; - // int index; - // if (childDefinitions.isEmpty()) { - // // going to look at the type - // if (ed.getType().size() == 0) - // throw new DefinitionException("Error in profile for " + path + " no children, no type"); - // snapshot = type.getSnapshot().getElement(); - // ed = snapshot.get(0); - // index = 0; - // } else { - // snapshot = childDefinitions; - // index = -1; - // } - // String originalPath = ed.getPath(); - // String goal = originalPath + "." + discriminator; - // - // 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+" in "+profile.getUrl()); - } - - private boolean tailMatches(ElementDefinition t, String d) { - String tail = tail(t.getPath()); - if (d.contains("[")) - return tail.startsWith(d.substring(0, d.indexOf('['))); - else - return tail.equals(d); - } - - private Element getExtensionByUrl(List extensions, String urlSimple) { - for (Element e : extensions) { - if (urlSimple.equals(e.getNamedChildValue("url"))) - return e; - } - return null; - } - - public List getExtensionDomains() { - return extensionDomains; - } - - private Element getFromBundle(Element bundle, String ref, String fullUrl, List errors, String path) { - String targetUrl = null; - String version = ""; - if (ref.startsWith("http") || ref.startsWith("urn")) { - // We've got an absolute reference, no need to calculate - if (ref.contains("/_history/")) { - targetUrl = ref.substring(0, ref.indexOf("/_history/") - 1); - version = ref.substring(ref.indexOf("/_history/") + 10); - } else - targetUrl = ref; - - } else if (fullUrl == null) { - //This isn't a problem for signatures - if it's a signature, we won't have a resolution for a relative reference. For anything else, this is an error - rule(errors, IssueType.REQUIRED, -1, -1, path, path.startsWith("Bundle.signature"), "Relative Reference appears inside Bundle whose entry is missing a fullUrl"); - return null; - - } else if (ref.split("/").length!=2) { - rule(errors, IssueType.INVALID, -1, -1, path, false, "Relative URLs must be of the format [ResourceName]/[id]. Encountered " + ref); - return null; - - } else { - String base = ""; - if (fullUrl.startsWith("urn")) { - String[] parts = fullUrl.split("\\:"); - for (int i=0; i < parts.length-1; i++) { - base = base + parts[i] + ":"; - } - } else { - String[] parts; - parts = fullUrl.split("/"); - for (int i=0; i < parts.length-2; i++) { - base = base + parts[i] + "/"; - } - } - - String id = null; - if (ref.contains("/_history/")) { - version = ref.substring(ref.indexOf("/_history/") + 10); - id = ref.substring(0, ref.indexOf("/history/")-1); - } else if (base.startsWith("urn")) - id = ref.split("/")[1]; - else - id = ref; - - targetUrl = base + id; - } - - List entries = new ArrayList(); - bundle.getNamedChildren("entry", entries); - Element match = null; - for (Element we : entries) { - if (we.getChildValue("fullUrl").equals(targetUrl)) { - Element r = we.getNamedChild("resource"); - if (version.isEmpty()) { - rule(errors, IssueType.FORBIDDEN, -1, -1, path, match==null, "Multiple matches in bundle for reference " + ref); - match = r; - } else { - try { - if (r.getChildren("meta").get(0).getChildValue("versionId").equals(version)) { - rule(errors, IssueType.FORBIDDEN, -1, -1, path, match==null, "Multiple matches in bundle for reference " + ref); - match = r; - } - } catch (Exception e) { - warning(errors, IssueType.REQUIRED, -1, -1, path, r.getChildren("meta").size()==1 && r.getChildren("meta").get(0).getChildValue("versionId")!=null, "Entries matching fullURL " + targetUrl + " should declare meta/versionId because there are version-specific references"); - // If one of these things is null - } - } - } - } - - warning(errors, IssueType.REQUIRED, -1, -1, path, match!=null || !targetUrl.startsWith("urn"), "URN reference is not locally contained within the bundle " + ref); - return match; - } - - private StructureDefinition getProfileForType(String type) { - if (logical != null) - for (BundleEntryComponent be : logical.getEntry()) { - if (be.hasResource() && be.getResource() instanceof StructureDefinition) { - StructureDefinition sd = (StructureDefinition) be.getResource(); - if (sd.getId().equals(type)) - return sd; - } - } - - long t = System.nanoTime(); - try { - return context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + type); - } finally { - sdTime = sdTime + (System.nanoTime() - t); - } - } - - private Element getValueForDiscriminator(Object appContext, List errors, Element element, String discriminator, ElementDefinition criteria, NodeStack stack) throws FHIRException, IOException { - String p = stack.getLiteralPath()+"."+element.getName(); - Element focus = element; - String[] dlist = discriminator.split("\\."); - for (String d : dlist) { - if (focus.fhirType().equals("Reference") && d.equals("reference")) { - String url = focus.getChildValue("reference"); - if (Utilities.noString(url)) - throw new FHIRException("No reference resolving discriminator "+discriminator+" from "+element.getProperty().getName()); - // Note that we use the passed in stack here. This might be a problem if the discriminator is deep enough? - Element target = resolve(appContext, url, stack, errors, p); - if (target == null) - throw new FHIRException("Unable to find resource "+url+" at "+d+" resolving discriminator "+discriminator+" from "+element.getProperty().getName()); - focus = target; - } else if (d.equals("value") && focus.isPrimitive()) { - return focus; - } else { - List children = focus.getChildren(d); - if (children.isEmpty()) - throw new FHIRException("Unable to find "+d+" resolving discriminator "+discriminator+" from "+element.getProperty().getName()); - if (children.size() > 1) - throw new FHIRException("Found "+Integer.toString(children.size())+" items for "+d+" resolving discriminator "+discriminator+" from "+element.getProperty().getName()); - focus = children.get(0); - p = p + "."+d; - } - } - return focus; - } - - private CodeSystem getCodeSystem(String system) { - long t = System.nanoTime(); - try { - return context.fetchCodeSystem(system); - } finally { - txTime = txTime + (System.nanoTime() - t); - } - } - - 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:") || uri.startsWith("urn:iso-astm:") || isValidFHIRUrn(uri); - } - - private boolean isValidFHIRUrn(String uri) { - return (uri.equals("urn:x-fhir:uk:id:nhs-number")) || uri.startsWith("urn:"); // Anyone can invent a URN, so why should we complain? - } - - public boolean isAnyExtensionsAllowed() { - return anyExtensionsAllowed; - } - - public boolean isErrorForUnknownProfiles() { - return errorForUnknownProfiles; - } - - public void setErrorForUnknownProfiles(boolean errorForUnknownProfiles) { - this.errorForUnknownProfiles = errorForUnknownProfiles; - } - - private boolean isParametersEntry(String path) { - String[] parts = path.split("\\."); - return parts.length > 2 && parts[parts.length - 1].equals("resource") && (pathEntryHasName(parts[parts.length - 2], "parameter") || pathEntryHasName(parts[parts.length - 2], "part")); - } - - private boolean isBundleEntry(String path) { - String[] parts = path.split("\\."); - return parts.length > 2 && parts[parts.length - 1].equals("resource") && pathEntryHasName(parts[parts.length - 2], "entry"); - } - - private boolean isBundleOutcome(String path) { - String[] parts = path.split("\\."); - return parts.length > 2 && parts[parts.length - 1].equals("outcome") && pathEntryHasName(parts[parts.length - 2], "response"); - } - - - private static boolean pathEntryHasName(String thePathEntry, String theName) { - if (thePathEntry.equals(theName)) { - return true; - } - if (thePathEntry.length() >= theName.length() + 3) { - if (thePathEntry.startsWith(theName)) { - if (thePathEntry.charAt(theName.length()) == '[') { - return true; - } - } - } - return false; - } - - private boolean isPrimitiveType(String code) { - StructureDefinition sd = context.fetchTypeDefinition(code); - return sd != null && sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE; - } - - - - 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 Element localResolve(String ref, NodeStack stack, List errors, String path) { - 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) { - if (stack.getElement().getProperty().isResource()) { - // ok, we'll try to find the contained reference - Element res = getContainedById(stack.getElement(), ref.substring(1)); - if (res != null) - return res; - } - if (stack.getElement().getSpecial() == SpecialElement.BUNDLE_ENTRY) { - return null; // we don't try to resolve contained references across this boundary - } - 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 - String fullUrl = null; // we're going to try to work this out as we go up - while (stack != null && stack.getElement() != null) { - if (stack.getElement().getSpecial() == SpecialElement.BUNDLE_ENTRY && fullUrl==null && stack.parent.getElement().getName().equals("entry")) { - fullUrl = stack.parent.getElement().getChildValue("fullUrl"); // we don't try to resolve contained references across this boundary - if (fullUrl==null) - rule(errors, IssueType.REQUIRED, stack.parent.getElement().line(), stack.parent.getElement().col(), stack.parent.getLiteralPath(), fullUrl!=null, "Bundle entry missing fullUrl"); - } - if ("Bundle".equals(stack.getElement().getType())) { - Element res = getFromBundle(stack.getElement(), ref, fullUrl, errors, path); - return res; - } - stack = stack.parent; - } - } - return null; - } - - private Element resolve(Object appContext, String ref, NodeStack stack, List errors, String path) throws IOException, FHIRException { - Element local = localResolve(ref, stack, errors, path); - if (local!=null) - return local; - if (fetcher == null) - return null; - return fetcher.fetch(appContext, ref); - } - - private ValueSet resolveBindingReference(DomainResource ctxt, Type reference, String uri) { - if (reference instanceof UriType) { - long t = System.nanoTime(); - ValueSet fr = context.fetchResource(ValueSet.class, ((UriType) reference).getValue().toString()); - txTime = txTime + (System.nanoTime() - t); - return fr; - } - else if (reference instanceof Reference) { - String s = ((Reference) reference).getReference(); - if (s.startsWith("#")) { - for (Resource c : ctxt.getContained()) { - if (c.getId().equals(s.substring(1)) && (c instanceof ValueSet)) - return (ValueSet) c; - } - return null; - } else { - long t = System.nanoTime(); - String ref = ((Reference) reference).getReference(); - if (!Utilities.isAbsoluteUrl(ref)) - ref = resolve(uri, ref); - ValueSet fr = context.fetchResource(ValueSet.class, ref); - txTime = txTime + (System.nanoTime() - t); - return fr; - } - } - else - return null; - } - - private String resolve(String uri, String ref) { - if (isBlank(uri)) { - return ref; - } - - String[] up = uri.split("\\/"); - String[] rp = ref.split("\\/"); - if (context.getResourceNames().contains(up[up.length-2]) && context.getResourceNames().contains(rp[0])) { - StringBuilder b = new StringBuilder(); - for (int i = 0; i < up.length-2; i++) { - b.append(up[i]); - b.append("/"); - } - b.append(ref); - return b.toString(); - } else - return ref; - } - - private Element 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 (Element 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 (Element entry : entries) { - String fu = entry.getNamedChildValue("fullUrl"); - if (u != null && fullUrl.equals(u)) - return entry; - if (u == null) { - Element resource = entry.getNamedChild("resource"); - String et = resource.getType(); - String eid = resource.getNamedChildValue("id"); - if (t.equals(et) && i.equals(eid)) - return entry; - } - } - } - return null; - } - } - - private ElementDefinition resolveNameReference(StructureDefinitionSnapshotComponent snapshot, String contentReference) { - for (ElementDefinition ed : snapshot.getElement()) - if (contentReference.equals("#"+ed.getId())) - return ed; - return null; - } - - private StructureDefinition resolveProfile(StructureDefinition profile, String pr) { - if (pr.startsWith("#")) { - for (Resource r : profile.getContained()) { - if (r.getId().equals(pr.substring(1)) && r instanceof StructureDefinition) - return (StructureDefinition) r; - } - return null; - } else { - long t = System.nanoTime(); - StructureDefinition fr = context.fetchResource(StructureDefinition.class, pr); - sdTime = sdTime + (System.nanoTime() - t); - return fr; - } - } - - private ElementDefinition resolveType(String type) { - if (logical != null) - for (BundleEntryComponent be : logical.getEntry()) { - if (be.hasResource() && be.getResource() instanceof StructureDefinition) { - StructureDefinition sd = (StructureDefinition) be.getResource(); - if (sd.getId().equals(type)) - return sd.getSnapshot().getElement().get(0); - } - } - String url = "http://hl7.org/fhir/StructureDefinition/" + type; - long t = System.nanoTime(); - StructureDefinition sd = context.fetchResource(StructureDefinition.class, url); - sdTime = sdTime + (System.nanoTime() - t); - if (sd == null || !sd.hasSnapshot()) - return null; - else - return sd.getSnapshot().getElement().get(0); - } - - public void setAnyExtensionsAllowed(boolean anyExtensionsAllowed) { - this.anyExtensionsAllowed = anyExtensionsAllowed; - } - - public IResourceValidator setBestPracticeWarningLevel(BestPracticeWarningLevel value) { - bpWarnings = value; - return this; - } - - @Override - public void setCheckDisplay(CheckDisplayOption checkDisplay) { - this.checkDisplay = checkDisplay; - } - - public void setSuppressLoincSnomedMessages(boolean suppressLoincSnomedMessages) { - this.suppressLoincSnomedMessages = suppressLoincSnomedMessages; - } - - public IdStatus getResourceIdRule() { - return resourceIdRule; - } - - public void setResourceIdRule(IdStatus resourceIdRule) { - this.resourceIdRule = resourceIdRule; - } - - - public boolean isAllowXsiLocation() { - return allowXsiLocation; - } - - public void setAllowXsiLocation(boolean allowXsiLocation) { - this.allowXsiLocation = allowXsiLocation; - } - - /** - * - * @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 - * @param errors - * @param stack - * @return - * @throws DefinitionException - * @throws DefinitionException - * @throws IOException - * @throws FHIRException - */ - private boolean sliceMatches(Object appContext, Element element, String path, ElementDefinition slicer, ElementDefinition ed, StructureDefinition profile, List errors, NodeStack stack) throws DefinitionException, FHIRException, IOException { - if (!slicer.getSlicing().hasDiscriminator()) - return false; // cannot validate in this case - - ExpressionNode n = (ExpressionNode) ed.getUserData("slice.expression.cache"); - if (n == null) { - long t = System.nanoTime(); - // GG: this approach is flawed because it treats discriminators individually rather than collectively - String expression = "true"; - for (ElementDefinitionSlicingDiscriminatorComponent s : slicer.getSlicing().getDiscriminator()) { - String discriminator = s.getPath(); - if (s.getType() == DiscriminatorType.PROFILE) - throw new FHIRException("Validating against slices with discriminators based on profiles is not yet supported by the FHIRPath engine: " + discriminator); - // Todo: Fix this once FHIRPath (and this engine) supports a boolean function that test profile conformance - - ElementDefinition criteriaElement = getCriteriaForDiscriminator(path, ed, discriminator, profile); - if (s.getType() == DiscriminatorType.TYPE) { - String type = null; - if (!criteriaElement.getPath().contains("[") && discriminator.contains("[")) { - discriminator = discriminator.substring(0, discriminator.indexOf('[')); - String lastNode = tail(discriminator); - type = tail(criteriaElement.getPath()).substring(lastNode.length()); - type = type.substring(0,1).toLowerCase() + type.substring(1); - } else if (!criteriaElement.hasType() || criteriaElement.getType().size()==1) { - if (discriminator.contains("[")) - discriminator = discriminator.substring(0, discriminator.indexOf('[')); - type = criteriaElement.getType().get(0).getCode(); - } - if (type==null) - throw new DefinitionException("Discriminator (" + discriminator + ") is based on type, but slice " + ed.getId() + " does not declare a type"); - if (discriminator.isEmpty()) - expression = expression + " and this is " + type; - else - expression = expression + " and " + discriminator + " is " + type; - } else if (criteriaElement.hasFixed()) { - expression = expression + " and " + discriminator + " = "; - Type fixed = criteriaElement.getFixed(); - if (fixed instanceof StringType) { - Gson gson = new Gson(); - String json = gson.toJson((StringType)fixed); - String escapedString = json.substring(json.indexOf(":")+2); - escapedString = escapedString.substring(0, escapedString.indexOf(",\"myStringValue")-1); - expression = expression + "'" + escapedString + "'"; - } else if (fixed instanceof UriType) { - expression = expression + "'" + ((UriType)fixed).asStringValue() + "'"; - } else if (fixed instanceof IntegerType) { - expression = expression + ((IntegerType)fixed).asStringValue(); - } else if (fixed instanceof DecimalType) { - expression = expression + ((IntegerType)fixed).asStringValue(); - } else if (fixed instanceof BooleanType) { - expression = expression + ((BooleanType)fixed).asStringValue(); - } else - throw new DefinitionException("Unsupported fixed value type for discriminator(" + discriminator + ") for slice " + ed.getId() + ": " + fixed.getClass().getName()); - } else if (criteriaElement.hasBinding() && criteriaElement.getBinding().hasStrength() && criteriaElement.getBinding().getStrength().equals(BindingStrength.REQUIRED) && criteriaElement.getBinding().getValueSetReference()!=null) { - expression = expression + " and " + discriminator + " in '" + criteriaElement.getBinding().getValueSetReference().getReference() + "'"; - } else if (criteriaElement.hasMin() && criteriaElement.getMin()>0) { - expression = expression + " and " + discriminator + ".exists()"; - } else if (criteriaElement.hasMax() && criteriaElement.getMax().equals("0")) { - expression = expression + " and " + discriminator + ".exists().not()"; - } else { - throw new DefinitionException("Could not match discriminator (" + discriminator + ") for slice " + ed.getId() + " - does not have fixed value, binding or existence assertions"); - } - } - - try { - n = fpe.parse(expression); - } catch (FHIRLexerException e) { - throw new FHIRException("Problem processing expression "+expression +" in profile " + profile.getUrl() + " path " + path + ": " + e.getMessage()); - } - fpeTime = fpeTime + (System.nanoTime() - t); - ed.setUserData("slice.expression.cache", n); - } - - String msg; - boolean ok; - try { - long t = System.nanoTime(); - ok = fpe.evaluateToBoolean(null, element, n); - fpeTime = fpeTime + (System.nanoTime() - t); - msg = fpe.forLog(); - } catch (Exception ex) { - throw new FHIRException("Problem evaluating slicing expression for element in profile " + profile.getUrl() + " path " + path + ": " + ex.getMessage()); - } - return ok; - } - - // 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(Object appContext, List errors, Element resource, Element element, StructureDefinition defn, NodeStack stack) throws FHIRException, FHIRException, IOException { - // profile is valid, and matches the resource name - ResourceProfiles resourceProfiles = getResourceProfiles(element, stack); - if (!resourceProfiles.isProcessed()) - checkDeclaredProfiles(resourceProfiles, errors, resource, element, stack); - - if (!resourceProfiles.isProcessed()) { - resourceProfiles.setProcessed(); - if (!resourceProfiles.hasProfiles() && - (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), defn.hasSnapshot(), - "StructureDefinition has no snapshot - validation is against the snapshot, so it must be provided"))) { - // Don't need to validate against the resource if there's a profile because the profile snapshot will include the relevant parts of the resources - validateElement(appContext, errors, defn, defn.getSnapshot().getElement().get(0), null, null, resource, element, element.getName(), stack, false); - } - - // specific known special validations - if (element.getType().equals("Bundle")) - validateBundle(errors, element, stack); - if (element.getType().equals("Observation")) - validateObservation(errors, element, stack); - if (element.getType().equals("QuestionnaireResponse")) - validateQuestionannaireResponse(errors, element, stack); - } - for (ProfileUsage profileUsage : resourceProfiles.uncheckedProfiles()) { - profileUsage.setChecked(); - validateElement(appContext, errors, profileUsage.getProfile(), profileUsage.getProfile().getSnapshot().getElement().get(0), null, null, resource, element, element.getName(), stack, false); - } - } - - private void validateQuestionannaireResponse(List errors, Element element, NodeStack stack) { - Element q = element.getNamedChild("questionnaire"); - if (hint(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), q != null && isNotBlank(q.getNamedChildValue("reference")), "No questionnaire is identified, so no validation can be performed against the base questionnaire")) { - long t = System.nanoTime(); - Questionnaire qsrc = context.fetchResource(Questionnaire.class, q.getNamedChildValue("reference")); - sdTime = sdTime + (System.nanoTime() - t); - if (warning(errors, IssueType.REQUIRED, q.line(), q.col(), stack.getLiteralPath(), qsrc != null, "The questionnaire could not be resolved, so no validation can be performed against the base questionnaire")) { - boolean inProgress = "in-progress".equals(element.getNamedChildValue("status")); - validateQuestionannaireResponseItems(qsrc, qsrc.getItem(), errors, element, stack, inProgress); - } - } - } - - private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List errors, Element element, NodeStack stack, boolean inProgress) { - String text = element.getNamedChildValue("text"); - rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), Utilities.noString(text) || text.equals(qItem.getText()), "If text exists, it must match the questionnaire definition for linkId "+qItem.getLinkId()); - - List answers = new ArrayList(); - element.getNamedChildren("answer", answers); - if (inProgress) - warning(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), (answers.size() > 0) || !qItem.getRequired(), "No response answer found for required item "+qItem.getLinkId()); - else - rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), (answers.size() > 0) || !qItem.getRequired(), "No response answer found for required item "+qItem.getLinkId()); - if (answers.size() > 1) - rule(errors, IssueType.INVALID, answers.get(1).line(), answers.get(1).col(), stack.getLiteralPath(), qItem.getRepeats(), "Only one response answer item with this linkId allowed"); - - for (Element answer : answers) { - NodeStack ns = stack.push(answer, -1, null, null); - switch (qItem.getType()) { - case GROUP: - rule(errors, IssueType.STRUCTURE, answer.line(), answer.col(), stack.getLiteralPath(), false, "Items of type group should not have answers"); - break; - case DISPLAY: // nothing - break; - case BOOLEAN: - validateQuestionnaireResponseItemType(errors, answer, ns, "boolean"); - break; - case DECIMAL: - validateQuestionnaireResponseItemType(errors, answer, ns, "decimal"); - break; - case INTEGER: - validateQuestionnaireResponseItemType(errors, answer, ns, "integer"); - break; - case DATE: - validateQuestionnaireResponseItemType(errors, answer, ns, "date"); - break; - case DATETIME: - validateQuestionnaireResponseItemType(errors, answer, ns, "dateTime"); - break; - case TIME: - validateQuestionnaireResponseItemType(errors, answer, ns, "time"); - break; - case STRING: - validateQuestionnaireResponseItemType(errors, answer, ns, "string"); - break; - case TEXT: - validateQuestionnaireResponseItemType(errors, answer, ns, "text"); - break; - case URL: - validateQuestionnaireResponseItemType(errors, answer, ns, "uri"); - break; - case ATTACHMENT: - validateQuestionnaireResponseItemType(errors, answer, ns, "Attachment"); - break; - case REFERENCE: - validateQuestionnaireResponseItemType(errors, answer, ns, "Reference"); - break; - case QUANTITY: - if (validateQuestionnaireResponseItemType(errors, answer, ns, "Quantity").equals("Quantity")) - if (qItem.hasExtension("???")) - validateQuestionnaireResponseItemQuantity(errors, answer, ns); - break; - case CHOICE: - String itemType=validateQuestionnaireResponseItemType(errors, answer, ns, "Coding", "date", "time", "integer", "string"); - if (itemType.equals("Coding")) validateAnswerCode(errors, answer, ns, qsrc, qItem, false); - else if (itemType.equals("date")) checkOption(errors, answer, ns, qsrc, qItem, "date"); - else if (itemType.equals("time")) checkOption(errors, answer, ns, qsrc, qItem, "time"); - else if (itemType.equals("integer")) checkOption(errors, answer, ns, qsrc, qItem, "integer"); - else if (itemType.equals("string")) checkOption(errors, answer, ns, qsrc, qItem, "string"); - break; - case OPENCHOICE: - itemType=validateQuestionnaireResponseItemType(errors, answer, ns, "Coding", "date", "time", "integer", "string"); - if (itemType.equals("Coding")) validateAnswerCode(errors, answer, ns, qsrc, qItem, true); - else if (itemType.equals("date")) checkOption(errors, answer, ns, qsrc, qItem, "date"); - else if (itemType.equals("time")) checkOption(errors, answer, ns, qsrc, qItem, "time"); - else if (itemType.equals("integer")) checkOption(errors, answer, ns, qsrc, qItem, "integer"); - else if (itemType.equals("string")) checkOption(errors, answer, ns, qsrc, qItem, "string", true); - break; - case QUESTION: - case NULL: - // nothing - break; - } - validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, answer, stack, inProgress); - } - if (qItem.getType() == null) { - fail(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, "Definition for item "+qItem.getLinkId() + " does not contain a type"); - } else if (qItem.getType() == QuestionnaireItemType.GROUP) { - validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, element, stack, inProgress); - } else { - List items = new ArrayList(); - element.getNamedChildren("item", items); - for (Element item : items) { - NodeStack ns = stack.push(item, -1, null, null); - rule(errors, IssueType.STRUCTURE, answers.get(0).line(), answers.get(0).col(), stack.getLiteralPath(), false, "Items not of type group should not have items - Item with linkId {0} of type {1} has {2} item(s)", qItem.getLinkId(), qItem.getType(), items.size()); - } - } - } - - private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List errors, List elements, NodeStack stack, boolean inProgress) { - if (elements.size() > 1) - rule(errors, IssueType.INVALID, elements.get(1).line(), elements.get(1).col(), stack.getLiteralPath(), qItem.getRepeats(), "Only one response item with this linkId allowed"); - for (Element element : elements) { - NodeStack ns = stack.push(element, -1, null, null); - validateQuestionannaireResponseItem(qsrc, qItem, errors, element, ns, inProgress); - } - } - - private int getLinkIdIndex(List qItems, String linkId) { - for (int i = 0; i < qItems.size(); i++) { - if (linkId.equals(qItems.get(i).getLinkId())) - return i; - } - return -1; - } - - private void validateQuestionannaireResponseItems(Questionnaire qsrc, List qItems, List errors, Element element, NodeStack stack, boolean inProgress) { - List items = new ArrayList(); - element.getNamedChildren("item", items); - // now, sort into stacks - Map> map = new HashMap>(); - int lastIndex = -1; - for (Element item : items) { - String linkId = item.getNamedChildValue("linkId"); - if (rule(errors, IssueType.REQUIRED, item.line(), item.col(), stack.getLiteralPath(), !Utilities.noString(linkId), "No LinkId, so can't be validated")) { - int index = getLinkIdIndex(qItems, linkId); - if (index == -1) { - QuestionnaireItemComponent qItem = findQuestionnaireItem(qsrc, linkId); - if (qItem != null) { - rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index > -1, "Structural Error: item is in the wrong place"); - NodeStack ns = stack.push(item, -1, null, null); - validateQuestionannaireResponseItem(qsrc, qItem, errors, element, ns, inProgress); - } - else - rule(errors, IssueType.NOTFOUND, item.line(), item.col(), stack.getLiteralPath(), index > -1, "LinkId \""+linkId+"\" not found in questionnaire"); - } - else - { - rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index >= lastIndex, "Structural Error: items are out of order"); - lastIndex = index; - List mapItem = map.get(linkId); - if (mapItem == null) { - mapItem = new ArrayList(); - map.put(linkId, mapItem); - } - mapItem.add(item); - } - } - } - - // ok, now we have a list of known items, grouped by linkId. We"ve made an error for anything out of order - for (QuestionnaireItemComponent qItem : qItems) { - List mapItem = map.get(qItem.getLinkId()); - if (mapItem != null) - validateQuestionannaireResponseItem(qsrc, qItem, errors, mapItem, stack, inProgress); - else - rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), !qItem.getRequired(), "No response found for required item "+qItem.getLinkId()); - - } - } - - private void validateQuestionnaireResponseItemQuantity( List errors, Element answer, NodeStack stack) { - - } - - private String validateQuestionnaireResponseItemType(List errors, Element element, NodeStack stack, String... types) { - List values = new ArrayList(); - element.getNamedChildrenWithWildcard("value[x]", values); - if (values.size() > 0) { - NodeStack ns = stack.push(values.get(0), -1, null, null); - CommaSeparatedStringBuilder l = new CommaSeparatedStringBuilder(); - for (String s : types) { - l.append(s); - if (values.get(0).getName().equals("value"+Utilities.capitalize(s))) - return(s); - } - if (types.length == 1) - rule(errors, IssueType.STRUCTURE, values.get(0).line(), values.get(0).col(), ns.getLiteralPath(), false, "Answer value must be of type "+types[0]); - else - rule(errors, IssueType.STRUCTURE, values.get(0).line(), values.get(0).col(), ns.getLiteralPath(), false, "Answer value must be one of the types "+l.toString()); - } - return null; - } - - private QuestionnaireItemComponent findQuestionnaireItem(Questionnaire qSrc, String linkId) { - return findItem(qSrc.getItem(), linkId); - } - - private QuestionnaireItemComponent findItem(List list, String linkId) { - for (QuestionnaireItemComponent item : list) { - if (linkId.equals(item.getLinkId())) - return item; - QuestionnaireItemComponent result = findItem(item.getItem(), linkId); - if (result != null) - return result; - } - return null; - } - - /* private void validateAnswerCode(List errors, Element value, NodeStack stack, List optionList) { - String system = value.getNamedChildValue("system"); - String code = value.getNamedChildValue("code"); - boolean found = false; - for (Coding c : optionList) { - if (ObjectUtil.equals(c.getSystem(), system) && ObjectUtil.equals(c.getCode(), code)) { - found = true; - break; - } - } - rule(errors, IssueType.STRUCTURE, value.line(), value.col(), stack.getLiteralPath(), found, "The code "+system+"::"+code+" is not a valid option"); - }*/ - - private void validateAnswerCode(List errors, Element value, NodeStack stack, Questionnaire qSrc, Reference ref, boolean theOpenChoice) { - ValueSet vs = resolveBindingReference(qSrc, ref, qSrc.getUrl()); - if (warning(errors, IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), vs != null, "ValueSet " + describeReference(ref) + " not found")) { - try { - Coding c = readAsCoding(value); - if (isBlank(c.getCode()) && isBlank(c.getSystem()) && isNotBlank(c.getDisplay())) { - if (theOpenChoice) { - return; - } - } - - long t = System.nanoTime(); - ValidationResult res = context.validateCode(c, vs); - txTime = txTime + (System.nanoTime() - t); - if (!res.isOk()) - rule(errors, IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), false, "The value provided ("+c.getSystem()+"::"+c.getCode()+") is not in the options value set in the questionnaire"); - } catch (Exception e) { - warning(errors, IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), false, "Error " + e.getMessage() + " validating Coding against Questionnaire Options"); - } - } - } - - private void validateAnswerCode( List errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean theOpenChoice) { - Element v = answer.getNamedChild("valueCoding"); - NodeStack ns = stack.push(v, -1, null, null); - if (qItem.getOption().size() > 0) - checkCodingOption(errors, answer, stack, qSrc, qItem, theOpenChoice); - // validateAnswerCode(errors, v, stack, qItem.getOption()); - else if (qItem.hasOptions()) - validateAnswerCode(errors, v, stack, qSrc, qItem.getOptions(), theOpenChoice); - else - hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Cannot validate options because no option or options are provided"); - } - - private void checkOption( List errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, String type) { - checkOption(errors, answer, stack, qSrc, qItem, type, false); - } - - private void checkOption( List errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, String type, boolean openChoice) { - if (type.equals("integer")) checkIntegerOption(errors, answer, stack, qSrc, qItem, openChoice); - else if (type.equals("date")) checkDateOption(errors, answer, stack, qSrc, qItem, openChoice); - else if (type.equals("time")) checkTimeOption(errors, answer, stack, qSrc, qItem, openChoice); - else if (type.equals("string")) checkStringOption(errors, answer, stack, qSrc, qItem, openChoice); - else if (type.equals("Coding")) checkCodingOption(errors, answer, stack, qSrc, qItem, openChoice); - } - - private void checkIntegerOption( List errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean openChoice) { - Element v = answer.getNamedChild("valueInteger"); - NodeStack ns = stack.push(v, -1, null, null); - if (qItem.getOption().size() > 0) { - List list = new ArrayList(); - for (QuestionnaireItemOptionComponent components : qItem.getOption()) { - try { - list.add(components.getValueIntegerType()); - } catch (FHIRException e) { - // If it's the wrong type, just keep going - } - } - if (list.isEmpty() && !openChoice) { - rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Option list has no option values of type integer"); - } else { - boolean found = false; - for (IntegerType item : list) { - if (item.getValue() == Integer.parseInt(v.primitiveValue())) { - found = true; - break; - } - } - if (!found) { - rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, "The integer "+v.primitiveValue()+" is not a valid option"); - } - } - } else - hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Cannot validate integer answer option because no option list is provided"); - } - - private void checkDateOption( List errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean openChoice) { - Element v = answer.getNamedChild("valueDate"); - NodeStack ns = stack.push(v, -1, null, null); - if (qItem.getOption().size() > 0) { - List list = new ArrayList(); - for (QuestionnaireItemOptionComponent components : qItem.getOption()) { - try { - list.add(components.getValueDateType()); - } catch (FHIRException e) { - // If it's the wrong type, just keep going - } - } - if (list.isEmpty() && !openChoice) { - rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Option list has no option values of type date"); - } else { - boolean found = false; - for (DateType item : list) { - if (item.getValue().equals(v.primitiveValue())) { - found = true; - break; - } - } - if (!found) { - rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, "The date "+v.primitiveValue()+" is not a valid option"); - } - } - } else - hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Cannot validate date answer option because no option list is provided"); - } - - private void checkTimeOption( List errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean openChoice) { - Element v = answer.getNamedChild("valueTime"); - NodeStack ns = stack.push(v, -1, null, null); - if (qItem.getOption().size() > 0) { - List list = new ArrayList(); - for (QuestionnaireItemOptionComponent components : qItem.getOption()) { - try { - list.add(components.getValueTimeType()); - } catch (FHIRException e) { - // If it's the wrong type, just keep going - } - } - if (list.isEmpty() && !openChoice) { - rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Option list has no option values of type time"); - } else { - boolean found = false; - for (TimeType item : list) { - if (item.getValue().equals(v.primitiveValue())) { - found = true; - break; - } - } - if (!found) { - rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, "The time "+v.primitiveValue()+" is not a valid option"); - } - } - } else - hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Cannot validate time answer option because no option list is provided"); - } - - private void checkStringOption( List errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean openChoice) { - Element v = answer.getNamedChild("valueString"); - NodeStack ns = stack.push(v, -1, null, null); - if (qItem.getOption().size() > 0) { - List list = new ArrayList(); - for (QuestionnaireItemOptionComponent components : qItem.getOption()) { - try { - if (components.getValue() != null) { - list.add(components.getValueStringType()); - } - } catch (FHIRException e) { - // If it's the wrong type, just keep going - } - } - if (list.isEmpty() && !openChoice) { - rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Option list has no option values of type string"); - } else { - boolean found = false; - for (StringType item : list) { - if (item.getValue().equals((v.primitiveValue()))) { - found = true; - break; - } - } - if (!found) { - rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, "The string "+v.primitiveValue()+" is not a valid option"); - } - } - } else { - hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Cannot validate string answer option because no option list is provided"); - } - } - - private void checkCodingOption( List errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean openChoice) { - Element v = answer.getNamedChild("valueCoding"); - String system = v.getNamedChildValue("system"); - String code = v.getNamedChildValue("code"); - NodeStack ns = stack.push(v, -1, null, null); - if (qItem.getOption().size() > 0) { - List list = new ArrayList(); - for (QuestionnaireItemOptionComponent components : qItem.getOption()) { - try { - if (components.getValue() != null) { - list.add(components.getValueCoding()); - } - } catch (FHIRException e) { - // If it's the wrong type, just keep going - } - } - if (list.isEmpty() && !openChoice) { - rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Option list has no option values of type coding"); - } else { - boolean found = false; - for (Coding item : list) { - if (ObjectUtil.equals(item.getSystem(), system) && ObjectUtil.equals(item.getCode(), code)) { - found = true; - break; - } - } - if (!found) { - rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, "The code "+system+"::"+code+" is not a valid option"); - } - } - } else - hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Cannot validate Coding option because no option list is provided"); - } - - private String tail(String path) { - return path.substring(path.lastIndexOf(".") + 1); - } - - private String tryParse(String ref) { - 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; - } - - private void validateBundle(List errors, Element bundle, NodeStack stack) { - List entries = new ArrayList(); - bundle.getNamedChildren("entry", entries); - String type = bundle.getNamedChildValue("type"); - type = StringUtils.defaultString(type); - - if (entries.size() == 0) { - rule(errors, IssueType.INVALID, stack.getLiteralPath(), !(type.equals("document") || type.equals("message")), "Documents or Messages must contain at least one entry"); - } else { - Element firstEntry = entries.get(0); - NodeStack firstStack = stack.push(firstEntry, 0, null, null); - String fullUrl = firstEntry.getNamedChildValue("fullUrl"); - - if (type.equals("document")) { - Element resource = firstEntry.getNamedChild("resource"); - NodeStack localStack = firstStack.push(resource, -1, null, null); - String id = resource.getNamedChildValue("id"); - if (rule(errors, IssueType.INVALID, firstEntry.line(), firstEntry.col(), stack.addToLiteralPath("entry", ":0"), resource != null, "No resource on first entry")) { - validateDocument(errors, entries, resource, localStack.push(resource, -1, null, null), fullUrl, id); - } - checkAllInterlinked(errors, entries, stack, bundle); - } - if (type.equals("message")) { - Element resource = firstEntry.getNamedChild("resource"); - NodeStack localStack = firstStack.push(resource, -1, null, null); - String id = resource.getNamedChildValue("id"); - if (rule(errors, IssueType.INVALID, firstEntry.line(), firstEntry.col(), stack.addToLiteralPath("entry", ":0"), resource != null, "No resource on first entry")) { - validateMessage(errors, entries, resource, localStack.push(resource, -1, null, null), fullUrl, id); - } - checkAllInterlinked(errors, entries, stack, bundle); - } - } - } - - private void checkAllInterlinked(List errors, List entries, NodeStack stack, Element bundle) { - List visitedResources = new ArrayList(); - HashMap candidateEntries = new HashMap(); - List candidateResources = new ArrayList(); - for (Element entry: entries) { - candidateEntries.put(entry.getNamedChild("resource"), entry); - candidateResources.add(entry.getNamedChild("resource")); - } - followResourceLinks(entries.get(0), visitedResources, candidateEntries, candidateResources, true, errors, stack); - List unusedResources = new ArrayList(); - unusedResources.addAll(candidateResources); - unusedResources.removeAll(visitedResources); - int i = 0; - for (Element entry : entries) { - warning(errors, IssueType.INFORMATIONAL, entry.line(), entry.col(), stack.addToLiteralPath("entry", Integer.toString(i)), !unusedResources.contains(entry.getNamedChild("resource")), "Entry isn't reachable by traversing from first Bundle entry"); - i++; - } - // Todo - check if the remaining resources point *to* the elements in the referenced set. Any that are still left over are errors - } - - private void followResourceLinks(Element entry, List visitedResources, HashMap candidateEntries, List candidateResources, boolean referenced, List errors, NodeStack stack) { - Element resource = entry.getNamedChild("resource"); - if (visitedResources.contains(resource)) - return; - - if (referenced) - visitedResources.add(resource); - - List references = findReferences(resource); - for (String reference: references) { - Element r = getFromBundle(stack.getElement(), reference, entry.getChildValue("fullUrl"), errors, stack.addToLiteralPath("entry:" + candidateResources.indexOf(resource))); - if (r!=null && !visitedResources.contains(r)) { - followResourceLinks(candidateEntries.get(r), visitedResources, candidateEntries, candidateResources, referenced, errors, stack); - } - } - } - - private List findReferences(Element start) { - List references = new ArrayList(); - findReferences(start, references); - return references; - } - - private void findReferences(Element start, List references) { - for (Element child : start.getChildren()) { - if (child.getType().equals("Reference")) { - String ref = child.getChildValue("reference"); - if (ref!=null && !ref.startsWith("#")) - references.add(ref); - } - findReferences(child, references); - } - } - - private void validateBundleReference(List errors, List entries, Element ref, String name, NodeStack stack, String fullUrl, String type, String id) { - String reference = null; - try { - reference = ref.getNamedChildValue("reference"); - } catch (Error e) { - - } - - if (ref != null && !Utilities.noString(reference)) { - Element target = resolveInBundle(entries, reference, fullUrl, type, id); - rule(errors, IssueType.INVALID, ref.line(), ref.col(), stack.addToLiteralPath("reference"), target != null, "Unable to resolve the target of the reference in the bundle (" + name + ")"); - } - } - - private void validateContains(Object appContext, List errors, String path, ElementDefinition child, ElementDefinition context, Element resource, Element element, NodeStack stack, IdStatus idstatus) throws FHIRException, FHIRException, IOException { - String resourceName = element.getType(); - long t = System.nanoTime(); - StructureDefinition profile = this.context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + resourceName); - sdTime = sdTime + (System.nanoTime() - t); - // special case: resource wrapper is reset if we're crossing a bundle boundary, but not otherwise - if (element.getSpecial() == SpecialElement.BUNDLE_ENTRY || element.getSpecial() == SpecialElement.BUNDLE_OUTCOME || element.getSpecial() == SpecialElement.PARAMETER ) - resource = element; - if (rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), profile != null, "No profile found for contained resource of type '" + resourceName + "'")) - validateResource(appContext, errors, resource, element, profile, null, idstatus, stack); - } - - private void validateDocument(List errors, List entries, Element 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.getType().equals("Composition"), - "The first entry in a document must be a composition")) { - // the composition subject and section references must resolve in the bundle - Element elem = composition.getNamedChild("subject"); - if (rule(errors, IssueType.INVALID, composition.line(), composition.col(), stack.getLiteralPath(), elem != null, "A document composition must have a subject")) - validateBundleReference(errors, entries, elem, "Composition Subject", stack.push(elem, -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 validateElement(Object appContext, List errors, StructureDefinition profile, ElementDefinition definition, StructureDefinition cprofile, ElementDefinition context, - Element resource, Element element, String actualType, NodeStack stack, boolean inCodeableConcept) throws FHIRException, FHIRException, IOException { - element.markValidation(profile, definition); - - // System.out.println(" "+stack.getLiteralPath()+" "+Long.toString((System.nanoTime() - time) / 1000000)); - // time = System.nanoTime(); - if (resource.getName().equals("contained")) { - NodeStack ancestor = stack; - while (!ancestor.element.isResource() || ancestor.element.getName().equals("contained")) - ancestor = ancestor.parent; - checkInvariants(errors, stack.getLiteralPath(), profile, definition, null, null, ancestor.element, element); - } else - checkInvariants(errors, stack.getLiteralPath(), profile, definition, null, null, resource, element); - if (definition.getFixed()!=null) - checkFixedValue(errors, stack.getLiteralPath(), element, definition.getFixed(), definition.getSliceName(), null); - - - // get the list of direct defined children, including slices - List childDefinitions = ProfileUtilities.getChildMap(profile, definition); - if (childDefinitions.isEmpty()) { - if (actualType == null) - return; // there'll be an error elsewhere in this case, and we're going to stop. - - StructureDefinition dt = this.context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + actualType); - if (dt == null) - throw new DefinitionException("Unable to resolve actual type " + actualType); - - childDefinitions = ProfileUtilities.getChildMap(dt, dt.getSnapshot().getElement().get(0)); - } - - // 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())); - - // 2. assign children to a definition - // for each definition, for each child, check whether it belongs in the slice - ElementDefinition slicer = null; - boolean unsupportedSlicing = false; - List problematicPaths = new ArrayList(); - String slicingPath = null; - int sliceOffset = 0; - for (int i = 0; i < childDefinitions.size(); i++) { - ElementDefinition ed = childDefinitions.get(i); - boolean childUnsupportedSlicing = false; - boolean process = true; - if (ed.hasSlicing() && !ed.getSlicing().getOrdered()) - slicingPath = ed.getPath(); - else if (slicingPath!=null && ed.getPath().equals(slicingPath)) - ; // nothing - else if (slicingPath != null && !ed.getPath().startsWith(slicingPath)) - slicingPath = null; - // where are we with slicing - if (ed.hasSlicing()) { - if (slicer != null && slicer.getPath().equals(ed.getPath())) - throw new DefinitionException("Slice encountered midway through path on " + slicer.getPath()); - slicer = ed; - process = false; - sliceOffset = i; - } else if (slicer != null && !slicer.getPath().equals(ed.getPath())) - slicer = null; - -// if (process) { - for (ElementInfo ei : children) { - boolean match = false; - if (slicer == null || slicer == ed) { - match = nameMatches(ei.name, tail(ed.getPath())); - } else { -// ei.slice = slice; - if (nameMatches(ei.name, tail(ed.getPath()))) - try { - match = sliceMatches(appContext, ei.element, ei.path, slicer, ed, profile, errors, stack); - if (match) - ei.slice = slicer; - } catch (FHIRException e) { - warning(errors, IssueType.PROCESSING, ei.line(), ei.col(), ei.path, false, e.getMessage()); - unsupportedSlicing = true; - childUnsupportedSlicing = true; - } - } - if (match) { - if (rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.path, ei.definition == null || ei.definition == slicer, "Profile " + profile.getUrl() + ", Element matches more than one slice")) { - ei.definition = ed; - if (ei.slice == null) { - ei.index = i; - } else { - ei.index = sliceOffset; - ei.sliceindex = i - (sliceOffset + 1); - } - } - } else if (childUnsupportedSlicing) { - problematicPaths.add(ed.getPath()); - } - } -// } - } - int last = -1; - int lastSlice = -1; - for (ElementInfo ei : children) { - String sliceInfo = ""; - if (slicer != null) - sliceInfo = " (slice: " + slicer.getPath()+")"; - 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.getNamedChildValue("url") + "\")" + (profile==null ? "" : " for profile " + profile.getUrl())); - else if (!unsupportedSlicing) - if (ei.slice!=null && (ei.slice.getSlicing().getRules().equals(ElementDefinition.SlicingRules.OPEN) || ei.slice.getSlicing().getRules().equals(ElementDefinition.SlicingRules.OPENATEND))) - hint(errors, IssueType.INFORMATIONAL, ei.line(), ei.col(), ei.path, (ei.definition != null), - "Element " + ei.element.getName() + " is unknown or does not match any slice " + sliceInfo + (profile==null ? "" : " for profile " + profile.getUrl())); - else - if (ei.slice!=null && (ei.slice.getSlicing().getRules().equals(ElementDefinition.SlicingRules.OPEN) || ei.slice.getSlicing().getRules().equals(ElementDefinition.SlicingRules.OPENATEND))) - rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.path, (ei.definition != null), - "Element " + ei.element.getName() + " is unknown or does not match any slice " + sliceInfo + (profile==null ? "" : " for profile " + profile.getUrl())); - else - hint(errors, IssueType.NOTSUPPORTED, ei.line(), ei.col(), ei.path, (ei.definition != null), - "Could not verify slice for profile " + profile.getUrl()); - // TODO: Should get the order of elements correct when parsing elements that are XML attributes vs. elements - boolean isXmlAttr = false; - if (ei.definition!=null) - for (Enumeration r : ei.definition.getRepresentation()) { - if (r.getValue() == PropertyRepresentation.XMLATTR) { - isXmlAttr = true; - break; - } - } - - rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.path, (ei.definition == null) || (ei.index >= last) || isXmlAttr, "As specified by profile " + profile.getUrl() + ", Element '"+ei.name+"' is out of order"); - if (ei.slice != null && ei.index == last && ei.slice.getSlicing().getOrdered()) - rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.path, (ei.definition == null) || (ei.sliceindex >= lastSlice) || isXmlAttr, "As specified by profile " + profile.getUrl() + ", Element '"+ei.name+"' is out of order in ordered slice"); - if (ei.definition == null || !isXmlAttr) - last = ei.index; - if (ei.slice != null) - lastSlice = ei.sliceindex; - else - lastSlice = -1; - } - - // 3. report any definitions that have a cardinality problem - for (ElementDefinition ed : childDefinitions) { - if (ed.getRepresentation().isEmpty()) { // ignore xml attributes - int count = 0; - List slices = null; - if (ed.hasSlicing()) - slices = ProfileUtilities.getSliceList(profile, ed); - for (ElementInfo ei : children) - if (ei.definition == ed) - count++; - else if (slices!=null) { - for (ElementDefinition sed : slices) { - if (ei.definition == sed) { - count++; - break; - } - } - } - String location = "Profile " + profile.getUrl() + ", Element '" + stack.getLiteralPath() + "." + tail(ed.getPath()) + (ed.hasSliceName()? "[" + ed.getSliceName() + "]": ""); - if (ed.getMin() > 0) { - if (problematicPaths.contains(ed.getPath())) - hint(errors, IssueType.NOTSUPPORTED, element.line(), element.col(), stack.getLiteralPath(), count >= ed.getMin(), - location + "': Unable to check minimum required (" + Integer.toString(ed.getMin()) + ") due to lack of slicing validation"); - else - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), count >= ed.getMin(), - location + "': minimum required = " + Integer.toString(ed.getMin()) + ", but only found " + Integer.toString(count)); - } - if (ed.hasMax() && !ed.getMax().equals("*")) { - if (problematicPaths.contains(ed.getPath())) - hint(errors, IssueType.NOTSUPPORTED, element.line(), element.col(), stack.getLiteralPath(), count <= Integer.parseInt(ed.getMax()), - location + ": Unable to check max allowed (" + ed.getMax() + ") due to lack of slicing validation"); - else - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), count <= Integer.parseInt(ed.getMax()), - location + ": max allowed = " + ed.getMax() + ", but found " + Integer.toString(count)); - } - } - } - // 4. check order if any slices are ordered. (todo) - - // 5. inspect each child for validity - for (ElementInfo ei : children) { - List profiles = new ArrayList(); - 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(); - // Excluding reference is a kludge to get around versioning issues - if (ei.definition.getType().get(0).hasProfile() && !type.equals("Reference")) - profiles.add(ei.definition.getType().get(0).getProfile()); - - } 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); - if (isPrimitiveType(type)) - type = Utilities.uncapitalize(type); - // Excluding reference is a kludge to get around versioning issues - if (ei.definition.getType().get(0).hasProfile() && !type.equals("Reference")) - profiles.add(ei.definition.getType().get(0).getProfile()); - } 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(); - // Excluding reference is a kludge to get around versioning issues - if (t.hasProfile() && !type.equals("Reference")) - profiles.add(t.getProfile()); - } - 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.getContentReference() != null) { - typeDefn = resolveNameReference(profile.getSnapshot(), ei.definition.getContentReference()); - } - - if (type != null) { - if (type.startsWith("@")) { - 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(); - String eiPath = ei.path; - assert(eiPath.equals(localStackLiterapPath)) : "ei.path: " + ei.path + " - localStack.getLiteralPath: " + localStackLiterapPath; - boolean thisIsCodeableConcept = false; - - if (type != null) { - if (isPrimitiveType(type)) { - checkPrimitive(appContext, errors, ei.path, type, ei.definition, ei.element, profile); - } 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, inCodeableConcept); - else if (type.equals("CodeableConcept")) { - checkCodeableConcept(errors, ei.path, ei.element, profile, ei.definition); - thisIsCodeableConcept = true; - } else if (type.equals("Reference")) - checkReference(appContext, errors, ei.path, ei.element, profile, ei.definition, actualType, localStack); - - // We only check extensions if we're not in a complex extension or if the element we're dealing with is not defined as part of that complex extension - if (type.equals("Extension") && ei.element.getChildValue("url").contains("/")) - checkExtension(appContext, errors, ei.path, resource, ei.element, ei.definition, profile, localStack); - else if (type.equals("Resource")) - validateContains(appContext, errors, ei.path, ei.definition, definition, resource, ei.element, localStack, idStatusForEntry(element, ei)); // if - // (str.matches(".*([.,/])work\\1$")) - else { - StructureDefinition p = null; - boolean elementValidated = false; - if (profiles.isEmpty()) { - p = getProfileForType(type); - rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.path, p != null, "Unknown type " + type); - } else if (profiles.size()==1) { - p = this.context.fetchResource(StructureDefinition.class, profiles.get(0)); - rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.path, p != null, "Unknown profile " + profiles.get(0)); - } else { - elementValidated = true; - HashMap> goodProfiles = new HashMap>(); - List> badProfiles = new ArrayList>(); - for (String typeProfile : profiles) { - p = this.context.fetchResource(StructureDefinition.class, typeProfile); - if (rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.path, p != null, "Unknown profile " + typeProfile)) { - List profileErrors = new ArrayList(); - validateElement(appContext, profileErrors, p, p.getSnapshot().getElement().get(0), profile, ei.definition, resource, ei.element, type, localStack, thisIsCodeableConcept); - boolean hasError = false; - for (ValidationMessage msg : profileErrors) { - if (msg.getLevel()==ValidationMessage.IssueSeverity.ERROR || msg.getLevel()==ValidationMessage.IssueSeverity.FATAL) { - hasError = true; - break; - } - } - if (hasError) - badProfiles.add(profileErrors); - else - goodProfiles.put(typeProfile, profileErrors); - } - if (goodProfiles.size()==1) { - errors.addAll(goodProfiles.values().iterator().next()); - } else if (goodProfiles.size()==0) { - rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.path, false, "Unable to find matching profile among choices: " + StringUtils.join("; ", profiles)); - for (List messages : badProfiles) { - errors.addAll(messages); - } - } else { - warning(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.path, false, "Found multiple matching profiles among choices: " + StringUtils.join("; ", goodProfiles.keySet())); - for (List messages : goodProfiles.values()) { - errors.addAll(messages); - } - } - } - } - if (p!=null) { - if (!elementValidated) - validateElement(appContext, errors, p, p.getSnapshot().getElement().get(0), profile, ei.definition, resource, ei.element, type, localStack, thisIsCodeableConcept); - int index = profile.getSnapshot().getElement().indexOf(ei.definition); - if (index < profile.getSnapshot().getElement().size() - 1) { - String nextPath = profile.getSnapshot().getElement().get(index+1).getPath(); - if (!nextPath.equals(ei.definition.getPath()) && nextPath.startsWith(ei.definition.getPath())) - validateElement(appContext, errors, profile, ei.definition, null, null, resource, ei.element, type, localStack, thisIsCodeableConcept); - } - } - } - } - } else { - if (rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), stack.getLiteralPath(), ei.definition != null, "Unrecognised Content " + ei.name)) - validateElement(appContext, errors, profile, ei.definition, null, null, resource, ei.element, type, localStack, false); - } - } - } - } - - private IdStatus idStatusForEntry(Element ep, ElementInfo ei) { - if (isBundleEntry(ei.path)) { - Element req = ep.getNamedChild("request"); - Element resp = ep.getNamedChild("response"); - Element fullUrl = ep.getNamedChild("fullUrl"); - Element method = null; - Element url = null; - if (req != null) { - method = req.getNamedChild("method"); - url = req.getNamedChild("url"); - } - if (resp != null) { - return IdStatus.OPTIONAL; - } if (method == null) { - if (fullUrl == null) - return IdStatus.REQUIRED; - else if (fullUrl.primitiveValue().startsWith("urn:uuid:") || fullUrl.primitiveValue().startsWith("urn:oid:")) - return IdStatus.OPTIONAL; - else - return IdStatus.REQUIRED; - } else { - String s = method.primitiveValue(); - if (s.equals("PUT")) { - if (url == null) - return IdStatus.REQUIRED; - else - return IdStatus.OPTIONAL; // or maybe prohibited? not clear - } else if (s.equals("POST")) - return IdStatus.OPTIONAL; // this should be prohibited, but see task 9102 - else // actually, we should never get to here; a bundle entry with method get/delete should not have a resource - return IdStatus.OPTIONAL; - } - } else if (isParametersEntry(ei.path) || isBundleOutcome(ei.path)) - return IdStatus.OPTIONAL; - else - return IdStatus.REQUIRED; - } - - private void checkInvariants(List errors, String path, StructureDefinition profile, ElementDefinition ed, String typename, String typeProfile, Element resource, Element element) throws FHIRException, FHIRException { - if (noInvariantChecks) - return; - - for (ElementDefinitionConstraintComponent inv : ed.getConstraint()) { - if (inv.hasExpression()) { - ExpressionNode n = (ExpressionNode) inv.getUserData("validator.expression.cache"); - if (n == null) { - long t = System.nanoTime(); - try { - n = fpe.parse(inv.getExpression()); - } catch (FHIRLexerException e) { - throw new FHIRException("Problem processing expression "+inv.getExpression() +" in profile " + profile.getUrl() + " path " + path + ": " + e.getMessage()); - } - fpeTime = fpeTime + (System.nanoTime() - t); - inv.setUserData("validator.expression.cache", n); - } - - String msg; - boolean ok; - try { - long t = System.nanoTime(); - ok = fpe.evaluateToBoolean(resource, element, n); - fpeTime = fpeTime + (System.nanoTime() - t); - msg = fpe.forLog(); - } catch (Exception ex) { - ok = false; - msg = ex.getMessage(); - } - if (!ok) { - try { - ok = fpe.evaluateToBoolean(resource, element, n); - } catch (PathEngineException e) { - throw new FHIRException("Problem processing expression "+inv.getExpression() +" in profile " + profile.getUrl() + " path " + path + ": " + e.getMessage()); - } - if (!Utilities.noString(msg)) - msg = " ("+msg+")"; - if (inv.getSeverity() == ConstraintSeverity.ERROR) - rule(errors, IssueType.INVARIANT, element.line(), element.col(), path, ok, inv.getHuman()+msg+" ["+inv.getExpression()+"]"); - else if (inv.getSeverity() == ConstraintSeverity.WARNING) - warning(errors, IssueType.INVARIANT, element.line(), element.line(), path, ok, inv.getHuman()+msg+" ["+inv.getExpression()+"]"); - } - } - } - } - - private void validateMessage(List errors, List entries, Element messageHeader, NodeStack stack, String fullUrl, String id) { - // first entry must be a messageheader - if (rule(errors, IssueType.INVALID, messageHeader.line(), messageHeader.col(), stack.getLiteralPath(), messageHeader.getType().equals("MessageHeader"), - "The first entry in a message must be a MessageHeader")) { - // the composition subject and section references must resolve in the bundle - List elements = messageHeader.getChildren("data"); - for (Element elem: elements) - validateBundleReference(errors, entries, elem, "MessageHeader Data", stack.push(elem, -1, null, null), fullUrl, "MessageHeader", id); - } - } - - private void validateObservation(List errors, Element 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 void validateResource(List errors, Element resource, Element element, StructureDefinition defn, ValidationProfileSet profiles, IdStatus idstatus, NodeStack stack) throws FHIRException, FHIRException { - List declProfiles = new ArrayList(); - List meta = element.getChildrenByName("meta"); - if (!meta.isEmpty()) { - for (Element profileName : meta.get(0).getChildrenByName("profile")) { - StructureDefinition sd = context.fetchResource(StructureDefinition.class, profileName.getValue()); - if (sd != null) - declProfiles.add(sd); - } - } - - if (!declProfiles.isEmpty()) { - // Validate against profiles rather than the resource itself as they'll be more constrained and will cover the resource elements anyhow - for (StructureDefinition sd : declProfiles) - validateResource2(errors, resource, element, sd, profiles, idstatus, stack); - } else - validateResource2(errors, resource, element, defn, profiles, idstatus, stack); - }*/ - - private void validateResource(Object appContext, List errors, Element resource, Element element, StructureDefinition defn, ValidationProfileSet profiles, IdStatus idstatus, NodeStack stack) throws FHIRException, FHIRException, IOException { - assert stack != null; - assert resource != null; - - boolean ok = true; - - String resourceName = element.getType(); // todo: consider namespace...? - if (defn == null) { - long t = System.nanoTime(); - defn = element.getProperty().getStructure(); - if (defn == null) - defn = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + resourceName); - if (profiles!=null) - getResourceProfiles(resource, stack).addProfiles(errors, profiles, stack.getLiteralPath(), element); - sdTime = sdTime + (System.nanoTime() - t); - ok = rule(errors, IssueType.INVALID, element.line(), element.col(), stack.addToLiteralPath(resourceName), defn != null, "No definition found for resource type '" + resourceName + "'"); - } - - String type = defn.getKind() == StructureDefinitionKind.LOGICAL ? defn.getId() : defn.getType(); - // special case: we have a bundle, and the profile is not for a bundle. We'll try the first entry instead - if (!type.equals(resourceName) && resourceName.equals("Bundle")) { - Element first = getFirstEntry(element); - if (first != null && first.getType().equals(type)) { - element = first; - resourceName = element.getType(); - idstatus = IdStatus.OPTIONAL; // why? - } - } - ok = rule(errors, IssueType.INVALID, -1, -1, stack.getLiteralPath(), type.equals(resourceName), "Specified profile type was '" + type + "', but found type '" + resourceName + "'"); - - if (ok) { - if (idstatus == IdStatus.REQUIRED && (element.getNamedChild("id") == null)) - rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), false, "Resource requires an id, but none is present"); - else if (idstatus == IdStatus.PROHIBITED && (element.getNamedChild("id") != null)) - rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), false, "Resource has an id, but none is allowed"); - start(appContext, errors, resource, element, defn, stack); // root is both definition and type - } - } - - private void loadProfiles(ValidationProfileSet profiles) throws DefinitionException { - if (profiles != null) { - for (String profile : profiles.getCanonicalUrls()) { - StructureDefinition p = context.fetchResource(StructureDefinition.class, profile); - if (p == null) - throw new DefinitionException("StructureDefinition '" + profile + "' not found"); - profiles.getDefinitions().add(p); - } - } - } - - private Element getFirstEntry(Element bundle) { - List list = new ArrayList(); - bundle.getNamedChildren("entry", list); - if (list.isEmpty()) - return null; - Element resource = list.get(0).getNamedChild("resource"); - if (resource == null) - return null; - else - return resource; - } - - private void validateSections(List errors, List entries, Element focus, NodeStack stack, String fullUrl, String id) { - List sections = new ArrayList(); - focus.getNamedChildren("entry", sections); - int i = 0; - for (Element 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++; - } - } - - private boolean valueMatchesCriteria(Element value, ElementDefinition criteria) throws FHIRException { - if (criteria.hasFixed()) { - List msgs = new ArrayList(); - checkFixedValue(msgs, "{virtual}", value, criteria.getFixed(), "value", null); - return msgs.size() == 0; - } else if (criteria.hasBinding() && criteria.getBinding().getStrength() == BindingStrength.REQUIRED && criteria.getBinding().hasValueSet()) { - throw new FHIRException("Unable to resolve slice matching - slice matching by value set not done"); - } else { - throw new FHIRException("Unable to resolve slice matching - no fixed value or required value set"); - } - } - - private boolean yearIsValid(String v) { - if (v == null) { - return false; - } - try { - int i = Integer.parseInt(v.substring(0, Math.min(4, v.length()))); - return i >= 1800 && i <= 2100; - } catch (NumberFormatException e) { - return false; - } - } - - public class ChildIterator { - private String basePath; - private Element parent; - private int cursor; - private int lastCount; - - public ChildIterator(String path, Element element) { - parent = element; - basePath = path; - cursor = -1; - } - - public int count() { - String nb = cursor == 0 ? "--" : parent.getChildren().get(cursor-1).getName(); - String na = cursor >= parent.getChildren().size() - 1 ? "--" : parent.getChildren().get(cursor+1).getName(); - if (name().equals(nb) || name().equals(na) ) { - return lastCount + 1; - } else - return -1; - } - - public Element element() { - return parent.getChildren().get(cursor); - } - - public String name() { - return element().getName(); - } - - public boolean next() { - if (cursor == -1) { - cursor++; - lastCount = 0; - } else { - String lastName = name(); - cursor++; - if (cursor < parent.getChildren().size() && name().equals(lastName)) - lastCount++; - else - lastCount = 0; - } - return cursor < parent.getChildren().size(); - } - - public String path() { - int i = count(); - String sfx = ""; - if (i > -1) { - sfx = "[" + Integer.toString(lastCount + 1) + "]"; - } - return basePath + "." + name() + sfx; - } - } - - public class NodeStack { - private ElementDefinition definition; - private Element element; - private ElementDefinition extension; - private String literalPath; // xpath format - private List logicalPaths; // dotted format, various entry points - private NodeStack parent; - private ElementDefinition type; - - public NodeStack() { - } - - public NodeStack(Element element) { - this.element = element; - literalPath = element.getName(); - } - - public String addToLiteralPath(String... path) { - StringBuilder b = new StringBuilder(); - b.append(getLiteralPath()); - for (String p : path) { - if (p.startsWith(":")) { - b.append("["); - b.append(p.substring(1)); - b.append("]"); - } else { - b.append("."); - b.append(p); - } - } - return b.toString(); - } - - private ElementDefinition getDefinition() { - return definition; - } - - private Element getElement() { - return element; - } - - protected String getLiteralPath() { - return literalPath == null ? "" : literalPath; - } - - private List getLogicalPaths() { - return logicalPaths == null ? new ArrayList() : logicalPaths; - } - - private ElementDefinition getType() { - return type; - } - - private NodeStack push(Element element, int count, ElementDefinition definition, ElementDefinition type) { - NodeStack res = new NodeStack(); - res.parent = this; - res.element = element; - res.definition = definition; - 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 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(); - } - } - - public class ElementInfo { - - public int index; // order of definition in overall order. all slices get the index of the slicing definition - public int sliceindex; // order of the definition in the slices (if slice != null) - public int count; - public ElementDefinition definition; - public ElementDefinition slice; - private Element element; - private String name; - private String path; - - public ElementInfo(String name, Element 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(); - } - - } - - public String reportTimes() { - String s = String.format("Times: overall = %d, tx = %d, sd = %d, load = %d, fpe = %d", overall, txTime, sdTime, loadTime, fpeTime); - overall = 0; - txTime = 0; - sdTime = 0; - loadTime = 0; - fpeTime = 0; - return s; - } - - public boolean isNoBindingMsgSuppressed() { - return noBindingMsgSuppressed; - } - - public IResourceValidator setNoBindingMsgSuppressed(boolean noBindingMsgSuppressed) { - this.noBindingMsgSuppressed = noBindingMsgSuppressed; - return this; - } - - - public boolean isNoTerminologyChecks() { - return noTerminologyChecks; - } - - public IResourceValidator setNoTerminologyChecks(boolean noTerminologyChecks) { - this.noTerminologyChecks = noTerminologyChecks; - return this; - } - - public void checkAllInvariants(){ - for (StructureDefinition sd : context.allStructures()) { - if (sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) { - for (ElementDefinition ed : sd.getSnapshot().getElement()) { - for (ElementDefinitionConstraintComponent inv : ed.getConstraint()) { - if (inv.hasExpression()) { - try { - ExpressionNode n = (ExpressionNode) inv.getUserData("validator.expression.cache"); - if (n == null) { - n = fpe.parse(inv.getExpression()); - inv.setUserData("validator.expression.cache", n); - } - fpe.check(null, sd.getKind() == StructureDefinitionKind.RESOURCE ? sd.getType() : "DomainResource", ed.getPath(), n); - } catch (Exception e) { - System.out.println("Error processing structure ["+sd.getId()+"] path "+ed.getPath()+":"+inv.getKey()+" (\""+inv.getExpression()+"\"): "+e.getMessage()); - } - } - } - } - } - } - } - -} diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/ProfileValidator.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/ProfileValidator.java deleted file mode 100644 index 6a65414be84..00000000000 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/ProfileValidator.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.hl7.fhir.dstu3.validation; - -import java.util.ArrayList; -import java.util.List; - -import org.hl7.fhir.dstu3.context.IWorkerContext; -import org.hl7.fhir.dstu3.model.ElementDefinition; -import org.hl7.fhir.dstu3.model.ElementDefinition.ElementDefinitionConstraintComponent; -import org.hl7.fhir.dstu3.model.StructureDefinition; -import org.hl7.fhir.dstu3.utils.FHIRPathEngine; -import org.hl7.fhir.utilities.Utilities; -import org.hl7.fhir.utilities.validation.ValidationMessage; -import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; - -public class ProfileValidator extends BaseValidator { - - IWorkerContext context; - - public void setContext(IWorkerContext context) { - this.context = context; - } - - protected boolean rule(List errors, IssueType type, String path, boolean b, String msg) { - String rn = path.contains(".") ? path.substring(0, path.indexOf(".")) : path; - return super.rule(errors, type, path, b, msg, ""+rn+": "+Utilities.escapeXml(msg)); - } - - public List validate(StructureDefinition profile, boolean forBuild) { - List errors = new ArrayList(); - - // first check: extensions must exist - for (ElementDefinition ec : profile.getDifferential().getElement()) - checkExtensions(profile, errors, "differential", ec); - rule(errors, IssueType.STRUCTURE, profile.getId(), profile.hasSnapshot(), "missing Snapshot at "+profile.getName()+"."+profile.getName()); - for (ElementDefinition ec : profile.getSnapshot().getElement()) - checkExtensions(profile, errors, "snapshot", ec); - - if (rule(errors, IssueType.STRUCTURE, profile.getId(), profile.hasSnapshot(), "A snapshot is required")) { - for (ElementDefinition ed : profile.getSnapshot().getElement()) { - checkExtensions(profile, errors, "snapshot", ed); - for (ElementDefinitionConstraintComponent inv : ed.getConstraint()) { - if (forBuild) { - if (!inExemptList(inv.getKey())) { - if (rule(errors, IssueType.BUSINESSRULE, profile.getId()+"::"+ed.getPath()+"::"+inv.getKey(), inv.hasExpression(), "The invariant has no FHIR Path expression ("+inv.getXpath()+")")) { - try { - new FHIRPathEngine(context).check(null, profile.getType(), ed.getPath(), inv.getExpression()); // , inv.hasXpath() && inv.getXpath().startsWith("@value") - } catch (Exception e) { -// rule(errors, IssueType.STRUCTURE, profile.getId()+"::"+ed.getPath()+"::"+inv.getId(), exprExt != null, e.getMessage()); - } - } - } - } - } - } - } - return errors; - } - - // these are special cases - private boolean inExemptList(String key) { - return key.startsWith("txt-"); - } - - private void checkExtensions(StructureDefinition profile, List errors, String kind, ElementDefinition ec) { - if (!ec.getType().isEmpty() && "Extension".equals(ec.getType().get(0).getCode()) && ec.getType().get(0).hasProfile()) { - String url = ec.getType().get(0).getProfile(); - StructureDefinition defn = context.fetchResource(StructureDefinition.class, url); - rule(errors, IssueType.BUSINESSRULE, profile.getId(), defn != null, "Unable to find Extension '"+url+"' referenced at "+profile.getUrl()+" "+kind+" "+ec.getPath()+" ("+ec.getSliceName()+")"); - } - } - -} diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/QuestionnaireResponseValidator.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/QuestionnaireResponseValidator.java deleted file mode 100644 index 1209c78c8a4..00000000000 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/QuestionnaireResponseValidator.java +++ /dev/null @@ -1,393 +0,0 @@ -package org.hl7.fhir.dstu3.validation; - -import static org.apache.commons.lang3.StringUtils.isBlank; -import static org.apache.commons.lang3.StringUtils.isNotBlank; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; - -import org.apache.commons.lang3.Validate; -import org.hl7.fhir.dstu3.context.IWorkerContext; -import org.hl7.fhir.dstu3.model.Attachment; -import org.hl7.fhir.dstu3.model.BooleanType; -import org.hl7.fhir.dstu3.model.Coding; -import org.hl7.fhir.dstu3.model.DateTimeType; -import org.hl7.fhir.dstu3.model.DateType; -import org.hl7.fhir.dstu3.model.DecimalType; -import org.hl7.fhir.dstu3.model.InstantType; -import org.hl7.fhir.dstu3.model.IntegerType; -import org.hl7.fhir.dstu3.model.Quantity; -import org.hl7.fhir.dstu3.model.Questionnaire; -import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemComponent; -import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemType; -import org.hl7.fhir.dstu3.model.QuestionnaireResponse; -import org.hl7.fhir.dstu3.model.QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent; -import org.hl7.fhir.dstu3.model.QuestionnaireResponse.QuestionnaireResponseItemComponent; -import org.hl7.fhir.dstu3.model.QuestionnaireResponse.QuestionnaireResponseStatus; -import org.hl7.fhir.dstu3.model.Reference; -import org.hl7.fhir.dstu3.model.Resource; -import org.hl7.fhir.dstu3.model.StringType; -import org.hl7.fhir.dstu3.model.TimeType; -import org.hl7.fhir.dstu3.model.Type; -import org.hl7.fhir.dstu3.model.UriType; -import org.hl7.fhir.dstu3.model.ValueSet; -import org.hl7.fhir.dstu3.model.ValueSet.ValueSetExpansionContainsComponent; -import org.hl7.fhir.utilities.validation.ValidationMessage; -import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; - -/** - * Validates that an instance of {@link QuestionnaireResponse} is valid against the {@link Questionnaire} that it claims - * to conform to. - * - * @author James Agnew - */ -public class QuestionnaireResponseValidator extends BaseValidator { - - /* - * ***************************************************************** Note to anyone working on this class - - * - * This class has unit tests which run within the HAPI project build. Please sync any changes here to HAPI and ensure - * that unit tests are run. **************************************************************** - */ - - private IWorkerContext myWorkerCtx; - - public QuestionnaireResponseValidator(IWorkerContext theWorkerCtx) { - this.myWorkerCtx = theWorkerCtx; - } - - private Set> allowedTypes(Class theClass0) { - return allowedTypes(theClass0, null); - } - - private Set> allowedTypes(Class theClass0, Class theClass1) { - HashSet> retVal = new HashSet>(); - retVal.add(theClass0); - if (theClass1 != null) { - retVal.add(theClass1); - } - return Collections.unmodifiableSet(retVal); - } - - private List findResponsesByLinkId(List theItem, String theLinkId) { - Validate.notBlank(theLinkId, "theLinkId must not be blank"); - - ArrayList retVal = new ArrayList(); - for (QuestionnaireResponseItemComponent next : theItem) { - if (theLinkId.equals(next.getLinkId())) { - retVal.add(next); - } - } - return retVal; - } - - public void validate(List theErrors, QuestionnaireResponse theAnswers) { - LinkedList pathStack = new LinkedList(); - pathStack.add("QuestionnaireResponse"); - pathStack.add(QuestionnaireResponse.SP_QUESTIONNAIRE); - - if (!fail(theErrors, IssueType.INVALID, pathStack, theAnswers.hasQuestionnaire(), "QuestionnaireResponse does not specity which questionnaire it is providing answers to")) { - return; - } - - Reference questionnaireRef = theAnswers.getQuestionnaire(); - Questionnaire questionnaire = getQuestionnaire(theAnswers, questionnaireRef); - if (!fail(theErrors, IssueType.INVALID, pathStack, questionnaire != null, "Questionnaire {0} is not found in the WorkerContext", theAnswers.getQuestionnaire().getReference())) { - return; - } - - QuestionnaireResponseStatus status = theAnswers.getStatus(); - boolean validateRequired = false; - if (status == QuestionnaireResponseStatus.COMPLETED || status == QuestionnaireResponseStatus.AMENDED) { - validateRequired = true; - } - - pathStack.removeLast(); - // pathStack.add("group(0)"); - validateItems(theErrors, questionnaire.getItem(), theAnswers.getItem(), pathStack, theAnswers, validateRequired); - } - - private Questionnaire getQuestionnaire(QuestionnaireResponse theAnswers, Reference theQuestionnaireRef) { - Questionnaire retVal; - String value = theQuestionnaireRef.getReferenceElement().getValue(); - if (theQuestionnaireRef.getReferenceElement().isLocal()) { - retVal = (Questionnaire) theQuestionnaireRef.getResource(); - if (retVal == null) { - for (Resource next : theAnswers.getContained()) { - if (value.equals(next.getId())) { - retVal = (Questionnaire) next; - } - } - } - } else { - retVal = myWorkerCtx.fetchResource(Questionnaire.class, value); - } - return retVal; - } - - private ValueSet getValueSet(QuestionnaireResponse theResponse, Reference theQuestionnaireRef) { - ValueSet retVal; - if (theQuestionnaireRef.getReferenceElement().isLocal()) { - retVal = (ValueSet) theQuestionnaireRef.getResource(); - if (retVal == null) { - for (Resource next : theResponse.getContained()) { - if (theQuestionnaireRef.getReferenceElement().getValue().equals(next.getId())) { - retVal = (ValueSet) next; - } - } - } - } else { - retVal = myWorkerCtx.fetchResource(ValueSet.class, theQuestionnaireRef.getReferenceElement().getValue()); - } - return retVal; - } - - private void validateGroup(List theErrors, QuestionnaireItemComponent theQuestGroup, QuestionnaireResponseItemComponent theRespGroup, LinkedList thePathStack, QuestionnaireResponse theResponse, boolean theValidateRequired) { - validateItems(theErrors, theQuestGroup.getItem(), theRespGroup.getItem(), thePathStack, theResponse, theValidateRequired); - } - - private void validateQuestion(List theErrors, QuestionnaireItemComponent theQuestion, QuestionnaireResponseItemComponent theRespGroup, LinkedList thePathStack, QuestionnaireResponse theResponse, boolean theValidateRequired) { - String linkId = theQuestion.getLinkId(); - if (!fail(theErrors, IssueType.INVALID, thePathStack, isNotBlank(linkId), "Questionnaire is invalid, question found with no link ID")) { - return; - } - - QuestionnaireItemType type = theQuestion.getType(); - if (type == null) { - rule(theErrors, IssueType.INVALID, thePathStack, false, "Questionnaire is invalid, no type specified for question with link ID[{0}]", linkId); - return; - } - - // List responses; - // if (theRespGroup == null) { - // responses = findResponsesByLinkId(theResponse.getItem(), linkId); - // } else { - // responses = findResponsesByLinkId(theRespGroup.getItem(), linkId); - // } - List responses = theRespGroup.getAnswer(); - - if (responses.size() > 1) { - rule(theErrors, IssueType.BUSINESSRULE, thePathStack, !theQuestion.getRepeats(), "Multiple answers found with linkId[{0}]", linkId); - } - if (responses.size() == 0) { - if (theValidateRequired) { - rule(theErrors, IssueType.BUSINESSRULE, thePathStack, !theQuestion.getRequired(), "Missing answer item for required item with linkId[{0}]", linkId); - } else { - hint(theErrors, IssueType.BUSINESSRULE, thePathStack, !theQuestion.getRequired(), "Missing answer item for required item with linkId[{0}]", linkId); - } - return; - } - - // QuestionnaireResponseItemComponent responseItem = responses.get(0); - try { - // thePathStack.add("item(" + responses.indexOf(responseItem) + ")"); - validateQuestionAnswers(theErrors, theQuestion, thePathStack, type, theRespGroup, theResponse, theValidateRequired); - } finally { - // thePathStack.removeLast(); - } - } - - private void validateItems(List theErrors, List theQuestionnaireItems, List theResponseItems, LinkedList thePathStack, QuestionnaireResponse theResponse, boolean theValidateRequired) { - Set allowedItems = new HashSet(); - for (QuestionnaireItemComponent nextQuestionnaireItem : theQuestionnaireItems) { - if (nextQuestionnaireItem.getType()== QuestionnaireItemType.NULL || nextQuestionnaireItem.getType() == null) { - rule(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Questionnaire definition contains item with no type"); - String linkId = nextQuestionnaireItem.getLinkId(); - if (isNotBlank(linkId)) { - // Just so that we don't also get a warning about the answer being present - allowedItems.add(linkId); - } - continue; - } - - if (!QuestionnaireItemType.DISPLAY.equals(nextQuestionnaireItem.getType())) { - String itemType = QuestionnaireItemType.GROUP.equals(nextQuestionnaireItem.getType()) ? "group" : "question"; - String linkId = nextQuestionnaireItem.getLinkId(); - if (isBlank(linkId)) { - rule(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Questionnaire definition contains {0} with no linkId", itemType); - continue; - } - allowedItems.add(linkId); - - List responseItems = findResponsesByLinkId(theResponseItems, linkId); - if (responseItems.isEmpty()) { - if (nextQuestionnaireItem.getRequired()) { - if (theValidateRequired) { - rule(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Missing required {0} with linkId[{1}]", itemType, linkId); - } else { - hint(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Missing required {0} with linkId[{1}]", itemType, linkId); - } - } - continue; - } - if (responseItems.size() > 1) { - if (nextQuestionnaireItem.getRepeats() == false) { - int index = theResponseItems.indexOf(responseItems.get(1)); - thePathStack.add("item(" + index + ")"); - rule(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Multiple repetitions of {0} with linkId[{1}] found at this position, but this item cannot repeat", itemType, linkId); - thePathStack.removeLast(); - } - } - for (QuestionnaireResponseItemComponent nextResponseItem : responseItems) { - int index = theResponseItems.indexOf(nextResponseItem); - thePathStack.add("item(" + index + ")"); - if (nextQuestionnaireItem.getType() == QuestionnaireItemType.GROUP) { - validateGroup(theErrors, nextQuestionnaireItem, nextResponseItem, thePathStack, theResponse, theValidateRequired); - } else { - validateQuestion(theErrors, nextQuestionnaireItem, nextResponseItem, thePathStack, theResponse, theValidateRequired); - } - thePathStack.removeLast(); - } - } - } - - // Make sure there are no items in response that aren't in the questionnaire - int idx = -1; - for (QuestionnaireResponseItemComponent next : theResponseItems) { - idx++; - if (!allowedItems.contains(next.getLinkId())) { - thePathStack.add("item(" + idx + ")"); - rule(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Item with linkId[{0}] found at this position, but this item does not exist at this position in Questionnaire", next.getLinkId()); - thePathStack.removeLast(); - } - } - } - - private void validateQuestionAnswers(List theErrors, QuestionnaireItemComponent theQuestion, LinkedList thePathStack, QuestionnaireItemType type, QuestionnaireResponseItemComponent responseQuestion, QuestionnaireResponse theResponse, boolean theValidateRequired) { - - String linkId = theQuestion.getLinkId(); - Set> allowedAnswerTypes = determineAllowedAnswerTypes(type); - if (allowedAnswerTypes.isEmpty()) { - rule(theErrors, IssueType.BUSINESSRULE, thePathStack, responseQuestion.isEmpty(), "Question with linkId[{0}] has no answer type but an answer was provided", linkId); - } else { - rule(theErrors, IssueType.BUSINESSRULE, thePathStack, !(responseQuestion.getAnswer().size() > 1 && !theQuestion.getRepeats()), "Multiple answers to non repeating question with linkId[{0}]", linkId); - if (theValidateRequired) { - rule(theErrors, IssueType.BUSINESSRULE, thePathStack, !(theQuestion.getRequired() && responseQuestion.getAnswer().isEmpty()), "Missing answer to required question with linkId[{0}]", linkId); - } else { - hint(theErrors, IssueType.BUSINESSRULE, thePathStack, !(theQuestion.getRequired() && responseQuestion.getAnswer().isEmpty()), "Missing answer to required question with linkId[{0}]", linkId); - } - } - - int answerIdx = -1; - for (QuestionnaireResponseItemAnswerComponent nextAnswer : responseQuestion.getAnswer()) { - answerIdx++; - try { - thePathStack.add("answer(" + answerIdx + ")"); - Type nextValue = nextAnswer.getValue(); - if (!allowedAnswerTypes.contains(nextValue.getClass())) { - rule(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Answer to question with linkId[{0}] found of type [{1}] but this is invalid for question of type [{2}]", linkId, nextValue.getClass().getSimpleName(), type.toCode()); - continue; - } - - // Validate choice answers - if (type == QuestionnaireItemType.CHOICE || type == QuestionnaireItemType.OPENCHOICE) { - if (nextAnswer.getValue() instanceof StringType) { - // n.b. we can only be here if it's an open-choice - String value = ((StringType)nextAnswer.getValue()).getValue(); - if (isBlank(value)) { - if (Boolean.TRUE.equals(theQuestion.getRequiredElement().getValue())) { - rule(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Answer to question with linkId[{0}] has no value but this item is required", linkId); - } - } - } else { - Coding coding = (Coding) nextAnswer.getValue(); - if (isBlank(coding.getCode()) && isBlank(coding.getDisplay()) && isBlank(coding.getSystem())) { - rule(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Answer to question with linkId[{0}] is of type coding, but none of code, system, and display are populated", linkId); - continue; - } else if (isBlank(coding.getCode()) && isBlank(coding.getSystem())) { - if (type != QuestionnaireItemType.OPENCHOICE) { - rule(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Answer to question with linkId[{0}] is of type only has a display populated (no code or system) but question does not allow {1}", linkId, QuestionnaireItemType.OPENCHOICE.name()); - continue; - } - } else if (isBlank(coding.getCode()) || isBlank(coding.getSystem())) { - rule(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Answer to question with linkId[{0}] has a coding, but this coding does not contain a code and system (both must be present, or neither as the question allows {1})", linkId, QuestionnaireItemType.OPENCHOICE.name()); - continue; - } - - String optionsRef = theQuestion.getOptions().getReference(); - if (isNotBlank(optionsRef)) { - ValueSet valueSet = getValueSet(theResponse, theQuestion.getOptions()); - if (valueSet == null) { - rule(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Question with linkId[{0}] has options ValueSet[{1}] but this ValueSet can not be found", linkId, optionsRef); - continue; - } - - boolean found = false; - for (ValueSetExpansionContainsComponent next : valueSet.getExpansion().getContains()) { - if (coding.getCode().equals(next.getCode()) && coding.getSystem().equals(next.getSystem())) { - found = true; - break; - } - } - - rule(theErrors, IssueType.BUSINESSRULE, thePathStack, found, "Question with linkId[{0}] has answer with system[{1}] and code[{2}] but this is not a valid answer for ValueSet[{3}]", linkId, coding.getSystem(), coding.getCode(), optionsRef); - } - } - } - - validateItems(theErrors, theQuestion.getItem(), nextAnswer.getItem(), thePathStack, theResponse, theValidateRequired); - - } finally { - thePathStack.removeLast(); - } - - } // for answers - } - - private Set> determineAllowedAnswerTypes(QuestionnaireItemType type) { - Set> allowedAnswerTypes; - switch (type) { - case ATTACHMENT: - allowedAnswerTypes = allowedTypes(Attachment.class); - break; - case BOOLEAN: - allowedAnswerTypes = allowedTypes(BooleanType.class); - break; - case CHOICE: - allowedAnswerTypes = allowedTypes(Coding.class); - break; - case DATE: - allowedAnswerTypes = allowedTypes(DateType.class); - break; - case DATETIME: - allowedAnswerTypes = allowedTypes(DateTimeType.class); - break; - case DECIMAL: - allowedAnswerTypes = allowedTypes(DecimalType.class); - break; - case INTEGER: - allowedAnswerTypes = allowedTypes(IntegerType.class); - break; - case OPENCHOICE: - allowedAnswerTypes = allowedTypes(Coding.class, StringType.class); - break; - case QUANTITY: - allowedAnswerTypes = allowedTypes(Quantity.class); - break; - case REFERENCE: - allowedAnswerTypes = allowedTypes(Reference.class); - break; - case STRING: - allowedAnswerTypes = allowedTypes(StringType.class); - break; - case TEXT: - allowedAnswerTypes = allowedTypes(StringType.class); - break; - case TIME: - allowedAnswerTypes = allowedTypes(TimeType.class); - break; - case URL: - allowedAnswerTypes = allowedTypes(UriType.class); - break; - case NULL: - default: - allowedAnswerTypes = Collections.emptySet(); - } - return allowedAnswerTypes; - } -} - diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/ValidationErrorHandler.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/ValidationErrorHandler.java deleted file mode 100644 index 6005bf68360..00000000000 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/ValidationErrorHandler.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.hl7.fhir.dstu3.validation; -/* -Copyright (c) 2011+, HL7, Inc -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name of HL7 nor the names of its contributors may be used to - endorse or promote products derived from this software without specific - prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, -INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. - -*/ - -import java.util.List; - -import org.hl7.fhir.utilities.validation.ValidationMessage; -import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; -import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; -import org.hl7.fhir.utilities.validation.ValidationMessage.Source; -import org.xml.sax.ErrorHandler; -import org.xml.sax.SAXException; -import org.xml.sax.SAXParseException; - -public class ValidationErrorHandler implements ErrorHandler { - - private List outputs; - private String path; - - public ValidationErrorHandler(List outputs, String path) { - this.outputs = outputs; - this.path = path; - } - - @Override -public void error(SAXParseException arg0) throws SAXException { - outputs.add(new ValidationMessage(Source.Schema, IssueType.INVALID, arg0.getLineNumber(), arg0.getColumnNumber(), path, arg0.getMessage(), IssueSeverity.ERROR)); - } - - @Override -public void fatalError(SAXParseException arg0) throws SAXException { - outputs.add(new ValidationMessage(Source.Schema, IssueType.INVALID, arg0.getLineNumber(), arg0.getColumnNumber(), path, arg0.getMessage(), IssueSeverity.FATAL)); - } - - @Override -public void warning(SAXParseException arg0) throws SAXException { - outputs.add(new ValidationMessage(Source.Schema, IssueType.INVALID, arg0.getLineNumber(), arg0.getColumnNumber(), path, arg0.getMessage(), IssueSeverity.WARNING)); - } - -} diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/ValidatorFrame.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/ValidatorFrame.java deleted file mode 100644 index b83c00f222d..00000000000 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/ValidatorFrame.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.hl7.fhir.dstu3.validation; - -import java.io.IOException; - -public class ValidatorFrame extends javax.swing.JFrame { - - public ValidatorFrame() throws IOException { - } - - -} diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/CachingValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/CachingValidationSupport.java index 3015b22abf3..1d12ce72cca 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/CachingValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/CachingValidationSupport.java @@ -66,6 +66,11 @@ public class CachingValidationSupport implements IValidationSupport { return myWrap.isCodeSystemSupported(theContext, theSystem); } + @Override + public StructureDefinition generateSnapshot(StructureDefinition theInput, String theUrl, String theProfileName) { + return myWrap.generateSnapshot(theInput, theUrl, theProfileName); + } + @Override public CodeValidationResult validateCode(FhirContext theContext, String theCodeSystem, String theCode, String theDisplay) { return myWrap.validateCode(theContext, theCodeSystem, theCode, theDisplay); diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/PrePopulatedValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/PrePopulatedValidationSupport.java index d27c3afc054..a940490c44c 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/PrePopulatedValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/PrePopulatedValidationSupport.java @@ -182,7 +182,12 @@ public class PrePopulatedValidationSupport implements IValidationSupport { return false; } - @Override + @Override + public StructureDefinition generateSnapshot(StructureDefinition theInput, String theUrl, String theProfileName) { + return null; + } + + @Override public CodeValidationResult validateCode(FhirContext theContext, String theCodeSystem, String theCode, String theDisplay) { return null; } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/SnapshotGeneratingValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/SnapshotGeneratingValidationSupport.java new file mode 100644 index 00000000000..e81b5d2f735 --- /dev/null +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/SnapshotGeneratingValidationSupport.java @@ -0,0 +1,140 @@ +package org.hl7.fhir.r4.hapi.validation; + +import ca.uhn.fhir.context.*; +import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; +import org.apache.commons.lang3.Validate; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.conformance.ProfileUtilities; +import org.hl7.fhir.r4.context.IWorkerContext; +import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext; +import org.hl7.fhir.r4.hapi.ctx.IValidationSupport; +import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.ElementDefinition; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.ValueSet; +import org.hl7.fhir.r4.terminologies.ValueSetExpander; +import org.hl7.fhir.utilities.validation.ValidationMessage; + +import java.util.ArrayList; +import java.util.List; + +/** + * Simple validation support module that handles profile snapshot generation. This is + * separate from other funcrtions since it needs a link to a validation support + * module itself, and it is useful to be able to pass a chain in. + */ +public class SnapshotGeneratingValidationSupport implements IValidationSupport { + private final FhirContext myCtx; + private final IValidationSupport myValidationSupport; + + public SnapshotGeneratingValidationSupport(FhirContext theCtx, IValidationSupport theValidationSupport) { + Validate.notNull(theCtx); + Validate.notNull(theValidationSupport); + myCtx = theCtx; + myValidationSupport = theValidationSupport; + } + + @Override + public ValueSetExpander.ValueSetExpansionOutcome expandValueSet(FhirContext theContext, ValueSet.ConceptSetComponent theInclude) { + return null; + } + + @Override + public List fetchAllConformanceResources(FhirContext theContext) { + return null; + } + + @Override + public List fetchAllStructureDefinitions(FhirContext theContext) { + return null; + } + + @Override + public CodeSystem fetchCodeSystem(FhirContext theContext, String uri) { + return null; + } + + @Override + public ValueSet fetchValueSet(FhirContext theContext, String uri) { + return null; + } + + @Override + public T fetchResource(FhirContext theContext, Class theClass, String theUri) { + return null; + } + + @Override + public StructureDefinition fetchStructureDefinition(FhirContext theCtx, String theUrl) { + return null; + } + + @Override + public boolean isCodeSystemSupported(FhirContext theContext, String theSystem) { + return false; + } + + @Override + public StructureDefinition generateSnapshot(StructureDefinition theInput, String theUrl, String theProfileName) { + IWorkerContext context = new HapiWorkerContext(myCtx, myValidationSupport); + ProfileUtilities.ProfileKnowledgeProvider profileKnowledgeProvider = new MyProfileKnowledgeWorker(); + ArrayList messages = new ArrayList<>(); + + StructureDefinition base = myValidationSupport.fetchStructureDefinition(myCtx, theInput.getBaseDefinition()); + if (base == null) { + throw new PreconditionFailedException("Unknown base definition: " + theInput.getBaseDefinition()); + } + + new ProfileUtilities(context, messages, profileKnowledgeProvider).generateSnapshot(base, theInput, theUrl, theProfileName); + + return theInput; + } + + @Override + public CodeValidationResult validateCode(FhirContext theContext, String theCodeSystem, String theCode, String theDisplay) { + return null; + } + + private class MyProfileKnowledgeWorker implements ProfileUtilities.ProfileKnowledgeProvider { + @Override + public boolean isDatatype(String typeSimple) { + BaseRuntimeElementDefinition def = myCtx.getElementDefinition(typeSimple); + Validate.notNull(typeSimple); + return (def instanceof RuntimePrimitiveDatatypeDefinition) || (def instanceof RuntimeCompositeDatatypeDefinition); + } + + @Override + public boolean isResource(String typeSimple) { + BaseRuntimeElementDefinition def = myCtx.getElementDefinition(typeSimple); + Validate.notNull(typeSimple); + return def instanceof RuntimeResourceDefinition; + } + + @Override + public boolean hasLinkFor(String typeSimple) { + return false; + } + + @Override + public String getLinkFor(String corePath, String typeSimple) { + return null; + } + + @Override + public BindingResolution resolveBinding(org.hl7.fhir.r4.model.StructureDefinition def, ElementDefinition.ElementDefinitionBindingComponent binding, String path) throws FHIRException { + return null; + } + + @Override + public String getLinkForProfile(org.hl7.fhir.r4.model.StructureDefinition profile, String url) { + return null; + } + + @Override + public boolean prependLinks() { + return false; + } + } + +} diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/ValidationSupportChain.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/ValidationSupportChain.java index 5dade765e8a..f5ab6b533fa 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/ValidationSupportChain.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/ValidationSupportChain.java @@ -2,7 +2,6 @@ package org.hl7.fhir.r4.hapi.validation; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import org.hl7.fhir.exceptions.TerminologyServiceException; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.hapi.ctx.IValidationSupport; import org.hl7.fhir.r4.model.CodeSystem; @@ -28,7 +27,7 @@ public class ValidationSupportChain implements IValidationSupport { * Constructor */ public ValidationSupportChain() { - myChain = new ArrayList(); + myChain = new ArrayList<>(); } /** @@ -61,19 +60,19 @@ public class ValidationSupportChain implements IValidationSupport { throw new InvalidRequestException("unable to find code system " + theInclude.getSystem()); } - @Override - public List fetchAllConformanceResources(FhirContext theContext) { - List retVal = new ArrayList<>(); - for (IValidationSupport next : myChain) { - List candidates = next.fetchAllConformanceResources(theContext); - if (candidates != null) { - retVal.addAll(candidates); - } - } - return retVal; - } + @Override + public List fetchAllConformanceResources(FhirContext theContext) { + List retVal = new ArrayList<>(); + for (IValidationSupport next : myChain) { + List candidates = next.fetchAllConformanceResources(theContext); + if (candidates != null) { + retVal.addAll(candidates); + } + } + return retVal; + } - @Override + @Override public CodeSystem fetchCodeSystem(FhirContext theCtx, String theSystem) { for (IValidationSupport next : myChain) { if (next.isCodeSystemSupported(theCtx, theSystem)) { @@ -129,11 +128,23 @@ public class ValidationSupportChain implements IValidationSupport { return false; } + @Override + public StructureDefinition generateSnapshot(StructureDefinition theInput, String theUrl, String theProfileName) { + StructureDefinition outcome = null; + for (IValidationSupport next : myChain) { + outcome = next.generateSnapshot(theInput, theUrl, theProfileName); + if (outcome != null) { + break; + } + } + return outcome; + } + @Override public CodeValidationResult validateCode(FhirContext theCtx, String theCodeSystem, String theCode, String theDisplay) { - + ourLog.debug("Validating code {} in chain with {} items", theCode, myChain.size()); - + for (IValidationSupport next : myChain) { if (next.isCodeSystemSupported(theCtx, theCodeSystem)) { CodeValidationResult result = next.validateCode(theCtx, theCodeSystem, theCode, theDisplay); @@ -150,12 +161,15 @@ public class ValidationSupportChain implements IValidationSupport { @Override public List fetchAllStructureDefinitions(FhirContext theContext) { - ArrayList retVal = new ArrayList(); - Set urls= new HashSet(); + ArrayList retVal = new ArrayList<>(); + Set urls = new HashSet<>(); for (IValidationSupport nextSupport : myChain) { - for (StructureDefinition next : nextSupport.fetchAllStructureDefinitions(theContext)) { - if (isBlank(next.getUrl()) || urls.add(next.getUrl())) { - retVal.add(next); + List list = nextSupport.fetchAllStructureDefinitions(theContext); + if (list != null) { + for (StructureDefinition next : list) { + if (isBlank(next.getUrl()) || urls.add(next.getUrl())) { + retVal.add(next); + } } } } diff --git a/hapi-tinder-plugin/src/main/resources/vm/jpa_resource_provider.vm b/hapi-tinder-plugin/src/main/resources/vm/jpa_resource_provider.vm index e2c3d23def3..4f14f481622 100644 --- a/hapi-tinder-plugin/src/main/resources/vm/jpa_resource_provider.vm +++ b/hapi-tinder-plugin/src/main/resources/vm/jpa_resource_provider.vm @@ -24,7 +24,7 @@ import ca.uhn.fhir.rest.api.SearchTotalModeEnum; public class ${className}ResourceProvider extends ## We have specialized base classes for RPs that handle certain resource types. These ## RPs implement type specific operations -#if ( $version != 'dstu' && (${className} == 'Encounter' || ${className} == 'Patient' || ${className} == 'ValueSet' || ${className} == 'QuestionnaireAnswers' || ${className} == 'CodeSystem' || ($version != 'dstu2' && ${className} == 'ConceptMap') || ${className} == 'MessageHeader' || ${className} == 'Composition')) +#if ( $version != 'dstu' && (${className} == 'Encounter' || ${className} == 'Patient' || ${className} == 'ValueSet' || ${className} == 'QuestionnaireAnswers' || ${className} == 'CodeSystem' || ($version != 'dstu2' && ${className} == 'ConceptMap') || ${className} == 'MessageHeader' || ${className} == 'Composition' || ${className} == 'StructureDefinition')) BaseJpaResourceProvider${className}${versionCapitalized} #else JpaResourceProvider${versionCapitalized}<${className}> diff --git a/hapi-tinder-plugin/src/main/resources/vm/jpa_spring_beans_java.vm b/hapi-tinder-plugin/src/main/resources/vm/jpa_spring_beans_java.vm index 04733b01df9..208dbb82224 100644 --- a/hapi-tinder-plugin/src/main/resources/vm/jpa_spring_beans_java.vm +++ b/hapi-tinder-plugin/src/main/resources/vm/jpa_spring_beans_java.vm @@ -66,7 +66,7 @@ public abstract class BaseJavaConfig${versionCapitalized} extends ca.uhn.fhir.jp IFhirResourceDaoConceptMap #elseif ( ${versionCapitalized} == 'R4' && ${res.name} == 'ConceptMap' ) IFhirResourceDaoConceptMap -#elseif ( ${versionCapitalized} != 'Dstu1' && (${res.name} == 'Composition' || ${res.name} == 'Encounter' || ${res.name} == 'Everything' || ${res.name} == 'Patient' || ${res.name} == 'Subscription' || ${res.name} == 'SearchParameter' || ${res.name} == 'MessageHeader')) +#elseif ( ${versionCapitalized} != 'Dstu1' && (${res.name} == 'Composition' || ${res.name} == 'Encounter' || ${res.name} == 'Everything' || ${res.name} == 'Patient' || ${res.name} == 'Subscription' || ${res.name} == 'SearchParameter' || ${res.name} == 'MessageHeader' || ${res.name} == 'StructureDefinition')) IFhirResourceDao${res.name}<${resourcePackage}.${res.declaringClassNameComplete}> #else IFhirResourceDao<${resourcePackage}.${res.declaringClassNameComplete}> @@ -76,7 +76,7 @@ public abstract class BaseJavaConfig${versionCapitalized} extends ca.uhn.fhir.jp ca.uhn.fhir.jpa.dao${package_suffix}.FhirResourceDao${res.name}${versionCapitalized} retVal = new ca.uhn.fhir.jpa.dao${package_suffix}.FhirResourceDao${res.name}${versionCapitalized}(); #elseif ( ${versionCapitalized} == 'R4' && ${res.name} == 'ConceptMap' ) ca.uhn.fhir.jpa.dao${package_suffix}.FhirResourceDao${res.name}${versionCapitalized} retVal = new ca.uhn.fhir.jpa.dao${package_suffix}.FhirResourceDao${res.name}${versionCapitalized}(); -#elseif ( ${res.name} == 'Bundle' || ${res.name} == 'Encounter' || ${res.name} == 'Everything' || ${res.name} == 'Patient' || ${res.name} == 'Subscription' || ${res.name} == 'ValueSet' || ${res.name} == 'QuestionnaireResponse' || ${res.name} == 'SearchParameter' || ${res.name} == 'CodeSystem' || ${res.name} == 'MessageHeader' || ${res.name} == 'Composition') +#elseif ( ${res.name} == 'Bundle' || ${res.name} == 'Encounter' || ${res.name} == 'Everything' || ${res.name} == 'Patient' || ${res.name} == 'Subscription' || ${res.name} == 'ValueSet' || ${res.name} == 'QuestionnaireResponse' || ${res.name} == 'SearchParameter' || ${res.name} == 'CodeSystem' || ${res.name} == 'MessageHeader' || ${res.name} == 'Composition' || ${res.name} == 'StructureDefinition') ca.uhn.fhir.jpa.dao${package_suffix}.FhirResourceDao${res.name}${versionCapitalized} retVal = new ca.uhn.fhir.jpa.dao${package_suffix}.FhirResourceDao${res.name}${versionCapitalized}(); #else ca.uhn.fhir.jpa.dao${package_suffix}.FhirResourceDao${versionCapitalized}<${resourcePackage}.${res.declaringClassNameComplete}> retVal = new ca.uhn.fhir.jpa.dao${package_suffix}.FhirResourceDao${versionCapitalized}<${resourcePackage}.${res.declaringClassNameComplete}>(); diff --git a/pom.xml b/pom.xml index 15e0d47d6b6..72516a69feb 100755 --- a/pom.xml +++ b/pom.xml @@ -880,7 +880,7 @@ mysql mysql-connector-java - 8.0.12 + 8.0.16 net.sf.json-lib diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 0391a884ddf..7c9f875844c 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -279,6 +279,11 @@ Added a new Pointcut STORAGE_PRESTORAGE_EXPUNGE_EVERYTHING that is called at the start of the expungeEverything operation. + + The JPA server now has the ability to generate snapshot profiles from differential + profiles via the $snapshot operation, and will automatically generate a snapshot when + needed for validation. +