Permit uplift (#3978)
* Add test and implementation for permitting uplifts * Add new code paths, error code * Add license * Modify to support arbitrary urls * Modify to support arbitrary urls
This commit is contained in:
parent
26ca950bce
commit
d9134fc553
|
@ -25,7 +25,7 @@ public final class Msg {
|
|||
|
||||
/**
|
||||
* IMPORTANT: Please update the following comment after you add a new code
|
||||
* Last code value: 2131
|
||||
* Last code value: 2132
|
||||
*/
|
||||
|
||||
private Msg() {}
|
||||
|
|
|
@ -1,5 +1,25 @@
|
|||
package ca.uhn.fhir.jpa.api.model;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR Storage api
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
public class Batch2JobOperationResult {
|
||||
// operation name
|
||||
private String myOperation;
|
||||
|
|
|
@ -52,4 +52,8 @@ public class SearchParamSubmitInterceptorLoader {
|
|||
public void setInterceptorRegistry(IInterceptorService theInterceptorRegistry) {
|
||||
myInterceptorRegistry = theInterceptorRegistry;
|
||||
}
|
||||
|
||||
protected SearchParamValidatingInterceptor getSearchParamValidatingInterceptor() {
|
||||
return mySearchParamValidatingInterceptor;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,9 +38,14 @@ import ca.uhn.fhir.rest.param.TokenAndListParam;
|
|||
import ca.uhn.fhir.rest.param.TokenOrListParam;
|
||||
import ca.uhn.fhir.rest.param.TokenParam;
|
||||
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.hl7.fhir.instance.model.api.IBaseExtension;
|
||||
import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
@ -53,6 +58,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
|||
public class SearchParamValidatingInterceptor {
|
||||
|
||||
public static final String SEARCH_PARAM = "SearchParameter";
|
||||
public List<String> myUpliftExtensions;
|
||||
|
||||
private FhirContext myFhirContext;
|
||||
|
||||
|
@ -76,7 +82,6 @@ public class SearchParamValidatingInterceptor {
|
|||
if(isNotSearchParameterResource(theResource)){
|
||||
return;
|
||||
}
|
||||
|
||||
RuntimeSearchParam runtimeSearchParam = mySearchParameterCanonicalizer.canonicalizeSearchParameter(theResource);
|
||||
if (runtimeSearchParam == null) {
|
||||
return;
|
||||
|
@ -84,8 +89,15 @@ public class SearchParamValidatingInterceptor {
|
|||
|
||||
SearchParameterMap searchParameterMap = extractSearchParameterMap(runtimeSearchParam);
|
||||
|
||||
List<ResourcePersistentId> persistedIdList = getDao().searchForIds(searchParameterMap, theRequestDetails);
|
||||
if (isUpliftSearchParam(theResource)) {
|
||||
validateUpliftSp(theRequestDetails, runtimeSearchParam, searchParameterMap);
|
||||
} else {
|
||||
validateStandardSpOnCreate(theRequestDetails, searchParameterMap);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateStandardSpOnCreate(RequestDetails theRequestDetails, SearchParameterMap searchParameterMap) {
|
||||
List<ResourcePersistentId> persistedIdList = getDao().searchForIds(searchParameterMap, theRequestDetails);
|
||||
if( isNotEmpty(persistedIdList) ) {
|
||||
throw new UnprocessableEntityException(Msg.code(2131) + "Can't process submitted SearchParameter as it is overlapping an existing one.");
|
||||
}
|
||||
|
@ -95,24 +107,92 @@ public class SearchParamValidatingInterceptor {
|
|||
if(isNotSearchParameterResource(theResource)){
|
||||
return;
|
||||
}
|
||||
|
||||
RuntimeSearchParam runtimeSearchParam = mySearchParameterCanonicalizer.canonicalizeSearchParameter(theResource);
|
||||
if (runtimeSearchParam == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
SearchParameterMap searchParameterMap = extractSearchParameterMap(runtimeSearchParam);
|
||||
|
||||
List<ResourcePersistentId> pidList = getDao().searchForIds(searchParameterMap, theRequestDetails);
|
||||
if (isUpliftSearchParam(theResource)) {
|
||||
validateUpliftSp(theRequestDetails, runtimeSearchParam, searchParameterMap);
|
||||
} else {
|
||||
validateStandardSpOnUpdate(theRequestDetails, runtimeSearchParam, searchParameterMap);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateUpliftSp(RequestDetails theRequestDetails, RuntimeSearchParam theRuntimeSearchParam, SearchParameterMap theSearchParameterMap) {
|
||||
Validate.notEmpty(getUpliftExtensions(), "You are attempting to validate an Uplift Search Parameter, but have not defined which URLs correspond to uplifted search parameter extensions.");
|
||||
|
||||
IBundleProvider bundleProvider = getDao().search(theSearchParameterMap, theRequestDetails);
|
||||
List<IBaseResource> allResources = bundleProvider.getAllResources();
|
||||
if(isNotEmpty(allResources)) {
|
||||
Set<String> existingIds = allResources.stream().map(resource -> resource.getIdElement().getIdPart()).collect(Collectors.toSet());
|
||||
if (isNewSearchParam(theRuntimeSearchParam, existingIds)) {
|
||||
for (String upliftExtensionUrl: getUpliftExtensions()) {
|
||||
boolean matchesExistingUplift = allResources.stream()
|
||||
.map(sp -> mySearchParameterCanonicalizer.canonicalizeSearchParameter(sp))
|
||||
.filter(sp -> !sp.getExtensions(upliftExtensionUrl).isEmpty())
|
||||
.anyMatch(sp -> isDuplicateUpliftParameter(theRuntimeSearchParam, sp, upliftExtensionUrl));
|
||||
|
||||
if (matchesExistingUplift) {
|
||||
throwDuplicateError();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isDuplicateUpliftParameter(RuntimeSearchParam theRuntimeSearchParam, RuntimeSearchParam theSp, String theUpliftUrl) {
|
||||
String firstCode = getUpliftChildExtensionValueByUrl(theRuntimeSearchParam, "code", theUpliftUrl);
|
||||
String secondCode = getUpliftChildExtensionValueByUrl(theSp, "code", theUpliftUrl);
|
||||
String firstElementName = getUpliftChildExtensionValueByUrl(theRuntimeSearchParam, "element-name", theUpliftUrl);
|
||||
String secondElementName = getUpliftChildExtensionValueByUrl(theSp, "element-name", theUpliftUrl);
|
||||
return firstCode.equals(secondCode) && firstElementName.equals(secondElementName);
|
||||
}
|
||||
|
||||
|
||||
private String getUpliftChildExtensionValueByUrl(RuntimeSearchParam theSp, String theUrl, String theUpliftUrl) {
|
||||
List<IBaseExtension<?, ?>> extensions = theSp.getExtensions(theUpliftUrl);
|
||||
Validate.isTrue(extensions.size() == 1);
|
||||
IBaseExtension<?, ?> topLevelExtension = extensions.get(0);
|
||||
List<IBaseExtension> extension = (List<IBaseExtension>) topLevelExtension.getExtension();
|
||||
String subExtensionValue = extension.stream().filter(ext -> ext.getUrl().equals(theUrl)).map(IBaseExtension::getValue)
|
||||
.map(IPrimitiveType.class::cast)
|
||||
.map(IPrimitiveType::getValueAsString)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new UnprocessableEntityException(Msg.code(2132), "Unable to process Uplift SP addition as the SearchParameter is malformed."));
|
||||
return subExtensionValue;
|
||||
}
|
||||
|
||||
private boolean isNewSearchParam(RuntimeSearchParam theSearchParam, Set<String> theExistingIds) {
|
||||
return theExistingIds
|
||||
.stream()
|
||||
.noneMatch(resId -> resId.equals(theSearchParam.getId().getIdPart()));
|
||||
}
|
||||
|
||||
private void validateStandardSpOnUpdate(RequestDetails theRequestDetails, RuntimeSearchParam runtimeSearchParam, SearchParameterMap searchParameterMap) {
|
||||
List<ResourcePersistentId> pidList = getDao().searchForIds(searchParameterMap, theRequestDetails);
|
||||
if(isNotEmpty(pidList)){
|
||||
Set<String> resolvedResourceIds = myIdHelperService.translatePidsToFhirResourceIds(new HashSet<>(pidList));
|
||||
String incomingResourceId = runtimeSearchParam.getId().getIdPart();
|
||||
if(isNewSearchParam(runtimeSearchParam, resolvedResourceIds)) {
|
||||
throwDuplicateError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean isNewSearchParam = resolvedResourceIds
|
||||
.stream()
|
||||
.noneMatch(resId -> resId.equals(incomingResourceId));
|
||||
|
||||
if(isNewSearchParam){
|
||||
private void throwDuplicateError() {
|
||||
throw new UnprocessableEntityException(Msg.code(2125) + "Can't process submitted SearchParameter as it is overlapping an existing one.");
|
||||
}
|
||||
|
||||
private boolean isUpliftSearchParam(IBaseResource theResource) {
|
||||
if (theResource instanceof IBaseHasExtensions) {
|
||||
IBaseHasExtensions resource = (IBaseHasExtensions) theResource;
|
||||
return resource.getExtension()
|
||||
.stream()
|
||||
.anyMatch(ext -> getUpliftExtensions().contains(ext.getUrl()));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -180,4 +260,14 @@ public class SearchParamValidatingInterceptor {
|
|||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
public List<String> getUpliftExtensions() {
|
||||
if (myUpliftExtensions == null) {
|
||||
myUpliftExtensions = new ArrayList<>();
|
||||
}
|
||||
return myUpliftExtensions;
|
||||
}
|
||||
public void addUpliftExtension(String theUrl) {
|
||||
getUpliftExtensions().add(theUrl);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,10 +8,14 @@ import ca.uhn.fhir.jpa.searchparam.registry.SearchParameterCanonicalizer;
|
|||
import ca.uhn.fhir.jpa.searchparam.submit.interceptor.SearchParamValidatingInterceptor;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
|
||||
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
|
||||
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||
import org.hl7.fhir.r4.model.CodeType;
|
||||
import org.hl7.fhir.r4.model.Enumerations;
|
||||
import org.hl7.fhir.r4.model.Extension;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.hl7.fhir.r4.model.SearchParameter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
@ -34,6 +38,7 @@ import static org.mockito.Mockito.when;
|
|||
public class SearchParameterValidatingInterceptorTest {
|
||||
|
||||
static final FhirContext ourFhirContext = FhirContext.forR4();
|
||||
public static final String UPLIFT_URL = "https://some-url";
|
||||
|
||||
@Mock
|
||||
RequestDetails myRequestDetails;
|
||||
|
@ -48,7 +53,7 @@ public class SearchParameterValidatingInterceptorTest {
|
|||
|
||||
SearchParamValidatingInterceptor mySearchParamValidatingInterceptor;
|
||||
|
||||
SearchParameter mySearchParameterId1;
|
||||
SearchParameter myExistingSearchParameter;
|
||||
|
||||
static String ID1 = "ID1";
|
||||
static String ID2 = "ID2";
|
||||
|
@ -61,8 +66,9 @@ public class SearchParameterValidatingInterceptorTest {
|
|||
mySearchParamValidatingInterceptor.setSearchParameterCanonicalizer(new SearchParameterCanonicalizer(ourFhirContext));
|
||||
mySearchParamValidatingInterceptor.setIIDHelperService(myIdHelperService);
|
||||
mySearchParamValidatingInterceptor.setDaoRegistry(myDaoRegistry);
|
||||
mySearchParamValidatingInterceptor.addUpliftExtension(UPLIFT_URL);
|
||||
|
||||
mySearchParameterId1 = aSearchParameter(ID1);
|
||||
myExistingSearchParameter = buildSearchParameterWithId(ID1);
|
||||
|
||||
}
|
||||
|
||||
|
@ -78,9 +84,9 @@ public class SearchParameterValidatingInterceptorTest {
|
|||
public void whenCreatingNonOverlappingSearchParam_thenIsAllowed(){
|
||||
when(myDaoRegistry.getResourceDao(eq(SearchParamValidatingInterceptor.SEARCH_PARAM))).thenReturn(myIFhirResourceDao);
|
||||
|
||||
setPersistedSearchParameters(emptyList());
|
||||
setPersistedSearchParameterIds(emptyList());
|
||||
|
||||
SearchParameter newSearchParam = aSearchParameter(ID1);
|
||||
SearchParameter newSearchParam = buildSearchParameterWithId(ID1);
|
||||
|
||||
mySearchParamValidatingInterceptor.resourcePreCreate(newSearchParam, myRequestDetails);
|
||||
|
||||
|
@ -90,9 +96,9 @@ public class SearchParameterValidatingInterceptorTest {
|
|||
public void whenCreatingOverlappingSearchParam_thenExceptionIsThrown(){
|
||||
when(myDaoRegistry.getResourceDao(eq(SearchParamValidatingInterceptor.SEARCH_PARAM))).thenReturn(myIFhirResourceDao);
|
||||
|
||||
setPersistedSearchParameters(asList(mySearchParameterId1));
|
||||
setPersistedSearchParameterIds(asList(myExistingSearchParameter));
|
||||
|
||||
SearchParameter newSearchParam = aSearchParameter(ID2);
|
||||
SearchParameter newSearchParam = buildSearchParameterWithId(ID2);
|
||||
|
||||
try {
|
||||
mySearchParamValidatingInterceptor.resourcePreCreate(newSearchParam, myRequestDetails);
|
||||
|
@ -107,9 +113,9 @@ public class SearchParameterValidatingInterceptorTest {
|
|||
public void whenUsingPutOperationToCreateNonOverlappingSearchParam_thenIsAllowed(){
|
||||
when(myDaoRegistry.getResourceDao(eq(SearchParamValidatingInterceptor.SEARCH_PARAM))).thenReturn(myIFhirResourceDao);
|
||||
|
||||
setPersistedSearchParameters(emptyList());
|
||||
setPersistedSearchParameterIds(emptyList());
|
||||
|
||||
SearchParameter newSearchParam = aSearchParameter(ID1);
|
||||
SearchParameter newSearchParam = buildSearchParameterWithId(ID1);
|
||||
|
||||
mySearchParamValidatingInterceptor.resourcePreUpdate(null, newSearchParam, myRequestDetails);
|
||||
}
|
||||
|
@ -118,9 +124,9 @@ public class SearchParameterValidatingInterceptorTest {
|
|||
public void whenUsingPutOperationToCreateOverlappingSearchParam_thenExceptionIsThrown(){
|
||||
when(myDaoRegistry.getResourceDao(eq(SearchParamValidatingInterceptor.SEARCH_PARAM))).thenReturn(myIFhirResourceDao);
|
||||
|
||||
setPersistedSearchParameters(asList(mySearchParameterId1));
|
||||
setPersistedSearchParameterIds(asList(myExistingSearchParameter));
|
||||
|
||||
SearchParameter newSearchParam = aSearchParameter(ID2);
|
||||
SearchParameter newSearchParam = buildSearchParameterWithId(ID2);
|
||||
|
||||
try {
|
||||
mySearchParamValidatingInterceptor.resourcePreUpdate(null, newSearchParam, myRequestDetails);
|
||||
|
@ -134,28 +140,77 @@ public class SearchParameterValidatingInterceptorTest {
|
|||
public void whenUpdateSearchParam_thenIsAllowed(){
|
||||
when(myDaoRegistry.getResourceDao(eq(SearchParamValidatingInterceptor.SEARCH_PARAM))).thenReturn(myIFhirResourceDao);
|
||||
|
||||
setPersistedSearchParameters(asList(mySearchParameterId1));
|
||||
when(myIdHelperService.translatePidsToFhirResourceIds(any())).thenReturn(Set.of(mySearchParameterId1.getId()));
|
||||
setPersistedSearchParameterIds(asList(myExistingSearchParameter));
|
||||
when(myIdHelperService.translatePidsToFhirResourceIds(any())).thenReturn(Set.of(myExistingSearchParameter.getId()));
|
||||
|
||||
|
||||
SearchParameter newSearchParam = aSearchParameter(ID1);
|
||||
SearchParameter newSearchParam = buildSearchParameterWithId(ID1);
|
||||
|
||||
mySearchParamValidatingInterceptor.resourcePreUpdate(null, newSearchParam, myRequestDetails);
|
||||
|
||||
}
|
||||
|
||||
private void setPersistedSearchParameters(List<SearchParameter> theSearchParams){
|
||||
@Test
|
||||
public void whenUpliftSearchParameter_thenMoreGranularComparisonSucceeds() {
|
||||
when(myDaoRegistry.getResourceDao(eq(SearchParamValidatingInterceptor.SEARCH_PARAM))).thenReturn(myIFhirResourceDao);
|
||||
|
||||
setPersistedSearchParameters(asList(myExistingSearchParameter));
|
||||
|
||||
SearchParameter newSearchParam = buildSearchParameterWithUpliftExtension(ID2);
|
||||
|
||||
mySearchParamValidatingInterceptor.resourcePreUpdate(null, newSearchParam, myRequestDetails);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenUpliftSearchParameter_thenMoreGranularComparisonFails() {
|
||||
when(myDaoRegistry.getResourceDao(eq(SearchParamValidatingInterceptor.SEARCH_PARAM))).thenReturn(myIFhirResourceDao);
|
||||
SearchParameter existingUpliftSp = buildSearchParameterWithUpliftExtension(ID1);
|
||||
setPersistedSearchParameters(asList(existingUpliftSp));
|
||||
|
||||
SearchParameter newSearchParam = buildSearchParameterWithUpliftExtension(ID2);
|
||||
|
||||
try {
|
||||
mySearchParamValidatingInterceptor.resourcePreUpdate(null, newSearchParam, myRequestDetails);
|
||||
fail();
|
||||
}catch (UnprocessableEntityException e){
|
||||
assertTrue(e.getMessage().contains("2125"));
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private SearchParameter buildSearchParameterWithUpliftExtension(String theID) {
|
||||
SearchParameter newSearchParam = buildSearchParameterWithId(theID);
|
||||
|
||||
Extension topLevelExtension = new Extension();
|
||||
topLevelExtension.setUrl(UPLIFT_URL);
|
||||
|
||||
Extension codeExtension = new Extension();
|
||||
codeExtension.setUrl("code");
|
||||
codeExtension.setValue(new CodeType("identifier"));
|
||||
|
||||
Extension elementExtension = new Extension();
|
||||
elementExtension.setUrl("element-name");
|
||||
elementExtension.setValue(new CodeType("patient-identifier"));
|
||||
|
||||
topLevelExtension.addExtension(codeExtension);
|
||||
topLevelExtension.addExtension(elementExtension);
|
||||
newSearchParam.addExtension(topLevelExtension);
|
||||
return newSearchParam;
|
||||
}
|
||||
|
||||
private void setPersistedSearchParameterIds(List<SearchParameter> theSearchParams){
|
||||
List<ResourcePersistentId> resourcePersistentIds = theSearchParams
|
||||
.stream()
|
||||
.map(SearchParameter::getId)
|
||||
.map(theS -> new ResourcePersistentId(theS))
|
||||
.collect(Collectors.toList());
|
||||
Set<String> ids = theSearchParams.stream().map(sp -> sp.getId()).collect(Collectors.toSet());
|
||||
|
||||
when(myIFhirResourceDao.searchForIds(any(), any())).thenReturn(resourcePersistentIds);
|
||||
}
|
||||
private void setPersistedSearchParameters(List<SearchParameter> theSearchParams) {
|
||||
when(myIFhirResourceDao.search(any(), any())).thenReturn(new SimpleBundleProvider(theSearchParams));
|
||||
}
|
||||
|
||||
private SearchParameter aSearchParameter(String id) {
|
||||
private SearchParameter buildSearchParameterWithId(String id) {
|
||||
SearchParameter retVal = new SearchParameter();
|
||||
retVal.setId(id);
|
||||
retVal.setCode("patient");
|
||||
|
|
Loading…
Reference in New Issue