Add package installer property resource filter on status (#5470)

* Expanding a ValueSet using hierarchical CodeSystem would fail in different scenarios with a constraint violation exception when codes (term concepts) were being persisted

* Expanding a ValueSet using hierarchical CodeSystem would fail in different scenarios with a constraint violation exception when codes (term concepts) were being persisted

* Small changes after code review. Doing some variable/method renaming and correcting changelog Jira issue number.

* There is a programmatic filter enabled which skips installation for certain resources based on their status
in PackageInstallerSvcImpl. The filter can now be controlled via StorageSettings.

* Fix typo in variable name from recent pull request 5461.
This commit is contained in:
Martha Mitran 2023-11-22 13:18:03 -08:00 committed by GitHub
parent 86ea069437
commit f1adacf827
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 125 additions and 105 deletions

View File

@ -0,0 +1,6 @@
---
type: add
issue: 5469
jira: SMILE-7594
title: "There is a programmatic filter enabled which skips installation for certain resources based on their status
in PackageInstallerSvcImpl. The filter can now be controlled via StorageSettings."

View File

@ -28,6 +28,7 @@ import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.context.support.ValidationSupportContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
@ -72,7 +73,6 @@ import javax.annotation.PostConstruct;
import static ca.uhn.fhir.jpa.packages.util.PackageUtils.DEFAULT_INSTALL_TYPES;
import static ca.uhn.fhir.util.SearchParameterUtil.getBaseAsStrings;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
/**
@ -114,6 +114,9 @@ public class PackageInstallerSvcImpl implements IPackageInstallerSvc {
@Autowired
private PackageResourceParsingSvc myPackageResourceParsingSvc;
@Autowired
private JpaStorageSettings myStorageSettings;
/**
* Constructor
*/
@ -506,7 +509,7 @@ public class PackageInstallerSvcImpl implements IPackageInstallerSvc {
if ("SearchParameter".equals(resourceType)) {
String code = SearchParameterUtil.getCode(myFhirContext, theResource);
if (defaultString(code).startsWith("_")) {
if (!isBlank(code) && code.startsWith("_")) {
ourLog.warn(
"Failed to validate resource of type {} with url {} - Error: Resource code starts with \"_\"",
theResource.fhirType(),
@ -548,7 +551,7 @@ public class PackageInstallerSvcImpl implements IPackageInstallerSvc {
* and {@link org.hl7.fhir.r4.model.Communication}, the status field doesn't necessarily need to be set to 'active'
* for that resource to be eligible for upload via packages. For example, all {@link org.hl7.fhir.r4.model.Subscription}
* have a status of {@link org.hl7.fhir.r4.model.Subscription.SubscriptionStatus#REQUESTED} when they are originally
* inserted into the database, so we accept that value for {@link org.hl7.fhir.r4.model.Subscription} isntead.
* inserted into the database, so we accept that value for {@link org.hl7.fhir.r4.model.Subscription} instead.
* Furthermore, {@link org.hl7.fhir.r4.model.DocumentReference} and {@link org.hl7.fhir.r4.model.Communication} can
* exist with a wide variety of values for status that include ones such as
* {@link org.hl7.fhir.r4.model.Communication.CommunicationStatus#ENTEREDINERROR},
@ -560,6 +563,9 @@ public class PackageInstallerSvcImpl implements IPackageInstallerSvc {
* @return {@link Boolean#TRUE} if the status value of this resource is acceptable for package upload.
*/
private boolean isValidResourceStatusForPackageUpload(IBaseResource theResource) {
if (!myStorageSettings.isValidateResourceStatusForPackageUpload()) {
return true;
}
List<IPrimitiveType> statusTypes =
myFhirContext.newFhirPath().evaluate(theResource, "status", IPrimitiveType.class);
// Resource does not have a status field

View File

@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.packages;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionDao;
@ -26,9 +27,11 @@ import org.hl7.fhir.r4.model.SearchParameter;
import org.hl7.fhir.r4.model.Subscription;
import org.hl7.fhir.utilities.npm.NpmPackage;
import org.hl7.fhir.utilities.npm.PackageGenerator;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
@ -46,10 +49,10 @@ import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@ -78,6 +81,8 @@ public class PackageInstallerSvcImplTest {
private SearchParameterHelper mySearchParameterHelper;
@Mock
private SearchParameterMap mySearchParameterMap;
@Mock
private JpaStorageSettings myStorageSettings;
@Spy
private FhirContext myCtx = FhirContext.forR4Cached();
@Spy
@ -103,111 +108,66 @@ public class PackageInstallerSvcImplTest {
mySvc.assertFhirVersionsAreCompatible("R4", "R4B");
}
@Test
public void testValidForUpload_SearchParameterWithMetaParam() {
SearchParameter sp = new SearchParameter();
sp.setCode("_id");
assertFalse(mySvc.validForUpload(sp));
}
@Nested
class ValidForUploadTest {
public static Stream<Arguments> parametersIsValidForUpload() {
SearchParameter sp1 = new SearchParameter();
sp1.setCode("_id");
@Test
public void testValidForUpload_SearchParameterWithNoBase() {
SearchParameter sp = new SearchParameter();
sp.setCode("name");
sp.setExpression("Patient.name");
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
assertFalse(mySvc.validForUpload(sp));
}
SearchParameter sp2 = new SearchParameter();
sp2.setCode("name");
sp2.setExpression("Patient.name");
sp2.setStatus(Enumerations.PublicationStatus.ACTIVE);
@Test
public void testValidForUpload_SearchParameterWithNoExpression() {
SearchParameter sp = new SearchParameter();
sp.setCode("name");
sp.addBase("Patient");
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
assertFalse(mySvc.validForUpload(sp));
}
SearchParameter sp3 = new SearchParameter();
sp3.setCode("name");
sp3.addBase("Patient");
sp3.setStatus(Enumerations.PublicationStatus.ACTIVE);
SearchParameter sp4 = new SearchParameter();
sp4.setCode("name");
sp4.addBase("Patient");
sp4.setExpression("Patient.name");
sp4.setStatus(Enumerations.PublicationStatus.ACTIVE);
@Test
public void testValidForUpload_GoodSearchParameter() {
SearchParameter sp = new SearchParameter();
sp.setCode("name");
sp.addBase("Patient");
sp.setExpression("Patient.name");
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
assertTrue(mySvc.validForUpload(sp));
}
SearchParameter sp5 = new SearchParameter();
sp5.setCode("name");
sp5.addBase("Patient");
sp5.setExpression("Patient.name");
sp5.setStatus(Enumerations.PublicationStatus.DRAFT);
@Test
public void testValidForUpload_RequestedSubscription() {
Subscription.SubscriptionChannelComponent subscriptionChannelComponent =
new Subscription.SubscriptionChannelComponent()
.setType(Subscription.SubscriptionChannelType.RESTHOOK)
.setEndpoint("https://tinyurl.com/2p95e27r");
Subscription subscription = new Subscription();
subscription.setCriteria("Patient?name=smith");
subscription.setChannel(subscriptionChannelComponent);
subscription.setStatus(Subscription.SubscriptionStatus.REQUESTED);
assertTrue(mySvc.validForUpload(subscription));
}
return Stream.of(
arguments(sp1, false, false),
arguments(sp2, false, true),
arguments(sp3, false, true),
arguments(sp4, true, true),
arguments(sp5, true, false),
arguments(createSubscription(Subscription.SubscriptionStatus.REQUESTED), true, true),
arguments(createSubscription(Subscription.SubscriptionStatus.ERROR), true, false),
arguments(createSubscription(Subscription.SubscriptionStatus.ACTIVE), true, false),
arguments(createDocumentReference(Enumerations.DocumentReferenceStatus.ENTEREDINERROR), true, true),
arguments(createDocumentReference(Enumerations.DocumentReferenceStatus.NULL), true, false),
arguments(createDocumentReference(null), true, false),
arguments(createCommunication(Communication.CommunicationStatus.NOTDONE), true, true),
arguments(createCommunication(Communication.CommunicationStatus.NULL), true, false),
arguments(createCommunication(null), true, false));
}
@Test
public void testValidForUpload_ErrorSubscription() {
Subscription.SubscriptionChannelComponent subscriptionChannelComponent =
new Subscription.SubscriptionChannelComponent()
.setType(Subscription.SubscriptionChannelType.RESTHOOK)
.setEndpoint("https://tinyurl.com/2p95e27r");
Subscription subscription = new Subscription();
subscription.setCriteria("Patient?name=smith");
subscription.setChannel(subscriptionChannelComponent);
subscription.setStatus(Subscription.SubscriptionStatus.ERROR);
assertFalse(mySvc.validForUpload(subscription));
}
@ParameterizedTest
@MethodSource(value = "parametersIsValidForUpload")
public void testValidForUpload_withResource(IBaseResource theResource,
boolean theTheMeetsOtherFilterCriteria,
boolean theMeetsStatusFilterCriteria) {
if (theTheMeetsOtherFilterCriteria) {
when(myStorageSettings.isValidateResourceStatusForPackageUpload()).thenReturn(true);
}
assertEquals(theTheMeetsOtherFilterCriteria && theMeetsStatusFilterCriteria, mySvc.validForUpload(theResource));
@Test
public void testValidForUpload_ActiveSubscription() {
Subscription.SubscriptionChannelComponent subscriptionChannelComponent =
new Subscription.SubscriptionChannelComponent()
.setType(Subscription.SubscriptionChannelType.RESTHOOK)
.setEndpoint("https://tinyurl.com/2p95e27r");
Subscription subscription = new Subscription();
subscription.setCriteria("Patient?name=smith");
subscription.setChannel(subscriptionChannelComponent);
subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE);
assertFalse(mySvc.validForUpload(subscription));
}
@Test
public void testValidForUpload_DocumentRefStatusValuePresent() {
DocumentReference documentReference = new DocumentReference();
documentReference.setStatus(Enumerations.DocumentReferenceStatus.ENTEREDINERROR);
assertTrue(mySvc.validForUpload(documentReference));
}
@Test
public void testValidForUpload_DocumentRefStatusValueNull() {
DocumentReference documentReference = new DocumentReference();
documentReference.setStatus(Enumerations.DocumentReferenceStatus.NULL);
assertFalse(mySvc.validForUpload(documentReference));
documentReference.setStatus(null);
assertFalse(mySvc.validForUpload(documentReference));
}
@Test
public void testValidForUpload_CommunicationStatusValuePresent() {
Communication communication = new Communication();
communication.setStatus(Communication.CommunicationStatus.NOTDONE);
assertTrue(mySvc.validForUpload(communication));
}
@Test
public void testValidForUpload_CommunicationStatusValueNull() {
Communication communication = new Communication();
communication.setStatus(Communication.CommunicationStatus.NULL);
assertFalse(mySvc.validForUpload(communication));
communication.setStatus(null);
assertFalse(mySvc.validForUpload(communication));
if (theTheMeetsOtherFilterCriteria) {
when(myStorageSettings.isValidateResourceStatusForPackageUpload()).thenReturn(false);
}
assertEquals(theTheMeetsOtherFilterCriteria, mySvc.validForUpload(theResource));
}
}
@Test
@ -346,4 +306,28 @@ public class PackageInstallerSvcImplTest {
searchParameter.setExpression("someExpression");
return searchParameter;
}
private static Subscription createSubscription(Subscription.SubscriptionStatus theSubscriptionStatus) {
Subscription.SubscriptionChannelComponent subscriptionChannelComponent =
new Subscription.SubscriptionChannelComponent()
.setType(Subscription.SubscriptionChannelType.RESTHOOK)
.setEndpoint("https://tinyurl.com/2p95e27r");
Subscription subscription = new Subscription();
subscription.setCriteria("Patient?name=smith");
subscription.setChannel(subscriptionChannelComponent);
subscription.setStatus(theSubscriptionStatus);
return subscription;
}
private static DocumentReference createDocumentReference(Enumerations.DocumentReferenceStatus theDocumentStatus) {
DocumentReference documentReference = new DocumentReference();
documentReference.setStatus(theDocumentStatus);
return documentReference;
}
private static Communication createCommunication(Communication.CommunicationStatus theCommunicationStatus) {
Communication communication = new Communication();
communication.setStatus(theCommunicationStatus);
return communication;
}
}

View File

@ -146,6 +146,14 @@ public class StorageSettings {
*/
private boolean myLanguageSearchParameterEnabled = false;
/**
* If set to false, all resource types will be installed via package installer, regardless of their status.
* Otherwise, resources will be filtered based on status according to some criteria which can be found in
* <code>PackageInstallerSvcImpl#isValidResourceStatusForPackageUpload<code>
* @since 7.0.0
*/
private boolean myValidateResourceStatusForPackageUpload = true;
/**
* If set to true, the server will prevent the creation of Subscriptions which cannot be evaluated IN-MEMORY. This can improve
* overall server performance.
@ -1319,6 +1327,22 @@ public class StorageSettings {
myLanguageSearchParameterEnabled = theLanguageSearchParameterEnabled;
}
/**
* @return true if the filter is enabled for resources installed via package installer, false otherwise
* @since 7.0.0
*/
public boolean isValidateResourceStatusForPackageUpload() {
return myValidateResourceStatusForPackageUpload;
}
/**
* Should resources being installed via package installer be filtered.
* @since 7.0.0
*/
public void setValidateResourceStatusForPackageUpload(boolean theValidateResourceStatusForPackageUpload) {
myValidateResourceStatusForPackageUpload = theValidateResourceStatusForPackageUpload;
}
private static void validateTreatBaseUrlsAsLocal(String theUrl) {
Validate.notBlank(theUrl, "Base URL must not be null or empty");

View File

@ -97,13 +97,13 @@ public class ValueSetExpansionWithHierarchyR4Test extends BaseTermR4Test {
@ParameterizedTest
@MethodSource(value = "parametersValueSets")
public void testExpandValueSet_whenUsingHierarchicalCodeSystem_willExpandSuccessfully(ValueSet theValueSet, int theExpectedConceptExpensionCount) {
public void testExpandValueSet_whenUsingHierarchicalCodeSystem_willExpandSuccessfully(ValueSet theValueSet, int theExpectedConceptExpansionCount) {
myValueSetDao.create(theValueSet, mySrd);
myTermSvc.preExpandDeferredValueSetsToTerminologyTables();
Optional<TermValueSet> optionalTermValueSet = runInTransaction(() -> myTermValueSetDao.findTermValueSetByUrlAndNullVersion(theValueSet.getUrl()));
assertTrue(optionalTermValueSet.isPresent());
TermValueSet expandedTermValueSet = optionalTermValueSet.get();
assertEquals(TermValueSetPreExpansionStatusEnum.EXPANDED, expandedTermValueSet.getExpansionStatus());
assertEquals(theExpectedConceptExpensionCount, expandedTermValueSet.getTotalConcepts());
assertEquals(theExpectedConceptExpansionCount, expandedTermValueSet.getTotalConcepts());
}
}