Add validation rules interceptor (#2264)
* Start work on validation rules interceptor * Work on interceptor * Add tests * Work on validation interceptor * Work on interceptor * Test fix * Documentation tweaks
This commit is contained in:
parent
84d9854e24
commit
8f8c5c31f1
|
@ -28,6 +28,7 @@ import ca.uhn.fhir.interceptor.api.IInterceptorService;
|
|||
import ca.uhn.fhir.interceptor.api.Interceptor;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import ca.uhn.fhir.util.ReflectionUtil;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
|
@ -567,7 +568,7 @@ public class InterceptorService implements IInterceptorService, IInterceptorBroa
|
|||
*/
|
||||
private static List<HookInvoker> scanInterceptorForHookMethods(Object theInterceptor, int theTypeOrder) {
|
||||
ArrayList<HookInvoker> retVal = new ArrayList<>();
|
||||
for (Method nextMethod : theInterceptor.getClass().getMethods()) {
|
||||
for (Method nextMethod : ReflectionUtil.getDeclaredMethods(theInterceptor.getClass(), true)) {
|
||||
Optional<Hook> hook = findAnnotation(nextMethod, Hook.class);
|
||||
|
||||
if (hook.isPresent()) {
|
||||
|
|
|
@ -78,7 +78,10 @@ public abstract class BaseServerResponseException extends RuntimeException {
|
|||
* @param theStatusCode The HTTP status code corresponding to this problem
|
||||
* @param theMessage The message
|
||||
*/
|
||||
public BaseServerResponseException(int theStatusCode, String theMessage) {
|
||||
public /**
|
||||
* Interceptor hook method. This method should not be called directly.
|
||||
*/
|
||||
BaseServerResponseException(int theStatusCode, String theMessage) {
|
||||
super(theMessage);
|
||||
myStatusCode = theStatusCode;
|
||||
myBaseOperationOutcome = null;
|
||||
|
|
|
@ -31,6 +31,8 @@ import org.hl7.fhir.instance.model.api.IBaseBundle;
|
|||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* This class can be used to build a Bundle resource to be used as a FHIR transaction. Convenience methods provide
|
||||
* support for setting various bundle fields and working with bundle parts such as metadata and entry
|
||||
|
@ -72,8 +74,6 @@ public class BundleBuilder {
|
|||
myBundleDef = myContext.getResourceDefinition("Bundle");
|
||||
myBundle = (IBaseBundle) myBundleDef.newInstance();
|
||||
|
||||
setBundleField("type", "transaction");
|
||||
|
||||
myEntryChild = myBundleDef.getChildByName("entry");
|
||||
myEntryDef = myEntryChild.getChildByName("entry");
|
||||
|
||||
|
@ -144,11 +144,14 @@ public class BundleBuilder {
|
|||
}
|
||||
|
||||
/**
|
||||
* Adds an entry containing an update (PUT) request
|
||||
* Adds an entry containing an update (PUT) request.
|
||||
* Also sets the Bundle.type value to "transaction" if it is not already set.
|
||||
*
|
||||
* @param theResource The resource to update
|
||||
*/
|
||||
public UpdateBuilder addUpdateEntry(IBaseResource theResource) {
|
||||
public UpdateBuilder addTransactionUpdateEntry(IBaseResource theResource) {
|
||||
setBundleField("type", "transaction");
|
||||
|
||||
IBase request = addEntryAndReturnRequest(theResource);
|
||||
|
||||
// Bundle.entry.request.url
|
||||
|
@ -165,11 +168,14 @@ public class BundleBuilder {
|
|||
}
|
||||
|
||||
/**
|
||||
* Adds an entry containing an create (POST) request
|
||||
* Adds an entry containing an create (POST) request.
|
||||
* Also sets the Bundle.type value to "transaction" if it is not already set.
|
||||
*
|
||||
* @param theResource The resource to create
|
||||
*/
|
||||
public CreateBuilder addCreateEntry(IBaseResource theResource) {
|
||||
setBundleField("type", "transaction");
|
||||
|
||||
IBase request = addEntryAndReturnRequest(theResource);
|
||||
|
||||
String resourceType = myContext.getResourceType(theResource);
|
||||
|
@ -212,6 +218,11 @@ public class BundleBuilder {
|
|||
return (IBaseBackboneElement) searchInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param theResource
|
||||
* @return
|
||||
*/
|
||||
public IBase addEntryAndReturnRequest(IBaseResource theResource) {
|
||||
Validate.notNull(theResource, "theResource must not be null");
|
||||
|
||||
|
@ -310,7 +321,7 @@ public class BundleBuilder {
|
|||
return retVal;
|
||||
}
|
||||
|
||||
public class UpdateBuilder {
|
||||
public static class UpdateBuilder {
|
||||
|
||||
private final IPrimitiveType<?> myUrl;
|
||||
|
||||
|
@ -338,7 +349,8 @@ public class BundleBuilder {
|
|||
* Make this create a Conditional Create
|
||||
*/
|
||||
public void conditional(String theConditionalUrl) {
|
||||
IPrimitiveType<?> ifNoneExist = (IPrimitiveType<?>) myContext.getElementDefinition("string").newInstance();
|
||||
BaseRuntimeElementDefinition<?> stringDefinition = Objects.requireNonNull(myContext.getElementDefinition("string"));
|
||||
IPrimitiveType<?> ifNoneExist = (IPrimitiveType<?>) stringDefinition.newInstance();
|
||||
ifNoneExist.setValueAsString(theConditionalUrl);
|
||||
|
||||
myEntryRequestIfNoneExistChild.getMutator().setValue(myRequest, ifNoneExist);
|
||||
|
|
|
@ -32,7 +32,9 @@ import java.lang.reflect.Type;
|
|||
import java.lang.reflect.TypeVariable;
|
||||
import java.lang.reflect.WildcardType;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
|
@ -45,25 +47,64 @@ public class ReflectionUtil {
|
|||
private static final ConcurrentHashMap<String, Object> ourFhirServerVersions = new ConcurrentHashMap<>();
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ReflectionUtil.class);
|
||||
|
||||
/**
|
||||
* Non instantiable
|
||||
*/
|
||||
private ReflectionUtil() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all methods declared against {@literal theClazz}. This method returns a predictable order, which is
|
||||
* sorted by method name and then by parameters.
|
||||
* <p>
|
||||
* This method does not include superclass methods (see {@link #getDeclaredMethods(Class, boolean)} if you
|
||||
* want to include those.
|
||||
* </p>
|
||||
*/
|
||||
public static List<Method> getDeclaredMethods(Class<?> theClazz) {
|
||||
return getDeclaredMethods(theClazz, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all methods declared against {@literal theClazz}. This method returns a predictable order, which is
|
||||
* sorted by method name and then by parameters.
|
||||
*/
|
||||
public static List<Method> getDeclaredMethods(Class<?> theClazz) {
|
||||
HashSet<Method> foundMethods = new LinkedHashSet<>();
|
||||
public static List<Method> getDeclaredMethods(Class<?> theClazz, boolean theIncludeMethodsFromSuperclasses) {
|
||||
HashMap<String, Method> foundMethods = new HashMap<>();
|
||||
|
||||
populateDeclaredMethodsMap(theClazz, foundMethods, theIncludeMethodsFromSuperclasses);
|
||||
|
||||
List<Method> retVal = new ArrayList<>(foundMethods.values());
|
||||
retVal.sort((Comparator.comparing(ReflectionUtil::describeMethodInSortFriendlyWay)));
|
||||
return retVal;
|
||||
}
|
||||
|
||||
private static void populateDeclaredMethodsMap(Class<?> theClazz, HashMap<String, Method> foundMethods, boolean theIncludeMethodsFromSuperclasses) {
|
||||
Method[] declaredMethods = theClazz.getDeclaredMethods();
|
||||
for (Method next : declaredMethods) {
|
||||
try {
|
||||
Method method = theClazz.getMethod(next.getName(), next.getParameterTypes());
|
||||
foundMethods.add(method);
|
||||
} catch (NoSuchMethodException | SecurityException e) {
|
||||
foundMethods.add(next);
|
||||
|
||||
if (Modifier.isAbstract(next.getModifiers()) ||
|
||||
Modifier.isStatic(next.getModifiers()) ||
|
||||
Modifier.isPrivate(next.getModifiers())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String description = next.getName() + Arrays.asList(next.getParameterTypes());
|
||||
|
||||
if (!foundMethods.containsKey(description)) {
|
||||
try {
|
||||
Method method = theClazz.getMethod(next.getName(), next.getParameterTypes());
|
||||
foundMethods.put(description, method);
|
||||
} catch (NoSuchMethodException | SecurityException e) {
|
||||
foundMethods.put(description, next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Method> retVal = new ArrayList<>(foundMethods);
|
||||
retVal.sort((Comparator.comparing(ReflectionUtil::describeMethodInSortFriendlyWay)));
|
||||
return retVal;
|
||||
if (theIncludeMethodsFromSuperclasses && !theClazz.getSuperclass().equals(Object.class)) {
|
||||
populateDeclaredMethodsMap(theClazz.getSuperclass(), foundMethods, theIncludeMethodsFromSuperclasses);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,6 +8,7 @@ import ca.uhn.fhir.rest.api.Constants;
|
|||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import com.google.common.escape.Escaper;
|
||||
import com.google.common.net.PercentEscaper;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.utils.URLEncodedUtils;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
|
@ -16,6 +17,8 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
|||
import javax.annotation.Nonnull;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.ArrayList;
|
||||
|
@ -29,6 +32,7 @@ import java.util.stream.Collectors;
|
|||
|
||||
import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
|
||||
import static org.apache.commons.lang3.StringUtils.defaultString;
|
||||
import static org.apache.commons.lang3.StringUtils.endsWith;
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
|
||||
/*
|
||||
|
@ -300,6 +304,27 @@ public class UrlUtil {
|
|||
return toQueryStringMap(map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes canonical URLs for comparison. Trailing "/" is stripped,
|
||||
* and any version identifiers or fragment hash is removed
|
||||
*/
|
||||
public static String normalizeCanonicalUrlForComparison(String theUrl) {
|
||||
String retVal;
|
||||
try {
|
||||
retVal = new URI(theUrl).normalize().toString();
|
||||
} catch (URISyntaxException e) {
|
||||
retVal = theUrl;
|
||||
}
|
||||
while (endsWith(retVal, "/")) {
|
||||
retVal = retVal.substring(0, retVal.length() - 1);
|
||||
}
|
||||
int hashOrPipeIndex = StringUtils.indexOfAny(retVal, '#', '|');
|
||||
if (hashOrPipeIndex != -1) {
|
||||
retVal = retVal.substring(0, hashOrPipeIndex);
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a URL in one of the following forms:
|
||||
* <ul>
|
||||
|
|
|
@ -183,3 +183,6 @@ ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor.
|
|||
ca.uhn.fhir.jpa.dao.HistoryBuilder.noSystemOrTypeHistoryForPartitionAwareServer=Type- and Server- level history operation not supported across partitions on partitioned server
|
||||
|
||||
ca.uhn.fhir.jpa.provider.DiffProvider.cantDiffDifferentTypes=Unable to diff two resources of different types
|
||||
|
||||
ca.uhn.fhir.jpa.interceptor.validation.RuleRequireProfileDeclaration.noMatchingProfile=Resource of type "{0}" does not declare conformance to profile from: {1}
|
||||
ca.uhn.fhir.jpa.interceptor.validation.RuleRequireProfileDeclaration.illegalProfile=Resource of type "{0}" must not declare conformance to profile: {1}
|
||||
|
|
|
@ -97,6 +97,75 @@ public class InterceptorServiceTest {
|
|||
assertEquals("1", ((InvalidRequestException) response).getMessage());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Hook methods with private access are ignored
|
||||
*/
|
||||
@Test
|
||||
public void testInterceptorWithPrivateAccessHookMethod() {
|
||||
|
||||
class InterceptorThrowingException {
|
||||
@Hook(Pointcut.TEST_RB)
|
||||
private void test(String theValue) {
|
||||
throw new AuthenticationException(theValue);
|
||||
}
|
||||
}
|
||||
|
||||
InterceptorService svc = new InterceptorService();
|
||||
svc.registerInterceptor(new InterceptorThrowingException());
|
||||
|
||||
// Should not fail
|
||||
svc.callHooks(Pointcut.TEST_RB, new HookParams("A MESSAGE", "B"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInterceptorWithDefaultAccessHookMethod() {
|
||||
|
||||
class InterceptorThrowingException {
|
||||
@Hook(Pointcut.TEST_RB)
|
||||
void test(String theValue) {
|
||||
throw new AuthenticationException(theValue);
|
||||
}
|
||||
}
|
||||
|
||||
InterceptorService svc = new InterceptorService();
|
||||
svc.registerInterceptor(new InterceptorThrowingException());
|
||||
|
||||
try {
|
||||
svc.callHooks(Pointcut.TEST_RB, new HookParams("A MESSAGE", "B"));
|
||||
fail();
|
||||
} catch (AuthenticationException e) {
|
||||
assertEquals("A MESSAGE", e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInterceptorWithInheritedHookMethod() {
|
||||
|
||||
class InterceptorThrowingException {
|
||||
@Hook(Pointcut.TEST_RB)
|
||||
void test(String theValue) {
|
||||
throw new AuthenticationException(theValue);
|
||||
}
|
||||
}
|
||||
|
||||
class InterceptorThrowingException2 extends InterceptorThrowingException {
|
||||
// nothing
|
||||
}
|
||||
|
||||
InterceptorService svc = new InterceptorService();
|
||||
svc.registerInterceptor(new InterceptorThrowingException2());
|
||||
|
||||
try {
|
||||
svc.callHooks(Pointcut.TEST_RB, new HookParams("A MESSAGE", "B"));
|
||||
fail();
|
||||
} catch (AuthenticationException e) {
|
||||
assertEquals("A MESSAGE", e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegisterHookFails() {
|
||||
InterceptorService svc = new InterceptorService();
|
||||
|
|
|
@ -6,6 +6,16 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||
|
||||
public class UrlUtilTest {
|
||||
|
||||
@Test
|
||||
public void testNormalizeCanonicalUrl() {
|
||||
assertEquals("http://foo", UrlUtil.normalizeCanonicalUrlForComparison("http://foo/"));
|
||||
assertEquals("http://foo", UrlUtil.normalizeCanonicalUrlForComparison("http://foo"));
|
||||
assertEquals("http://foo", UrlUtil.normalizeCanonicalUrlForComparison("http://foo|1.23"));
|
||||
assertEquals("http://foo", UrlUtil.normalizeCanonicalUrlForComparison("http://foo|1.23#333"));
|
||||
assertEquals("abc", UrlUtil.normalizeCanonicalUrlForComparison("abc"));
|
||||
assertEquals("abc", UrlUtil.normalizeCanonicalUrlForComparison("abc/"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConstructAbsoluteUrl() {
|
||||
assertEquals("http://foo/bar/baz", UrlUtil.constructAbsoluteUrl(null, "http://foo/bar/baz"));
|
||||
|
|
|
@ -50,6 +50,11 @@
|
|||
<artifactId>hapi-fhir-jaxrsserver-base</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-jpaserver-base</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||
|
|
|
@ -33,7 +33,7 @@ import java.util.Date;
|
|||
import java.util.UUID;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class TransactionBuilderExamples {
|
||||
public class BundleBuilderExamples {
|
||||
|
||||
private FhirContext myFhirContext;
|
||||
private IGenericClient myFhirClient;
|
||||
|
@ -49,7 +49,7 @@ public class TransactionBuilderExamples {
|
|||
patient.setActive(true);
|
||||
|
||||
// Add the patient as an update (aka PUT) to the Bundle
|
||||
builder.addUpdateEntry(patient);
|
||||
builder.addTransactionUpdateEntry(patient);
|
||||
|
||||
// Execute the transaction
|
||||
IBaseBundle outcome = myFhirClient.transaction().withBundle(builder.getBundle()).execute();
|
||||
|
@ -67,7 +67,7 @@ public class TransactionBuilderExamples {
|
|||
patient.addIdentifier().setSystem("http://foo").setValue("bar");
|
||||
|
||||
// Add the patient as an update (aka PUT) to the Bundle
|
||||
builder.addUpdateEntry(patient).conditional("Patient?identifier=http://foo|bar");
|
||||
builder.addTransactionUpdateEntry(patient).conditional("Patient?identifier=http://foo|bar");
|
||||
|
||||
// Execute the transaction
|
||||
IBaseBundle outcome = myFhirClient.transaction().withBundle(builder.getBundle()).execute();
|
|
@ -0,0 +1,103 @@
|
|||
package ca.uhn.hapi.fhir.docs;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - Docs
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2020 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.context.FhirContext;
|
||||
import ca.uhn.fhir.interceptor.executor.InterceptorService;
|
||||
import ca.uhn.fhir.jpa.interceptor.validation.IRepositoryValidatingRule;
|
||||
import ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingInterceptor;
|
||||
import ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingRuleBuilder;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class RepositoryValidatingInterceptorExamples {
|
||||
|
||||
private ApplicationContext myAppCtx;
|
||||
private FhirContext myFhirCtx;
|
||||
private InterceptorService myInterceptorService;
|
||||
|
||||
public void createSimpleRule() {
|
||||
//START SNIPPET: createSimpleRule
|
||||
// First you must ask the Spring Application Context for a rule builder
|
||||
RepositoryValidatingRuleBuilder ruleBuilder = myAppCtx.getBean(RepositoryValidatingRuleBuilder.class);
|
||||
|
||||
// Add a simple rule requiring all Patient resources to declare conformance to the US Core
|
||||
// Patient Profile, and to validate successfully.
|
||||
ruleBuilder
|
||||
.forResourcesOfType("Patient")
|
||||
.requireAtLeastProfile("http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient")
|
||||
.and()
|
||||
.requireValidationToDeclaredProfiles();
|
||||
|
||||
// Build the rule list
|
||||
List<IRepositoryValidatingRule> rules = ruleBuilder.build();
|
||||
|
||||
// Create and register the interceptor
|
||||
RepositoryValidatingInterceptor interceptor = new RepositoryValidatingInterceptor(myFhirCtx, rules);
|
||||
myInterceptorService.registerInterceptor(interceptor);
|
||||
//END SNIPPET: createSimpleRule
|
||||
}
|
||||
|
||||
public void requireProfileDeclarations() {
|
||||
RepositoryValidatingRuleBuilder ruleBuilder = myAppCtx.getBean(RepositoryValidatingRuleBuilder.class);
|
||||
|
||||
//START SNIPPET: requireProfileDeclarations
|
||||
// Require Patient resources to declare conformance to US Core patient profile
|
||||
ruleBuilder
|
||||
.forResourcesOfType("Patient")
|
||||
.requireAtLeastProfile("http://www.hl7.org/fhir/us/core/StructureDefinition-us-core-patient.html");
|
||||
|
||||
// Require Patient resources to declare conformance to either the US Core patient profile
|
||||
// or the UK Core patient profile
|
||||
ruleBuilder
|
||||
.forResourcesOfType("Patient")
|
||||
.requireAtLeastOneProfileOf(
|
||||
"http://www.hl7.org/fhir/us/core/StructureDefinition-us-core-patient.html",
|
||||
"https://fhir.nhs.uk/R4/StructureDefinition/UKCore-Patient");
|
||||
//END SNIPPET: requireProfileDeclarations
|
||||
}
|
||||
|
||||
public void requireValidationToDeclaredProfiles() {
|
||||
RepositoryValidatingRuleBuilder ruleBuilder = myAppCtx.getBean(RepositoryValidatingRuleBuilder.class);
|
||||
|
||||
//START SNIPPET: requireValidationToDeclaredProfiles
|
||||
// Require Patient resources to validate to any declared profiles
|
||||
ruleBuilder
|
||||
.forResourcesOfType("Patient")
|
||||
.requireValidationToDeclaredProfiles();
|
||||
//END SNIPPET: requireValidationToDeclaredProfiles
|
||||
}
|
||||
|
||||
|
||||
public void disallowProfiles() {
|
||||
RepositoryValidatingRuleBuilder ruleBuilder = myAppCtx.getBean(RepositoryValidatingRuleBuilder.class);
|
||||
|
||||
//START SNIPPET: disallowProfiles
|
||||
// No UK Core patients allowed!
|
||||
ruleBuilder
|
||||
.forResourcesOfType("Patient")
|
||||
.disallowProfile("https://fhir.nhs.uk/R4/StructureDefinition/UKCore-Patient");
|
||||
//END SNIPPET: disallowProfiles
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
type: add
|
||||
issue: 2264
|
||||
title: "The interceptor framework will now recognize and invoke `@Hook` methods that have an access level
|
||||
of public, protected, or default. Previously only public methods were recognized."
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
type: change
|
||||
issue: 2264
|
||||
title: "The experimental `TransactionBuilder` helper class has been renamed to `BundleBuilder` as it now has
|
||||
utility methods for working with other types of Bundles too."
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
type: add
|
||||
issue: 2264
|
||||
title: "A new interceptor called the
|
||||
[Repository Validating Interceptor](/hapi-fhir/docs/validation/repository_validating_interceptor.html)
|
||||
has been added. This new interceptor allows a HAPI FHIR JPA Server to declare rules about mandatory profiles
|
||||
that all resources stored to the server must declare conformance and/or correctly validate against."
|
|
@ -15,7 +15,7 @@ page.model.profiles_and_extensions=Profiles and Extensions
|
|||
page.model.converter=Version Converters
|
||||
page.model.custom_structures=Custom Structures
|
||||
page.model.narrative_generation=Narrative Generation
|
||||
page.model.transaction_builder=Transaction Builder
|
||||
page.model.bundle_builder=Bundle Builder
|
||||
|
||||
section.client.title=Client
|
||||
page.client.introduction=Introduction
|
||||
|
@ -87,6 +87,7 @@ page.validation.parser_error_handler=Parser Error Handler
|
|||
page.validation.instance_validator=Instance Validator
|
||||
page.validation.validation_support_modules=Validation Support Modules
|
||||
page.validation.schema_validator=Schema/Schematron Validator
|
||||
page.validation.repository_validating_interceptor=Repository Validating Interceptor
|
||||
page.validation.examples=Validation Examples
|
||||
|
||||
section.android.title=Android
|
||||
|
|
|
@ -106,6 +106,8 @@ A sample response to this query is shown below:
|
|||
}
|
||||
```
|
||||
|
||||
<a name="request_and_response_validation"/>
|
||||
|
||||
# Validation: Request and Response Validation
|
||||
|
||||
HAPI FHIR provides a pair of interceptors that can be used to validate incoming requests received by the server, as well as outgoing responses generated by the server.
|
||||
|
@ -129,6 +131,9 @@ The following example shows how to register this interceptor within a HAPI FHIR
|
|||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ServletExamples.java|validatingInterceptor}}
|
||||
```
|
||||
|
||||
**See Also:** The [Repository Validating Interceptor](/docs/validation/repository_validating_interceptor.html) provides a different and potentially more powerful way of validating data when paired with a HAPI FHIR JPA Server.
|
||||
|
||||
|
||||
# Security: CORS
|
||||
|
||||
HAPI FHIR includes an interceptor which can be used to implement CORS support on your server. See [Server CORS Documentation](/docs/security/cors.html#cors_interceptor) for information on how to use this interceptor.
|
||||
|
@ -201,3 +206,7 @@ This interceptor uses FHIRPath expressions to indicate the resource paths that s
|
|||
# JPA Server: Retry on Version Conflicts
|
||||
|
||||
The UserRequestRetryVersionConflictsInterceptor allows clients to request that the server avoid version conflicts (HTTP 409) when two concurrent client requests attempt to modify the same resource. See [Version Conflicts](/docs/server_jpa/configuration.html#retry-on-version-conflict) for more information.
|
||||
|
||||
# JPA Server: Validate Data Being Stored
|
||||
|
||||
The RepositoryValidatingInterceptor can be used to enforce validation rules on data stored in a HAPI FHIR JPA Repository. See [Repository Validating Interceptor](/docs/validation/repository_validating_interceptor.html) for more information.
|
||||
|
|
|
@ -4,12 +4,12 @@ The BundleBuilder ([JavaDoc](/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/util/
|
|||
|
||||
Note that this class is a work in progress! It does not yet support all transaction features. We will add more features over time, and document them here. Pull requests are welcomed.
|
||||
|
||||
# Resource Create
|
||||
# Transaction Resource Create
|
||||
|
||||
To add an update (aka PUT) operation to a transaction bundle
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/TransactionBuilderExamples.java|create}}
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/BundleBuilderExamples.java|create}}
|
||||
```
|
||||
|
||||
## Conditional Create
|
||||
|
@ -17,15 +17,15 @@ To add an update (aka PUT) operation to a transaction bundle
|
|||
If you want to perform a conditional create:
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/TransactionBuilderExamples.java|createConditional}}
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/BundleBuilderExamples.java|createConditional}}
|
||||
```
|
||||
|
||||
# Resource Updates
|
||||
# Transaction Resource Updates
|
||||
|
||||
To add an update (aka PUT) operation to a transaction bundle:
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/TransactionBuilderExamples.java|update}}
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/BundleBuilderExamples.java|update}}
|
||||
```
|
||||
|
||||
## Conditional Update
|
||||
|
@ -33,7 +33,7 @@ To add an update (aka PUT) operation to a transaction bundle:
|
|||
If you want to perform a conditional update:
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/TransactionBuilderExamples.java|updateConditional}}
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/BundleBuilderExamples.java|updateConditional}}
|
||||
```
|
||||
|
||||
# Customizing bundle
|
||||
|
@ -41,6 +41,6 @@ If you want to perform a conditional update:
|
|||
If you want to manipulate a bundle:
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/TransactionBuilderExamples.java|customizeBundle}}
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/BundleBuilderExamples.java|customizeBundle}}
|
||||
```
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
# Repository Validating Interceptor
|
||||
|
||||
When using a HAPI FHIR JPA server as a FHIR Repository, it is often desirable to enforce specific rules about which specific FHIR profiles can or must be used.
|
||||
|
||||
For example, if an organization has created a FHIR Repository for the purposes of hosting and serving [US Core](https://www.hl7.org/fhir/us/core/) data, that organization might want to enforce rules that data being stored in the repository must actually declare conformance to US Core Profiles such as the [US Core Patient Profile](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-patient.html) and [US Core Observation Profile](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-observation.html).
|
||||
|
||||
In this situation, it would also be useful to require that any data being stored in the repository be validated, and rejected if it is not valid.
|
||||
|
||||
The **RepositoryValidatingInterceptor** interceptor can be used to easily add this kind of rule to the server.
|
||||
|
||||
# Benefits and Limitations
|
||||
|
||||
For a HAPI FHIR JPA Server, the RepositoryValidatingInterceptor is a very powerful option compared to the [Request and Response Validation](/docs/interceptors/built_in_server_interceptors.html#request_and_response_validation) that is also often used for validation.
|
||||
|
||||
## Request and Response Validation
|
||||
|
||||
The *Request and Response Validation* interceptors examine incoming HTTP payloads (e.g. FHIR creates, FHIR updates, etc.) and apply the validator to them. This approach has its limitations:
|
||||
|
||||
* It may miss validating data that is added or modified through Java API calls as opposed to through the HTTP endpoint.
|
||||
|
||||
* It may miss validating data that is added or modified through other interceptors
|
||||
|
||||
* It is not able to validate changes coming from operations such as FHIR Patch, since the patch itself may pass validation, but may ultimately result in modifying a resource so that it is no longer valid.
|
||||
|
||||
## Repository Validation
|
||||
|
||||
The *Repository Validating Interceptor* uses the direct storage pointcuts provided by the JPA Server in order to validate data exactly as it will appear in storage. In other words, no matter whether data is being written via the HTTP API or by an internal Java API call, the interceptor will catch it.
|
||||
|
||||
This means that:
|
||||
|
||||
* Repository validation applies to patch operations and validates the results of the resource after a patch is applied (but before the actual results are saved, in case the outcome of the validation operation should roll the operation back)
|
||||
|
||||
* Repository validation requires pointcuts that are thrown directly by the storage engine, meaning that it can not be used from a plain server unless the plain server code manually invokes the same pointcuts.
|
||||
|
||||
# Using the Repository Validating Interceptor
|
||||
|
||||
Using the repository validating interceptor is as simple as creating a new instance of [RepositoryValidatingInterceptor](/hapi-fhir/apidocs/hapi-fhir-jpaserver-base/ca/uhn/fhir/jpa/interceptor/validation/RepositoryValidatingInterceptor.html) and registering it with the interceptor registry. The only tricky part is initializing your rules, which must be done using a [RepositoryValidatingRuleBuilder](/hapi-fhir/apidocs/hapi-fhir-jpaserver-base/ca/uhn/fhir/jpa/interceptor/validation/RepositoryValidatingRuleBuilder.html).
|
||||
|
||||
The rule builder must be obtained from the Spring context, as shown below:
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/RepositoryValidatingInterceptorExamples.java|createSimpleRule}}
|
||||
```
|
||||
|
||||
# Rules: Require Profile Declarations
|
||||
|
||||
Use one of the following rule formats to require any resources submitted to the server to declare conformance to the given profiles.
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/RepositoryValidatingInterceptorExamples.java|requireProfileDeclarations}}
|
||||
```
|
||||
|
||||
Note that this rule alone does not actually enforce validation against the specified profiles. It simply requires that any resources submitted to the server contain a declaration that they intend to conform. See the following example:
|
||||
|
||||
```json
|
||||
{
|
||||
"resourceType": "Patient",
|
||||
"id": "123",
|
||||
"meta": {
|
||||
"profile": [
|
||||
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Rules: Require Validation to Declared Profiles
|
||||
|
||||
Use the following rule to require that resources of the given type be validated successfully before allowing them to be persisted. For every resource of the given type that is submitted for storage, the `Resource.meta.profile` field will be examined and the resource will be validated against any declarations found there.
|
||||
|
||||
This rule is generally combined with the *Require Profile Declarations* above.
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/RepositoryValidatingInterceptorExamples.java|requireValidationToDeclaredProfiles}}
|
||||
```
|
||||
|
||||
Any resource creates or updates that do not conform to the given profile will be rejected.
|
||||
|
||||
# Rules: Disallow Specific Profiles
|
||||
|
||||
Rules can declare that a specific profile is not allowed.
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/RepositoryValidatingInterceptorExamples.java|disallowProfiles}}
|
||||
```
|
|
@ -60,6 +60,7 @@ import ca.uhn.fhir.jpa.graphql.JpaStorageServices;
|
|||
import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor;
|
||||
import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices;
|
||||
import ca.uhn.fhir.jpa.interceptor.OverridePathBasedReferentialIntegrityForDeletesInterceptor;
|
||||
import ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingRuleBuilder;
|
||||
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
|
||||
import ca.uhn.fhir.jpa.packages.IHapiPackageCacheManager;
|
||||
import ca.uhn.fhir.jpa.packages.IPackageInstallerSvc;
|
||||
|
@ -190,6 +191,7 @@ public abstract class BaseConfig {
|
|||
public static final String SEARCH_BUILDER = "SearchBuilder";
|
||||
public static final String HISTORY_BUILDER = "HistoryBuilder";
|
||||
private static final String HAPI_DEFAULT_SCHEDULER_GROUP = "HAPI";
|
||||
public static final String REPOSITORY_VALIDATING_RULE_BUILDER = "repositoryValidatingRuleBuilder";
|
||||
|
||||
@Autowired
|
||||
protected Environment myEnv;
|
||||
|
@ -507,6 +509,12 @@ public abstract class BaseConfig {
|
|||
return new PersistedJpaSearchFirstPageBundleProvider(theSearch, theSearchTask, theSearchBuilder, theRequest);
|
||||
}
|
||||
|
||||
@Bean(name = REPOSITORY_VALIDATING_RULE_BUILDER)
|
||||
@Scope("prototype")
|
||||
public RepositoryValidatingRuleBuilder repositoryValidatingRuleBuilder() {
|
||||
return new RepositoryValidatingRuleBuilder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Scope("prototype")
|
||||
public CompositeUniqueSearchParameterPredicateBuilder newCompositeUniqueSearchParameterPredicateBuilder(SearchQueryBuilder theSearchSqlBuilder) {
|
||||
|
|
|
@ -32,12 +32,10 @@ import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc;
|
|||
import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
|
||||
import ca.uhn.fhir.jpa.term.api.ITermReindexingSvc;
|
||||
import ca.uhn.fhir.jpa.term.api.ITermVersionAdapterSvc;
|
||||
import ca.uhn.fhir.jpa.validation.JpaFhirInstanceValidator;
|
||||
import ca.uhn.fhir.jpa.validation.JpaValidationSupportChain;
|
||||
import ca.uhn.fhir.jpa.validation.ValidationSettings;
|
||||
import ca.uhn.fhir.jpa.validation.ValidatorResourceFetcher;
|
||||
import ca.uhn.fhir.validation.IInstanceValidatorModule;
|
||||
import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport;
|
||||
import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain;
|
||||
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
|
||||
import org.hl7.fhir.r5.utils.IResourceValidator;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
@ -82,7 +80,7 @@ public abstract class BaseConfigDstu3Plus extends BaseConfig {
|
|||
}
|
||||
|
||||
@Primary
|
||||
@Bean()
|
||||
@Bean
|
||||
public IValidationSupport validationSupportChain() {
|
||||
return new CachingValidationSupport(jpaValidationSupportChain());
|
||||
}
|
||||
|
@ -90,12 +88,19 @@ public abstract class BaseConfigDstu3Plus extends BaseConfig {
|
|||
@Bean(name = "myInstanceValidator")
|
||||
@Lazy
|
||||
public IInstanceValidatorModule instanceValidator() {
|
||||
FhirInstanceValidator val = new JpaFhirInstanceValidator(fhirContext());
|
||||
FhirInstanceValidator val = new FhirInstanceValidator(validationSupportChain());
|
||||
val.setValidatorResourceFetcher(jpaValidatorResourceFetcher());
|
||||
val.setBestPracticeWarningLevel(IResourceValidator.BestPracticeWarningLevel.Warning);
|
||||
val.setValidationSupport(validationSupportChain());
|
||||
return val;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Lazy
|
||||
public ValidatorResourceFetcher jpaValidatorResourceFetcher() {
|
||||
return new ValidatorResourceFetcher();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public abstract ITermReadSvc terminologyService();
|
||||
|
||||
|
|
|
@ -143,6 +143,8 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
protected PlatformTransactionManager myPlatformTransactionManager;
|
||||
@Autowired
|
||||
protected DaoConfig myDaoConfig;
|
||||
@Autowired(required = false)
|
||||
protected IFulltextSearchSvc mySearchDao;
|
||||
@Autowired
|
||||
private MatchResourceUrlService myMatchResourceUrlService;
|
||||
@Autowired
|
||||
|
@ -155,8 +157,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
private IRequestPartitionHelperSvc myRequestPartitionHelperService;
|
||||
@Autowired
|
||||
private HapiTransactionService myTransactionService;
|
||||
@Autowired(required = false)
|
||||
protected IFulltextSearchSvc mySearchDao;
|
||||
@Autowired
|
||||
private MatchUrlService myMatchUrlService;
|
||||
@Autowired
|
||||
|
@ -631,13 +631,15 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
}
|
||||
|
||||
|
||||
private <MT extends IBaseMetaType> void doMetaAdd(MT theMetaAdd, BaseHasResource entity) {
|
||||
private <MT extends IBaseMetaType> void doMetaAdd(MT theMetaAdd, BaseHasResource theEntity, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails) {
|
||||
IBaseResource oldVersion = toResource(theEntity, false);
|
||||
|
||||
List<TagDefinition> tags = toTagList(theMetaAdd);
|
||||
|
||||
for (TagDefinition nextDef : tags) {
|
||||
|
||||
boolean hasTag = false;
|
||||
for (BaseTag next : new ArrayList<>(entity.getTags())) {
|
||||
for (BaseTag next : new ArrayList<>(theEntity.getTags())) {
|
||||
if (ObjectUtil.equals(next.getTag().getTagType(), nextDef.getTagType()) &&
|
||||
ObjectUtil.equals(next.getTag().getSystem(), nextDef.getSystem()) &&
|
||||
ObjectUtil.equals(next.getTag().getCode(), nextDef.getCode())) {
|
||||
|
@ -647,11 +649,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
}
|
||||
|
||||
if (!hasTag) {
|
||||
entity.setHasTags(true);
|
||||
theEntity.setHasTags(true);
|
||||
|
||||
TagDefinition def = getTagOrNull(nextDef.getTagType(), nextDef.getSystem(), nextDef.getCode(), nextDef.getDisplay());
|
||||
if (def != null) {
|
||||
BaseTag newEntity = entity.addTag(def);
|
||||
BaseTag newEntity = theEntity.addTag(def);
|
||||
if (newEntity.getTagId() == null) {
|
||||
myEntityManager.persist(newEntity);
|
||||
}
|
||||
|
@ -659,30 +661,55 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
}
|
||||
}
|
||||
|
||||
validateMetaCount(entity.getTags().size());
|
||||
validateMetaCount(theEntity.getTags().size());
|
||||
|
||||
theEntity = myEntityManager.merge(theEntity);
|
||||
|
||||
// Interceptor call: STORAGE_PRECOMMIT_RESOURCE_UPDATED
|
||||
IBaseResource newVersion = toResource(theEntity, false);
|
||||
HookParams params = new HookParams()
|
||||
.add(IBaseResource.class, oldVersion)
|
||||
.add(IBaseResource.class, newVersion)
|
||||
.add(RequestDetails.class, theRequestDetails)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
|
||||
.add(TransactionDetails.class, theTransactionDetails);
|
||||
myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, params);
|
||||
|
||||
myEntityManager.merge(entity);
|
||||
}
|
||||
|
||||
private <MT extends IBaseMetaType> void doMetaDelete(MT theMetaDel, BaseHasResource entity) {
|
||||
private <MT extends IBaseMetaType> void doMetaDelete(MT theMetaDel, BaseHasResource theEntity, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails) {
|
||||
|
||||
IBaseResource oldVersion = toResource(theEntity, false);
|
||||
|
||||
List<TagDefinition> tags = toTagList(theMetaDel);
|
||||
|
||||
for (TagDefinition nextDef : tags) {
|
||||
for (BaseTag next : new ArrayList<BaseTag>(entity.getTags())) {
|
||||
for (BaseTag next : new ArrayList<BaseTag>(theEntity.getTags())) {
|
||||
if (ObjectUtil.equals(next.getTag().getTagType(), nextDef.getTagType()) &&
|
||||
ObjectUtil.equals(next.getTag().getSystem(), nextDef.getSystem()) &&
|
||||
ObjectUtil.equals(next.getTag().getCode(), nextDef.getCode())) {
|
||||
myEntityManager.remove(next);
|
||||
entity.getTags().remove(next);
|
||||
theEntity.getTags().remove(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (entity.getTags().isEmpty()) {
|
||||
entity.setHasTags(false);
|
||||
if (theEntity.getTags().isEmpty()) {
|
||||
theEntity.setHasTags(false);
|
||||
}
|
||||
|
||||
myEntityManager.merge(entity);
|
||||
theEntity = myEntityManager.merge(theEntity);
|
||||
|
||||
// Interceptor call: STORAGE_PRECOMMIT_RESOURCE_UPDATED
|
||||
IBaseResource newVersion = toResource(theEntity, false);
|
||||
HookParams params = new HookParams()
|
||||
.add(IBaseResource.class, oldVersion)
|
||||
.add(IBaseResource.class, newVersion)
|
||||
.add(RequestDetails.class, theRequestDetails)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
|
||||
.add(TransactionDetails.class, theTransactionDetails);
|
||||
myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, params);
|
||||
|
||||
}
|
||||
|
||||
private void validateExpungeEnabled() {
|
||||
|
@ -824,6 +851,8 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
@Override
|
||||
@Transactional
|
||||
public <MT extends IBaseMetaType> MT metaAddOperation(IIdType theResourceId, MT theMetaAdd, RequestDetails theRequest) {
|
||||
TransactionDetails transactionDetails = new TransactionDetails();
|
||||
|
||||
// Notify interceptors
|
||||
if (theRequest != null) {
|
||||
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, getResourceName(), theResourceId);
|
||||
|
@ -838,16 +867,16 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
|
||||
ResourceTable latestVersion = readEntityLatestVersion(theResourceId, theRequest);
|
||||
if (latestVersion.getVersion() != entity.getVersion()) {
|
||||
doMetaAdd(theMetaAdd, entity);
|
||||
doMetaAdd(theMetaAdd, entity, theRequest, transactionDetails);
|
||||
} else {
|
||||
doMetaAdd(theMetaAdd, latestVersion);
|
||||
doMetaAdd(theMetaAdd, latestVersion, theRequest, transactionDetails);
|
||||
|
||||
// Also update history entry
|
||||
ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(entity.getId(), entity.getVersion());
|
||||
doMetaAdd(theMetaAdd, history);
|
||||
doMetaAdd(theMetaAdd, history, theRequest, transactionDetails);
|
||||
}
|
||||
|
||||
ourLog.debug("Processed metaAddOperation on {} in {}ms", new Object[]{theResourceId, w.getMillisAndRestart()});
|
||||
ourLog.debug("Processed metaAddOperation on {} in {}ms", theResourceId, w.getMillisAndRestart());
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
MT retVal = (MT) metaGetOperation(theMetaAdd.getClass(), theResourceId, theRequest);
|
||||
|
@ -857,6 +886,8 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
@Override
|
||||
@Transactional
|
||||
public <MT extends IBaseMetaType> MT metaDeleteOperation(IIdType theResourceId, MT theMetaDel, RequestDetails theRequest) {
|
||||
TransactionDetails transactionDetails = new TransactionDetails();
|
||||
|
||||
// Notify interceptors
|
||||
if (theRequest != null) {
|
||||
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, getResourceName(), theResourceId);
|
||||
|
@ -871,18 +902,16 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
|
||||
ResourceTable latestVersion = readEntityLatestVersion(theResourceId, theRequest);
|
||||
if (latestVersion.getVersion() != entity.getVersion()) {
|
||||
doMetaDelete(theMetaDel, entity);
|
||||
doMetaDelete(theMetaDel, entity, theRequest, transactionDetails);
|
||||
} else {
|
||||
doMetaDelete(theMetaDel, latestVersion);
|
||||
doMetaDelete(theMetaDel, latestVersion, theRequest, transactionDetails);
|
||||
|
||||
// Also update history entry
|
||||
ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(entity.getId(), entity.getVersion());
|
||||
doMetaDelete(theMetaDel, history);
|
||||
doMetaDelete(theMetaDel, history, theRequest, transactionDetails);
|
||||
}
|
||||
|
||||
myEntityManager.flush();
|
||||
|
||||
ourLog.debug("Processed metaDeleteOperation on {} in {}ms", new Object[]{theResourceId.getValue(), w.getMillisAndRestart()});
|
||||
ourLog.debug("Processed metaDeleteOperation on {} in {}ms", theResourceId.getValue(), w.getMillisAndRestart());
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
MT retVal = (MT) metaGetOperation(theMetaDel.getClass(), theResourceId, theRequest);
|
||||
|
@ -1038,7 +1067,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
public T read(IIdType theId, RequestDetails theRequest, boolean theDeletedOk) {
|
||||
validateResourceTypeAndThrowInvalidRequestException(theId);
|
||||
|
||||
return myTransactionService.execute(theRequest, tx-> doRead(theId, theRequest, theDeletedOk));
|
||||
return myTransactionService.execute(theRequest, tx -> doRead(theId, theRequest, theDeletedOk));
|
||||
}
|
||||
|
||||
public T doRead(IIdType theId, RequestDetails theRequest, boolean theDeletedOk) {
|
||||
|
@ -1630,7 +1659,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
|
||||
private static class IdChecker implements IValidatorModule {
|
||||
|
||||
private ValidationModeEnum myMode;
|
||||
private final ValidationModeEnum myMode;
|
||||
|
||||
IdChecker(ValidationModeEnum theMode) {
|
||||
myMode = theMode;
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
package ca.uhn.fhir.jpa.interceptor.validation;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2020 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.context.FhirContext;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
abstract class BaseTypedRule implements IRepositoryValidatingRule {
|
||||
|
||||
private final String myResourceType;
|
||||
private final FhirContext myFhirContext;
|
||||
|
||||
protected BaseTypedRule(FhirContext theFhirContext, String theResourceType) {
|
||||
Validate.notNull(theFhirContext);
|
||||
Validate.notBlank(theResourceType);
|
||||
myFhirContext = theFhirContext;
|
||||
myResourceType = theResourceType;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getResourceType() {
|
||||
return myResourceType;
|
||||
}
|
||||
|
||||
protected FhirContext getFhirContext() {
|
||||
return myFhirContext;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
package ca.uhn.fhir.jpa.interceptor.validation;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2020 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 org.apache.commons.lang3.Validate;
|
||||
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* This is an internal API for HAPI FHIR. It is subject to change without warning.
|
||||
*/
|
||||
public interface IRepositoryValidatingRule {
|
||||
|
||||
@Nonnull
|
||||
String getResourceType();
|
||||
|
||||
@Nonnull
|
||||
RuleEvaluation evaluate(@Nonnull IBaseResource theResource);
|
||||
|
||||
class RuleEvaluation {
|
||||
|
||||
private final IBaseOperationOutcome myOperationOutcome;
|
||||
private IRepositoryValidatingRule myRule;
|
||||
private boolean myPasses;
|
||||
private String myFailureDescription;
|
||||
|
||||
private RuleEvaluation(IRepositoryValidatingRule theRule, boolean thePasses, String theFailureDescription, IBaseOperationOutcome theOperationOutcome) {
|
||||
myRule = theRule;
|
||||
myPasses = thePasses;
|
||||
myFailureDescription = theFailureDescription;
|
||||
myOperationOutcome = theOperationOutcome;
|
||||
}
|
||||
|
||||
public IBaseOperationOutcome getOperationOutcome() {
|
||||
return myOperationOutcome;
|
||||
}
|
||||
|
||||
public IRepositoryValidatingRule getRule() {
|
||||
return myRule;
|
||||
}
|
||||
|
||||
public boolean isPasses() {
|
||||
return myPasses;
|
||||
}
|
||||
|
||||
public String getFailureDescription() {
|
||||
return myFailureDescription;
|
||||
}
|
||||
|
||||
static RuleEvaluation forSuccess(IRepositoryValidatingRule theRule) {
|
||||
Validate.notNull(theRule);
|
||||
return new RuleEvaluation(theRule, true, null, null);
|
||||
}
|
||||
|
||||
static RuleEvaluation forFailure(IRepositoryValidatingRule theRule, String theFailureDescription) {
|
||||
Validate.notNull(theRule);
|
||||
Validate.notNull(theFailureDescription);
|
||||
return new RuleEvaluation(theRule, false, theFailureDescription, null);
|
||||
}
|
||||
|
||||
static RuleEvaluation forFailure(IRepositoryValidatingRule theRule, IBaseOperationOutcome theOperationOutcome) {
|
||||
Validate.notNull(theRule);
|
||||
Validate.notNull(theOperationOutcome);
|
||||
return new RuleEvaluation(theRule, false, null, theOperationOutcome);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package ca.uhn.fhir.jpa.interceptor.validation;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2020 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 java.util.List;
|
||||
|
||||
interface IRuleRoot {
|
||||
|
||||
/**
|
||||
* Starts a new rule that applies to any resources of type {@literal theType}
|
||||
*
|
||||
* @param theType The resource type e.g. "Patient" (must not be null)
|
||||
*/
|
||||
RepositoryValidatingRuleBuilder.RepositoryValidatingRuleBuilderTyped forResourcesOfType(String theType);
|
||||
|
||||
/**
|
||||
* Create the repository validation rules
|
||||
*/
|
||||
List<IRepositoryValidatingRule> build();
|
||||
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
package ca.uhn.fhir.jpa.interceptor.validation;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2020 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.context.FhirContext;
|
||||
import ca.uhn.fhir.interceptor.api.Hook;
|
||||
import ca.uhn.fhir.interceptor.api.Interceptor;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
|
||||
import ca.uhn.fhir.util.OperationOutcomeUtil;
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.collect.Multimaps;
|
||||
import org.apache.commons.collections4.MultiMap;
|
||||
import org.apache.commons.collections4.MultiValuedMap;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This interceptor enforces validation rules on any data saved in a HAPI FHIR JPA repository.
|
||||
* See <a href="https://hapifhir.io/hapi-fhir/docs/validation/repository_validating_interceptor.html">Repository Validating Interceptor</a>
|
||||
* in the HAPI FHIR documentation for more information on how to use this.
|
||||
*/
|
||||
@Interceptor
|
||||
public class RepositoryValidatingInterceptor {
|
||||
|
||||
private Multimap<String, IRepositoryValidatingRule> myRules = ArrayListMultimap.create();
|
||||
private FhirContext myFhirContext;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* If this constructor is used, {@link #setFhirContext(FhirContext)} and {@link #setRules(List)} must be called
|
||||
* manually before the interceptor is used.
|
||||
*/
|
||||
public RepositoryValidatingInterceptor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param theFhirContext The FHIR Context (must not be <code>null</code>)
|
||||
* @param theRules The rule list (must not be <code>null</code>)
|
||||
*/
|
||||
public RepositoryValidatingInterceptor(FhirContext theFhirContext, List<IRepositoryValidatingRule> theRules) {
|
||||
setFhirContext(theFhirContext);
|
||||
setRules(theRules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide the FHIR Context (mandatory)
|
||||
*/
|
||||
public void setFhirContext(FhirContext theFhirContext) {
|
||||
myFhirContext = theFhirContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide the rules to use for validation (mandatory)
|
||||
*/
|
||||
public void setRules(List<IRepositoryValidatingRule> theRules) {
|
||||
Validate.notNull(theRules, "theRules must not be null");
|
||||
myRules.clear();
|
||||
for (IRepositoryValidatingRule next : theRules) {
|
||||
myRules.put(next.getResourceType(), next);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interceptor hook method. This method should not be called directly.
|
||||
*/
|
||||
@Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)
|
||||
void create(IBaseResource theResource) {
|
||||
handle(theResource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interceptor hook method. This method should not be called directly.
|
||||
*/
|
||||
@Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED)
|
||||
void update(IBaseResource theOldResource, IBaseResource theNewResource) {
|
||||
handle(theNewResource);
|
||||
}
|
||||
|
||||
private void handle(IBaseResource theNewResource) {
|
||||
Validate.notNull(myFhirContext, "No FhirContext has been set for this interceptor of type: %s", getClass());
|
||||
|
||||
String resourceType = myFhirContext.getResourceType(theNewResource);
|
||||
Collection<IRepositoryValidatingRule> rules = myRules.get(resourceType);
|
||||
for (IRepositoryValidatingRule nextRule : rules) {
|
||||
IRepositoryValidatingRule.RuleEvaluation outcome = nextRule.evaluate(theNewResource);
|
||||
if (!outcome.isPasses()) {
|
||||
handleFailure(outcome);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleFailure(IRepositoryValidatingRule.RuleEvaluation theOutcome) {
|
||||
if (theOutcome.getOperationOutcome() != null) {
|
||||
String firstIssue = OperationOutcomeUtil.getFirstIssueDetails(myFhirContext, theOutcome.getOperationOutcome());
|
||||
throw new PreconditionFailedException(firstIssue, theOutcome.getOperationOutcome());
|
||||
}
|
||||
throw new PreconditionFailedException(theOutcome.getFailureDescription());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,211 @@
|
|||
package ca.uhn.fhir.jpa.interceptor.validation;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2020 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.context.FhirContext;
|
||||
import ca.uhn.fhir.context.support.IValidationSupport;
|
||||
import ca.uhn.fhir.jpa.validation.ValidatorResourceFetcher;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.apache.commons.text.WordUtils;
|
||||
import org.hl7.fhir.r5.utils.IResourceValidator;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
/**
|
||||
* This class is used to construct rules to populate the {@link RepositoryValidatingInterceptor}.
|
||||
* See <a href="https://hapifhir.io/hapi-fhir/docs/validation/repository_validating_interceptor.html">Repository Validating Interceptor</a>
|
||||
* in the HAPI FHIR documentation for more information on how to use this.
|
||||
*/
|
||||
public final class RepositoryValidatingRuleBuilder implements IRuleRoot {
|
||||
|
||||
private final List<IRepositoryValidatingRule> myRules = new ArrayList<>();
|
||||
|
||||
@Autowired
|
||||
private FhirContext myFhirContext;
|
||||
@Autowired
|
||||
private IValidationSupport myValidationSupport;
|
||||
@Autowired
|
||||
private ValidatorResourceFetcher myValidatorResourceFetcher;
|
||||
|
||||
/**
|
||||
* Begin a new rule for a specific resource type.
|
||||
*
|
||||
* @param theType The resource type e.g. "Patient" (must not be null)
|
||||
*/
|
||||
@Override
|
||||
public RepositoryValidatingRuleBuilderTyped forResourcesOfType(String theType) {
|
||||
return new RepositoryValidatingRuleBuilderTyped(theType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the repository validation rules
|
||||
*/
|
||||
@Override
|
||||
public List<IRepositoryValidatingRule> build() {
|
||||
return myRules;
|
||||
}
|
||||
|
||||
public class FinalizedTypedRule implements IRuleRoot {
|
||||
|
||||
private final String myType;
|
||||
|
||||
FinalizedTypedRule(String theType) {
|
||||
myType = theType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RepositoryValidatingRuleBuilderTyped forResourcesOfType(String theType) {
|
||||
return RepositoryValidatingRuleBuilder.this.forResourcesOfType(theType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<IRepositoryValidatingRule> build() {
|
||||
return RepositoryValidatingRuleBuilder.this.build();
|
||||
}
|
||||
|
||||
public RepositoryValidatingRuleBuilderTyped and() {
|
||||
return new RepositoryValidatingRuleBuilderTyped(myType);
|
||||
}
|
||||
}
|
||||
|
||||
public final class RepositoryValidatingRuleBuilderTyped {
|
||||
|
||||
private final String myType;
|
||||
|
||||
RepositoryValidatingRuleBuilderTyped(String theType) {
|
||||
myType = myFhirContext.getResourceType(theType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Require any resource being persisted to declare conformance to the given profile, meaning that the specified
|
||||
* profile URL must be found within the resource in <code>Resource.meta.profile</code>.
|
||||
* <p>
|
||||
* This rule is non-exclusive, meaning that a resource will pass as long as one of its profile declarations
|
||||
* in <code>Resource.meta.profile</code> matches. If the resource declares conformance to multiple profiles, any
|
||||
* other profile declarations found in that field will be ignored.
|
||||
* </p>
|
||||
*/
|
||||
public FinalizedTypedRule requireAtLeastProfile(String theProfileUrl) {
|
||||
return requireAtLeastOneProfileOf(theProfileUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Require any resource being persisted to declare conformance to at least one of the given profiles, meaning that the specified
|
||||
* profile URL must be found within the resource in <code>Resource.meta.profile</code>.
|
||||
* <p>
|
||||
* This rule is non-exclusive, meaning that a resource will pass as long as one of its profile declarations
|
||||
* in <code>Resource.meta.profile</code> matches. If the resource declares conformance to multiple profiles, any
|
||||
* other profile declarations found in that field will be ignored.
|
||||
* </p>
|
||||
*/
|
||||
public FinalizedTypedRule requireAtLeastOneProfileOf(String... theProfileUrls) {
|
||||
Validate.notNull(theProfileUrls, "theProfileUrls must not be null");
|
||||
requireAtLeastOneProfileOf(Arrays.asList(theProfileUrls));
|
||||
return new FinalizedTypedRule(myType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Require any resource being persisted to declare conformance to at least one of the given profiles, meaning that the specified
|
||||
* profile URL must be found within the resource in <code>Resource.meta.profile</code>.
|
||||
* <p>
|
||||
* This rule is non-exclusive, meaning that a resource will pass as long as one of its profile declarations
|
||||
* in <code>Resource.meta.profile</code> matches. If the resource declares conformance to multiple profiles, any
|
||||
* other profile declarations found in that field will be ignored.
|
||||
* </p>
|
||||
*/
|
||||
private FinalizedTypedRule requireAtLeastOneProfileOf(Collection<String> theProfileUrls) {
|
||||
Validate.notNull(theProfileUrls, "theProfileUrls must not be null");
|
||||
Validate.notEmpty(theProfileUrls, "theProfileUrls must not be null or empty");
|
||||
myRules.add(new RuleRequireProfileDeclaration(myFhirContext, myType, theProfileUrls));
|
||||
return new FinalizedTypedRule(myType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param theProfileUrl
|
||||
* @return
|
||||
*/
|
||||
public FinalizedTypedRule disallowProfile(String theProfileUrl) {
|
||||
return disallowProfiles(theProfileUrl);
|
||||
}
|
||||
|
||||
public FinalizedRequireValidationRule requireValidationToDeclaredProfiles() {
|
||||
RequireValidationRule rule = new RequireValidationRule(myFhirContext, myType, myValidationSupport, myValidatorResourceFetcher);
|
||||
myRules.add(rule);
|
||||
return new FinalizedRequireValidationRule(rule);
|
||||
}
|
||||
|
||||
public FinalizedTypedRule disallowProfiles(String... theProfileUrls) {
|
||||
Validate.notNull(theProfileUrls, "theProfileUrl must not be null or empty");
|
||||
Validate.notEmpty(theProfileUrls, "theProfileUrl must not be null or empty");
|
||||
myRules.add(new RuleDisallowProfile(myFhirContext, myType, theProfileUrls));
|
||||
return new FinalizedTypedRule(myType);
|
||||
}
|
||||
|
||||
|
||||
public class FinalizedRequireValidationRule extends FinalizedTypedRule {
|
||||
|
||||
private final RequireValidationRule myRule;
|
||||
|
||||
public FinalizedRequireValidationRule(RequireValidationRule theRule) {
|
||||
super(myType);
|
||||
myRule = theRule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the "Best Practice Warning Level", which is the severity at which any "best practices" that
|
||||
* are specified in the FHIR specification will be added to the validation outcome. Set to
|
||||
* <code>ERROR</code> to cause any best practice notices to result in a validation failure.
|
||||
* Set to <code>IGNORE</code> to not include any best practice notifications.
|
||||
*/
|
||||
@Nonnull
|
||||
public FinalizedRequireValidationRule withBestPracticeWarningLevel(String theBestPracticeWarningLevel) {
|
||||
IResourceValidator.BestPracticeWarningLevel level = null;
|
||||
if (isNotBlank(theBestPracticeWarningLevel)) {
|
||||
level = IResourceValidator.BestPracticeWarningLevel.valueOf(WordUtils.capitalize(theBestPracticeWarningLevel.toLowerCase()));
|
||||
}
|
||||
return withBestPracticeWarningLevel(level);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the "Best Practice Warning Level", which is the severity at which any "best practices" that
|
||||
* are specified in the FHIR specification will be added to the validation outcome. Set to
|
||||
* {@link org.hl7.fhir.r5.utils.IResourceValidator.BestPracticeWarningLevel#Error} to
|
||||
* cause any best practice notices to result in a validation failure.
|
||||
* Set to {@link org.hl7.fhir.r5.utils.IResourceValidator.BestPracticeWarningLevel#Ignore}
|
||||
* to not include any best practice notifications.
|
||||
*/
|
||||
@Nonnull
|
||||
public FinalizedRequireValidationRule withBestPracticeWarningLevel(IResourceValidator.BestPracticeWarningLevel bestPracticeWarningLevel) {
|
||||
myRule.setBestPracticeWarningLevel(bestPracticeWarningLevel);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package ca.uhn.fhir.jpa.interceptor.validation;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2020 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.context.FhirContext;
|
||||
import ca.uhn.fhir.context.support.IValidationSupport;
|
||||
import ca.uhn.fhir.jpa.validation.ValidatorResourceFetcher;
|
||||
import ca.uhn.fhir.validation.FhirValidator;
|
||||
import ca.uhn.fhir.validation.ResultSeverityEnum;
|
||||
import ca.uhn.fhir.validation.SingleValidationMessage;
|
||||
import ca.uhn.fhir.validation.ValidationResult;
|
||||
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.r5.utils.IResourceValidator;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
class RequireValidationRule extends BaseTypedRule {
|
||||
private final IValidationSupport myValidationSupport;
|
||||
private final ValidatorResourceFetcher myValidatorResourceFetcher;
|
||||
private final FhirInstanceValidator myValidator;
|
||||
|
||||
RequireValidationRule(FhirContext theFhirContext, String theType, IValidationSupport theValidationSupport, ValidatorResourceFetcher theValidatorResourceFetcher) {
|
||||
super(theFhirContext, theType);
|
||||
myValidationSupport = theValidationSupport;
|
||||
myValidatorResourceFetcher = theValidatorResourceFetcher;
|
||||
|
||||
myValidator = new FhirInstanceValidator(theValidationSupport);
|
||||
myValidator.setValidatorResourceFetcher(theValidatorResourceFetcher);
|
||||
myValidator.setBestPracticeWarningLevel(IResourceValidator.BestPracticeWarningLevel.Warning);
|
||||
}
|
||||
|
||||
void setBestPracticeWarningLevel(IResourceValidator.BestPracticeWarningLevel theBestPracticeWarningLevel) {
|
||||
myValidator.setBestPracticeWarningLevel(theBestPracticeWarningLevel);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public RuleEvaluation evaluate(@Nonnull IBaseResource theResource) {
|
||||
|
||||
FhirValidator validator = getFhirContext().newValidator();
|
||||
validator.registerValidatorModule(myValidator);
|
||||
ValidationResult outcome = validator.validateWithResult(theResource);
|
||||
|
||||
for (SingleValidationMessage next : outcome.getMessages()) {
|
||||
if (next.getSeverity().ordinal() >= ResultSeverityEnum.ERROR.ordinal()) {
|
||||
return RuleEvaluation.forFailure(this, outcome.toOperationOutcome());
|
||||
}
|
||||
}
|
||||
|
||||
return RuleEvaluation.forSuccess(this);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package ca.uhn.fhir.jpa.interceptor.validation;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2020 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.context.FhirContext;
|
||||
import ca.uhn.fhir.util.UrlUtil;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
class RuleDisallowProfile extends BaseTypedRule {
|
||||
private final Set<String> myProfileUrls;
|
||||
|
||||
RuleDisallowProfile(FhirContext theFhirContext, String theResourceType, String[] theProfileUrls) {
|
||||
super(theFhirContext, theResourceType);
|
||||
Validate.notNull(theProfileUrls);
|
||||
Validate.notEmpty(theProfileUrls);
|
||||
myProfileUrls = new HashSet<>();
|
||||
for (String theProfileUrl : theProfileUrls) {
|
||||
myProfileUrls.add(UrlUtil.normalizeCanonicalUrlForComparison(theProfileUrl));
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public RuleEvaluation evaluate(@Nonnull IBaseResource theResource) {
|
||||
for (IPrimitiveType<String> next : theResource.getMeta().getProfile()) {
|
||||
String nextUrl = next.getValueAsString();
|
||||
String nextUrlNormalized = UrlUtil.normalizeCanonicalUrlForComparison(nextUrl);
|
||||
if (myProfileUrls.contains(nextUrlNormalized)) {
|
||||
String msg = getFhirContext().getLocalizer().getMessage(RuleRequireProfileDeclaration.class, "illegalProfile", getResourceType(), nextUrl);
|
||||
return RuleEvaluation.forFailure(this, msg);
|
||||
}
|
||||
}
|
||||
|
||||
return RuleEvaluation.forSuccess(this);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package ca.uhn.fhir.jpa.interceptor.validation;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2020 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.context.FhirContext;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
|
||||
class RuleRequireProfileDeclaration extends BaseTypedRule {
|
||||
private final Collection<String> myProfileOptions;
|
||||
|
||||
RuleRequireProfileDeclaration(FhirContext theFhirContext, String theType, Collection<String> theProfileOptions) {
|
||||
super(theFhirContext, theType);
|
||||
myProfileOptions = theProfileOptions;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public RuleEvaluation evaluate(@Nonnull IBaseResource theResource) {
|
||||
Optional<String> matchingProfile = theResource
|
||||
.getMeta()
|
||||
.getProfile()
|
||||
.stream()
|
||||
.map(t -> t.getValueAsString())
|
||||
.filter(t -> myProfileOptions.contains(t))
|
||||
.findFirst();
|
||||
if (matchingProfile.isPresent()) {
|
||||
return RuleEvaluation.forSuccess(this);
|
||||
}
|
||||
String msg = getFhirContext().getLocalizer().getMessage(RuleRequireProfileDeclaration.class, "noMatchingProfile", getResourceType(), myProfileOptions);
|
||||
return RuleEvaluation.forFailure(this, msg);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,114 +0,0 @@
|
|||
package ca.uhn.fhir.jpa.validation;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2020 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.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
|
||||
import org.hl7.fhir.exceptions.DefinitionException;
|
||||
import org.hl7.fhir.exceptions.FHIRException;
|
||||
import org.hl7.fhir.exceptions.FHIRFormatError;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.r4.model.IdType;
|
||||
import org.hl7.fhir.r5.elementmodel.Element;
|
||||
import org.hl7.fhir.r5.elementmodel.JsonParser;
|
||||
import org.hl7.fhir.r5.utils.IResourceValidator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.util.Locale;
|
||||
|
||||
public class JpaFhirInstanceValidator extends FhirInstanceValidator {
|
||||
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(JpaFhirInstanceValidator.class);
|
||||
private final FhirContext myFhirContext;
|
||||
@Autowired
|
||||
private ValidationSettings myValidationSettings;
|
||||
@Autowired
|
||||
private DaoRegistry myDaoRegistry;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public JpaFhirInstanceValidator(FhirContext theFhirContext) {
|
||||
super(theFhirContext);
|
||||
myFhirContext = theFhirContext;
|
||||
setValidatorResourceFetcher(new MyValidatorResourceFetcher());
|
||||
}
|
||||
|
||||
private class MyValidatorResourceFetcher implements IResourceValidator.IValidatorResourceFetcher {
|
||||
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
public Element fetch(Object appContext, String theUrl) throws FHIRException {
|
||||
|
||||
IdType id = new IdType(theUrl);
|
||||
String resourceType = id.getResourceType();
|
||||
IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(resourceType);
|
||||
IBaseResource target;
|
||||
try {
|
||||
target = dao.read(id, (RequestDetails) appContext);
|
||||
} catch (ResourceNotFoundException e) {
|
||||
ourLog.info("Failed to resolve local reference: {}", theUrl);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new JsonParser(provideWorkerContext()).parse(myFhirContext.newJsonParser().encodeResourceToString(target), resourceType);
|
||||
} catch (Exception e) {
|
||||
throw new FHIRException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IResourceValidator.ReferenceValidationPolicy validationPolicy(Object appContext, String path, String url) {
|
||||
int slashIdx = url.indexOf("/");
|
||||
if (slashIdx > 0 && myFhirContext.getResourceTypes().contains(url.substring(0, slashIdx))) {
|
||||
return myValidationSettings.getLocalReferenceValidationDefaultPolicy();
|
||||
}
|
||||
|
||||
return IResourceValidator.ReferenceValidationPolicy.IGNORE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean resolveURL(Object appContext, String path, String url) throws IOException, FHIRException {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] fetchRaw(String url) throws IOException {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLocale(Locale locale) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
package ca.uhn.fhir.jpa.validation;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2020 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.context.FhirContext;
|
||||
import ca.uhn.fhir.context.support.IValidationSupport;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||
import org.hl7.fhir.common.hapi.validation.validator.VersionSpecificWorkerContextWrapper;
|
||||
import org.hl7.fhir.exceptions.FHIRException;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.r4.model.IdType;
|
||||
import org.hl7.fhir.r5.elementmodel.Element;
|
||||
import org.hl7.fhir.r5.elementmodel.JsonParser;
|
||||
import org.hl7.fhir.r5.utils.IResourceValidator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
|
||||
public class ValidatorResourceFetcher implements IResourceValidator.IValidatorResourceFetcher {
|
||||
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(ValidatorResourceFetcher.class);
|
||||
|
||||
@Autowired
|
||||
private DaoRegistry myDaoRegistry;
|
||||
@Autowired
|
||||
private ValidationSettings myValidationSettings;
|
||||
@Autowired
|
||||
private FhirContext myFhirContext;
|
||||
@Autowired
|
||||
private IValidationSupport myValidationSupport;
|
||||
private VersionSpecificWorkerContextWrapper myVersionSpecificCOntextWrapper;
|
||||
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
myVersionSpecificCOntextWrapper = VersionSpecificWorkerContextWrapper.newVersionSpecificWorkerContextWrapper(myValidationSupport);
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
public Element fetch(Object appContext, String theUrl) throws FHIRException {
|
||||
|
||||
IdType id = new IdType(theUrl);
|
||||
String resourceType = id.getResourceType();
|
||||
IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(resourceType);
|
||||
IBaseResource target;
|
||||
try {
|
||||
target = dao.read(id, (RequestDetails) appContext);
|
||||
} catch (ResourceNotFoundException e) {
|
||||
ourLog.info("Failed to resolve local reference: {}", theUrl);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new JsonParser(myVersionSpecificCOntextWrapper).parse(myFhirContext.newJsonParser().encodeResourceToString(target), resourceType);
|
||||
} catch (Exception e) {
|
||||
throw new FHIRException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IResourceValidator.ReferenceValidationPolicy validationPolicy(Object appContext, String path, String url) {
|
||||
int slashIdx = url.indexOf("/");
|
||||
if (slashIdx > 0 && myFhirContext.getResourceTypes().contains(url.substring(0, slashIdx))) {
|
||||
return myValidationSettings.getLocalReferenceValidationDefaultPolicy();
|
||||
}
|
||||
|
||||
return IResourceValidator.ReferenceValidationPolicy.IGNORE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean resolveURL(Object appContext, String path, String url) throws IOException, FHIRException {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] fetchRaw(String url) throws IOException {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLocale(Locale locale) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,316 @@
|
|||
package ca.uhn.fhir.jpa.interceptor.validation;
|
||||
|
||||
import ca.uhn.fhir.jpa.config.BaseConfig;
|
||||
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
|
||||
import ca.uhn.fhir.rest.api.PatchTypeEnum;
|
||||
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.CanonicalType;
|
||||
import org.hl7.fhir.r4.model.CodeType;
|
||||
import org.hl7.fhir.r4.model.IntegerType;
|
||||
import org.hl7.fhir.r4.model.Meta;
|
||||
import org.hl7.fhir.r4.model.Observation;
|
||||
import org.hl7.fhir.r4.model.OperationOutcome;
|
||||
import org.hl7.fhir.r4.model.Parameters;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.hl7.fhir.r4.model.StringType;
|
||||
import org.hl7.fhir.r4.model.UrlType;
|
||||
import org.hl7.fhir.r5.utils.IResourceValidator;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
public class RepositoryValidatingInterceptorR4Test extends BaseJpaR4Test {
|
||||
|
||||
private RepositoryValidatingInterceptor myValInterceptor;
|
||||
@Autowired
|
||||
private ApplicationContext myApplicationContext;
|
||||
|
||||
@BeforeEach
|
||||
public void before() {
|
||||
myValInterceptor = new RepositoryValidatingInterceptor();
|
||||
myValInterceptor.setFhirContext(myFhirCtx);
|
||||
myInterceptorRegistry.registerInterceptor(myValInterceptor);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void after() {
|
||||
myInterceptorRegistry.unregisterInterceptorsIf(t -> t instanceof RepositoryValidatingInterceptor);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequireAtLeastProfile_Allowed() {
|
||||
|
||||
List<IRepositoryValidatingRule> rules = newRuleBuilder()
|
||||
.forResourcesOfType("Patient")
|
||||
.requireAtLeastProfile("http://foo/Profile1")
|
||||
.build();
|
||||
myValInterceptor.setRules(rules);
|
||||
|
||||
// Create with correct profile allowed
|
||||
Patient patient = new Patient();
|
||||
patient.getMeta().addProfile("http://foo/Profile1");
|
||||
IIdType id = myPatientDao.create(patient).getId();
|
||||
assertEquals("1", id.getVersionIdPart());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequireAtLeastOneProfileOf_Allowed() {
|
||||
|
||||
List<IRepositoryValidatingRule> rules = newRuleBuilder()
|
||||
.forResourcesOfType("Patient")
|
||||
.requireAtLeastOneProfileOf("http://foo/Profile1", "http://foo/Profile2")
|
||||
.build();
|
||||
myValInterceptor.setRules(rules);
|
||||
|
||||
// Create with correct profile allowed
|
||||
Patient patient = new Patient();
|
||||
patient.getMeta().addProfile("http://foo/Profile1");
|
||||
IIdType id = myPatientDao.create(patient).getId();
|
||||
|
||||
// Update with correct profile allowed
|
||||
patient = new Patient();
|
||||
patient.setId(id);
|
||||
patient.getMeta().addProfile("http://foo/Profile2");
|
||||
patient.setActive(true);
|
||||
id = myPatientDao.update(patient).getId();
|
||||
assertEquals("2", id.getVersionIdPart());
|
||||
|
||||
// Create with other resource type allowed
|
||||
Observation obs = new Observation();
|
||||
obs.setStatus(Observation.ObservationStatus.AMENDED);
|
||||
myObservationDao.create(obs);
|
||||
|
||||
// Create with correct profile and other profile allowed
|
||||
patient = new Patient();
|
||||
patient.getMeta().addProfile("http://foo/Profile1");
|
||||
patient.getMeta().addProfile("http://foo/Profile9999");
|
||||
myPatientDao.create(patient);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequireAtLeastOneProfileOf_CreateBlocked() {
|
||||
|
||||
List<IRepositoryValidatingRule> rules = newRuleBuilder()
|
||||
.forResourcesOfType("Patient")
|
||||
.requireAtLeastOneProfileOf("http://foo/Profile1", "http://foo/Profile2")
|
||||
.build();
|
||||
myValInterceptor.setRules(rules);
|
||||
|
||||
// Disallowed profile blocked
|
||||
try {
|
||||
Patient patient = new Patient();
|
||||
patient.getMeta().addProfile("http://foo/Profile3");
|
||||
myPatientDao.create(patient);
|
||||
fail();
|
||||
} catch (PreconditionFailedException e) {
|
||||
assertEquals("Resource of type \"Patient\" does not declare conformance to profile from: [http://foo/Profile1, http://foo/Profile2]", e.getMessage());
|
||||
}
|
||||
|
||||
// No profile blocked
|
||||
try {
|
||||
Patient patient = new Patient();
|
||||
patient.setActive(true);
|
||||
myPatientDao.create(patient);
|
||||
fail();
|
||||
} catch (PreconditionFailedException e) {
|
||||
assertEquals("Resource of type \"Patient\" does not declare conformance to profile from: [http://foo/Profile1, http://foo/Profile2]", e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequireAtLeastOneProfileOf_UpdateBlocked() {
|
||||
|
||||
List<IRepositoryValidatingRule> rules = newRuleBuilder()
|
||||
.forResourcesOfType("Patient")
|
||||
.requireAtLeastOneProfileOf("http://foo/Profile1", "http://foo/Profile2")
|
||||
.build();
|
||||
myValInterceptor.setRules(rules);
|
||||
|
||||
// Create a resource with an allowable profile
|
||||
Patient patient = new Patient();
|
||||
patient.getMeta().addProfile("http://foo/Profile1");
|
||||
IIdType id = myPatientDao.create(patient).getId();
|
||||
|
||||
// Explicitly dropping the profile is blocked
|
||||
try {
|
||||
Meta metaDel = new Meta();
|
||||
metaDel.addProfile("http://foo/Profile1");
|
||||
myPatientDao.metaDeleteOperation(id, metaDel, mySrd);
|
||||
fail();
|
||||
} catch (PreconditionFailedException e) {
|
||||
assertEquals("Resource of type \"Patient\" does not declare conformance to profile from: [http://foo/Profile1, http://foo/Profile2]", e.getMessage());
|
||||
}
|
||||
|
||||
patient = myPatientDao.read(id);
|
||||
assertThat(patient.getMeta().getProfile().stream().map(t -> t.getValue()).collect(Collectors.toList()), containsInAnyOrder("http://foo/Profile1"));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDisallowProfile_CreateBlocked() {
|
||||
|
||||
List<IRepositoryValidatingRule> rules = newRuleBuilder()
|
||||
.forResourcesOfType("Patient")
|
||||
.disallowProfile("http://profile-bad")
|
||||
.build();
|
||||
myValInterceptor.setRules(rules);
|
||||
|
||||
// Disallowed profile blocked
|
||||
try {
|
||||
Patient patient = new Patient();
|
||||
patient.getMeta().addProfile("http://profile-bad");
|
||||
myPatientDao.create(patient);
|
||||
fail();
|
||||
} catch (PreconditionFailedException e) {
|
||||
assertEquals("Resource of type \"Patient\" must not declare conformance to profile: http://profile-bad", e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDisallowProfile_UpdateBlocked() {
|
||||
|
||||
List<IRepositoryValidatingRule> rules = newRuleBuilder()
|
||||
.forResourcesOfType("Patient")
|
||||
.disallowProfiles("http://profile-bad", "http://profile-bad2")
|
||||
.build();
|
||||
myValInterceptor.setRules(rules);
|
||||
|
||||
// Create a resource with an allowable profile
|
||||
Patient patient = new Patient();
|
||||
patient.getMeta().addProfile("http://foo/Profile1");
|
||||
IIdType id = myPatientDao.create(patient).getId();
|
||||
|
||||
// Explicitly adding the profile is blocked using $meta-add
|
||||
try {
|
||||
Meta metaDel = new Meta();
|
||||
metaDel.addProfile("http://profile-bad");
|
||||
myPatientDao.metaAddOperation(id, metaDel, mySrd);
|
||||
fail();
|
||||
} catch (PreconditionFailedException e) {
|
||||
assertEquals("Resource of type \"Patient\" must not declare conformance to profile: http://profile-bad", e.getMessage());
|
||||
}
|
||||
|
||||
// Explicitly adding the profile is blocked using patch
|
||||
try {
|
||||
Parameters patch = new Parameters();
|
||||
Parameters.ParametersParameterComponent operation = patch.addParameter();
|
||||
operation.setName("operation");
|
||||
operation
|
||||
.addPart()
|
||||
.setName("type")
|
||||
.setValue(new CodeType("insert"));
|
||||
operation
|
||||
.addPart()
|
||||
.setName("path")
|
||||
.setValue(new StringType("Patient.meta.profile"));
|
||||
operation
|
||||
.addPart()
|
||||
.setName("index")
|
||||
.setValue(new IntegerType(0));
|
||||
operation
|
||||
.addPart()
|
||||
.setName("value")
|
||||
.setValue(new CanonicalType("http://profile-bad2"));
|
||||
myCaptureQueriesListener.clear();
|
||||
myPatientDao.patch(id, null, PatchTypeEnum.FHIR_PATCH_JSON, null, patch, mySrd);
|
||||
fail();
|
||||
} catch (PreconditionFailedException e) {
|
||||
assertEquals("Resource of type \"Patient\" must not declare conformance to profile: http://profile-bad2", e.getMessage());
|
||||
} catch (Exception e) {
|
||||
myCaptureQueriesListener.logAllQueriesForCurrentThread();
|
||||
fail(e.toString());
|
||||
}
|
||||
|
||||
patient = myPatientDao.read(id);
|
||||
assertThat(patient.getMeta().getProfile().stream().map(t -> t.getValue()).collect(Collectors.toList()), containsInAnyOrder("http://foo/Profile1"));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequireValidation_Allowed() {
|
||||
List<IRepositoryValidatingRule> rules = newRuleBuilder()
|
||||
.forResourcesOfType("Observation")
|
||||
.requireValidationToDeclaredProfiles()
|
||||
.withBestPracticeWarningLevel("IGNORE")
|
||||
.build();
|
||||
myValInterceptor.setRules(rules);
|
||||
|
||||
Observation obs = new Observation();
|
||||
obs.getCode().addCoding().setSystem("http://foo").setCode("123").setDisplay("help im a bug");
|
||||
obs.setStatus(Observation.ObservationStatus.AMENDED);
|
||||
try {
|
||||
IIdType id = myObservationDao.create(obs).getId();
|
||||
assertEquals("1", id.getVersionIdPart());
|
||||
} catch (PreconditionFailedException e) {
|
||||
// should not happen
|
||||
fail(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(e.getOperationOutcome()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequireValidation_Blocked() {
|
||||
List<IRepositoryValidatingRule> rules = newRuleBuilder()
|
||||
.forResourcesOfType("Observation")
|
||||
.requireValidationToDeclaredProfiles()
|
||||
.withBestPracticeWarningLevel("IGNORE")
|
||||
.build();
|
||||
myValInterceptor.setRules(rules);
|
||||
|
||||
Observation obs = new Observation();
|
||||
obs.getCode().addCoding().setSystem("http://foo").setCode("123").setDisplay("help im a bug");
|
||||
obs.addIdentifier().setSystem("system");
|
||||
try {
|
||||
myObservationDao.create(obs).getId();
|
||||
fail();
|
||||
} catch (PreconditionFailedException e) {
|
||||
OperationOutcome oo = (OperationOutcome) e.getOperationOutcome();
|
||||
assertEquals("Observation.status: minimum required = 1, but only found 0 (from http://hl7.org/fhir/StructureDefinition/Observation)", oo.getIssue().get(0).getDiagnostics());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testMultipleTypedRules() {
|
||||
|
||||
List<IRepositoryValidatingRule> rules = newRuleBuilder()
|
||||
.forResourcesOfType("Observation")
|
||||
.requireAtLeastProfile("http://hl7.org/fhir/StructureDefinition/Observation")
|
||||
.and()
|
||||
.requireValidationToDeclaredProfiles()
|
||||
.withBestPracticeWarningLevel(IResourceValidator.BestPracticeWarningLevel.Ignore)
|
||||
.build();
|
||||
myValInterceptor.setRules(rules);
|
||||
|
||||
// Create with correct profile allowed
|
||||
Observation Observation = new Observation();
|
||||
Observation.getMeta().addProfile("http://hl7.org/fhir/StructureDefinition/Observation");
|
||||
try {
|
||||
myObservationDao.create(Observation);
|
||||
fail();
|
||||
} catch (PreconditionFailedException e) {
|
||||
assertThat(e.getMessage(), containsString("Observation.status: minimum required = 1, but only found 0"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private RepositoryValidatingRuleBuilder newRuleBuilder() {
|
||||
return myApplicationContext.getBean(BaseConfig.REPOSITORY_VALIDATING_RULE_BUILDER, RepositoryValidatingRuleBuilder.class);
|
||||
}
|
||||
|
||||
}
|
|
@ -156,7 +156,7 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe
|
|||
|
||||
BundleBuilder builder = new BundleBuilder(myFhirContext);
|
||||
for (IBaseResource next : searchResults.getResources(0, searchResults.size())) {
|
||||
builder.addUpdateEntry(next);
|
||||
builder.addTransactionUpdateEntry(next);
|
||||
}
|
||||
|
||||
operation = theClient.transaction().withBundle(builder.getBundle());
|
||||
|
|
|
@ -1,37 +1,7 @@
|
|||
package ca.uhn.fhir.rest.server.interceptor;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
|
||||
/*
|
||||
* #%L
|
||||
* HAPI FHIR - Server Framework
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2020 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 java.nio.charset.Charset;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import ca.uhn.fhir.interceptor.api.Hook;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
import ca.uhn.fhir.rest.api.EncodingEnum;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.RestfulServerUtils;
|
||||
|
@ -41,6 +11,14 @@ import ca.uhn.fhir.rest.server.method.ResourceParameter;
|
|||
import ca.uhn.fhir.validation.FhirValidator;
|
||||
import ca.uhn.fhir.validation.ResultSeverityEnum;
|
||||
import ca.uhn.fhir.validation.ValidationResult;
|
||||
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
|
||||
/**
|
||||
* This interceptor intercepts each incoming request and if it contains a FHIR resource, validates that resource. The
|
||||
|
@ -53,15 +31,12 @@ public class RequestValidatingInterceptor extends BaseValidatingInterceptor<Stri
|
|||
* X-HAPI-Request-Validation
|
||||
*/
|
||||
public static final String DEFAULT_RESPONSE_HEADER_NAME = "X-FHIR-Request-Validation";
|
||||
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RequestValidatingInterceptor.class);
|
||||
|
||||
/**
|
||||
* A {@link RequestDetails#getUserData() user data} entry will be created with this
|
||||
* key which contains the {@link ValidationResult} from validating the request.
|
||||
*/
|
||||
public static final String REQUEST_VALIDATION_RESULT = RequestValidatingInterceptor.class.getName() + "_REQUEST_VALIDATION_RESULT";
|
||||
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RequestValidatingInterceptor.class);
|
||||
private boolean myAddValidationResultsToResponseOperationOutcome = true;
|
||||
|
||||
@Override
|
||||
|
@ -97,13 +72,24 @@ public class RequestValidatingInterceptor extends BaseValidatingInterceptor<Stri
|
|||
* If set to {@literal true} (default is true), the validation results
|
||||
* will be added to the OperationOutcome being returned to the client,
|
||||
* unless the response being returned is not an OperationOutcome
|
||||
* to begin with (e.g. if the client has requested
|
||||
* to begin with (e.g. if the client has requested
|
||||
* <code>Return: prefer=representation</code>)
|
||||
*/
|
||||
public boolean isAddValidationResultsToResponseOperationOutcome() {
|
||||
return myAddValidationResultsToResponseOperationOutcome;
|
||||
}
|
||||
|
||||
/**
|
||||
* If set to {@literal true} (default is true), the validation results
|
||||
* will be added to the OperationOutcome being returned to the client,
|
||||
* unless the response being returned is not an OperationOutcome
|
||||
* to begin with (e.g. if the client has requested
|
||||
* <code>Return: prefer=representation</code>)
|
||||
*/
|
||||
public void setAddValidationResultsToResponseOperationOutcome(boolean theAddValidationResultsToResponseOperationOutcome) {
|
||||
myAddValidationResultsToResponseOperationOutcome = theAddValidationResultsToResponseOperationOutcome;
|
||||
}
|
||||
|
||||
@Hook(Pointcut.SERVER_OUTGOING_RESPONSE)
|
||||
public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject) {
|
||||
if (myAddValidationResultsToResponseOperationOutcome) {
|
||||
|
@ -128,20 +114,9 @@ public class RequestValidatingInterceptor extends BaseValidatingInterceptor<Stri
|
|||
return DEFAULT_RESPONSE_HEADER_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* If set to {@literal true} (default is true), the validation results
|
||||
* will be added to the OperationOutcome being returned to the client,
|
||||
* unless the response being returned is not an OperationOutcome
|
||||
* to begin with (e.g. if the client has requested
|
||||
* <code>Return: prefer=representation</code>)
|
||||
*/
|
||||
public void setAddValidationResultsToResponseOperationOutcome(boolean theAddValidationResultsToResponseOperationOutcome) {
|
||||
myAddValidationResultsToResponseOperationOutcome = theAddValidationResultsToResponseOperationOutcome;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of the response header to add validation failures to
|
||||
*
|
||||
*
|
||||
* @see #DEFAULT_RESPONSE_HEADER_NAME
|
||||
* @see #setAddResponseHeaderOnSeverity(ResultSeverityEnum)
|
||||
*/
|
||||
|
|
|
@ -40,7 +40,7 @@ public class BundleBuilderTest {
|
|||
Patient patient = new Patient();
|
||||
patient.setId("http://foo/Patient/123");
|
||||
patient.setActive(true);
|
||||
builder.addUpdateEntry(patient);
|
||||
builder.addTransactionUpdateEntry(patient);
|
||||
|
||||
Bundle bundle = (Bundle) builder.getBundle();
|
||||
ourLog.info("Bundle:\n{}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle));
|
||||
|
@ -87,7 +87,7 @@ public class BundleBuilderTest {
|
|||
Patient patient = new Patient();
|
||||
patient.setId("http://foo/Patient/123");
|
||||
patient.setActive(true);
|
||||
builder.addUpdateEntry(patient).conditional("Patient?active=true");
|
||||
builder.addTransactionUpdateEntry(patient).conditional("Patient?active=true");
|
||||
|
||||
Bundle bundle = (Bundle) builder.getBundle();
|
||||
ourLog.info("Bundle:\n{}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle));
|
||||
|
|
|
@ -4,16 +4,12 @@ import ca.uhn.fhir.context.FhirContext;
|
|||
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
|
||||
import ca.uhn.fhir.context.support.IValidationSupport;
|
||||
import ca.uhn.fhir.context.support.ValidationSupportContext;
|
||||
import ca.uhn.fhir.validation.IInstanceValidatorModule;
|
||||
import ca.uhn.fhir.validation.IValidationContext;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.hl7.fhir.convertors.VersionConvertor_10_50;
|
||||
import org.hl7.fhir.exceptions.FHIRException;
|
||||
import org.hl7.fhir.exceptions.PathEngineException;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.r5.model.Base;
|
||||
import org.hl7.fhir.r5.model.Resource;
|
||||
import org.hl7.fhir.r5.model.TypeDetails;
|
||||
import org.hl7.fhir.r5.model.ValueSet;
|
||||
import org.hl7.fhir.r5.utils.FHIRPathEngine;
|
||||
|
@ -211,62 +207,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IInsta
|
|||
protected VersionSpecificWorkerContextWrapper provideWorkerContext() {
|
||||
VersionSpecificWorkerContextWrapper wrappedWorkerContext = myWrappedWorkerContext;
|
||||
if (wrappedWorkerContext == null) {
|
||||
VersionSpecificWorkerContextWrapper.IVersionTypeConverter converter;
|
||||
|
||||
switch (myValidationSupport.getFhirContext().getVersion().getVersion()) {
|
||||
case DSTU2:
|
||||
case DSTU2_HL7ORG: {
|
||||
converter = new VersionSpecificWorkerContextWrapper.IVersionTypeConverter() {
|
||||
@Override
|
||||
public Resource toCanonical(IBaseResource theNonCanonical) {
|
||||
IBaseResource nonCanonical = theNonCanonical;
|
||||
Resource retVal = VersionConvertor_10_50.convertResource((org.hl7.fhir.dstu2.model.Resource) nonCanonical);
|
||||
if (nonCanonical instanceof org.hl7.fhir.dstu2.model.ValueSet) {
|
||||
org.hl7.fhir.dstu2.model.ValueSet valueSet = (org.hl7.fhir.dstu2.model.ValueSet) nonCanonical;
|
||||
if (valueSet.hasCodeSystem() && valueSet.getCodeSystem().hasSystem()) {
|
||||
if (!valueSet.hasCompose()) {
|
||||
ValueSet valueSetR5 = (ValueSet) retVal;
|
||||
valueSetR5.getCompose().addInclude().setSystem(valueSet.getCodeSystem().getSystem());
|
||||
}
|
||||
}
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBaseResource fromCanonical(Resource theCanonical) {
|
||||
IBaseResource canonical = VersionConvertor_10_50.convertResource(theCanonical);
|
||||
return canonical;
|
||||
}
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
case DSTU2_1: {
|
||||
converter = new VersionTypeConverterDstu21();
|
||||
break;
|
||||
}
|
||||
|
||||
case DSTU3: {
|
||||
converter = new VersionTypeConverterDstu3();
|
||||
break;
|
||||
}
|
||||
|
||||
case R4: {
|
||||
converter = new VersionTypeConverterR4();
|
||||
break;
|
||||
}
|
||||
|
||||
case R5: {
|
||||
converter = VersionSpecificWorkerContextWrapper.IDENTITY_VERSION_TYPE_CONVERTER;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
wrappedWorkerContext = new VersionSpecificWorkerContextWrapper(new ValidationSupportContext(myValidationSupport), converter);
|
||||
wrappedWorkerContext = VersionSpecificWorkerContextWrapper.newVersionSpecificWorkerContextWrapper(myValidationSupport);
|
||||
}
|
||||
myWrappedWorkerContext = wrappedWorkerContext;
|
||||
return wrappedWorkerContext;
|
||||
|
|
|
@ -15,6 +15,7 @@ import org.apache.commons.lang3.builder.EqualsBuilder;
|
|||
import org.apache.commons.lang3.builder.HashCodeBuilder;
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import org.fhir.ucum.UcumService;
|
||||
import org.hl7.fhir.convertors.VersionConvertor_10_50;
|
||||
import org.hl7.fhir.exceptions.FHIRException;
|
||||
import org.hl7.fhir.exceptions.TerminologyServiceException;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
@ -653,6 +654,63 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo
|
|||
return retVal;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static VersionSpecificWorkerContextWrapper newVersionSpecificWorkerContextWrapper(IValidationSupport theValidationSupport) {
|
||||
IVersionTypeConverter converter;
|
||||
|
||||
switch (theValidationSupport.getFhirContext().getVersion().getVersion()) {
|
||||
case DSTU2:
|
||||
case DSTU2_HL7ORG: {
|
||||
converter = new IVersionTypeConverter() {
|
||||
@Override
|
||||
public Resource toCanonical(IBaseResource theNonCanonical) {
|
||||
Resource retVal = VersionConvertor_10_50.convertResource((org.hl7.fhir.dstu2.model.Resource) theNonCanonical);
|
||||
if (theNonCanonical instanceof org.hl7.fhir.dstu2.model.ValueSet) {
|
||||
org.hl7.fhir.dstu2.model.ValueSet valueSet = (org.hl7.fhir.dstu2.model.ValueSet) theNonCanonical;
|
||||
if (valueSet.hasCodeSystem() && valueSet.getCodeSystem().hasSystem()) {
|
||||
if (!valueSet.hasCompose()) {
|
||||
ValueSet valueSetR5 = (ValueSet) retVal;
|
||||
valueSetR5.getCompose().addInclude().setSystem(valueSet.getCodeSystem().getSystem());
|
||||
}
|
||||
}
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBaseResource fromCanonical(Resource theCanonical) {
|
||||
return VersionConvertor_10_50.convertResource(theCanonical);
|
||||
}
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
case DSTU2_1: {
|
||||
converter = new VersionTypeConverterDstu21();
|
||||
break;
|
||||
}
|
||||
|
||||
case DSTU3: {
|
||||
converter = new VersionTypeConverterDstu3();
|
||||
break;
|
||||
}
|
||||
|
||||
case R4: {
|
||||
converter = new VersionTypeConverterR4();
|
||||
break;
|
||||
}
|
||||
|
||||
case R5: {
|
||||
converter = IDENTITY_VERSION_TYPE_CONVERTER;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
return new VersionSpecificWorkerContextWrapper(new ValidationSupportContext(theValidationSupport), converter);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue