Delete expunge with cascade (#4931)

* Delete expunge with cascade

* Work

* Workgin

* Version bump hibernate

* Start working on delete cascade

* Work on delete expunge

* Test fixes

* Test fixes

* Add changelog

* Work on cascade

* Fixes

* Test work

* Test fixes
This commit is contained in:
James Agnew 2023-05-27 11:07:59 -04:00 committed by GitHub
parent f8548c5d7b
commit 46857711c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
110 changed files with 1300 additions and 923 deletions

View File

@ -111,7 +111,6 @@
<classpathDependencyExcludes>
<dependencyExclude>org.slf4j:slf4j-android</dependencyExclude>
</classpathDependencyExcludes>
<redirectTestOutputToFile>true</redirectTestOutputToFile>
</configuration>
<executions>
<execution>

View File

@ -2122,7 +2122,7 @@ public enum Pointcut implements IPointcut {
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
* </li>
* <li>
* ca.uhn.fhir.context.RuntimeResourceDefinition - the resource type being accessed
* ca.uhn.fhir.context.RuntimeResourceDefinition - The resource type being accessed, or {@literal null} if no specific type is associated with the request.
* </li>
* </ul>
* <p>

View File

@ -263,7 +263,9 @@ public class Constants {
public static final String PARAM_SEARCH_TOTAL_MODE = "_total";
public static final String CAPABILITYSTATEMENT_WEBSOCKET_URL = "http://hl7.org/fhir/StructureDefinition/capabilitystatement-websocket";
public static final String PARAMETER_CASCADE_DELETE = "_cascade";
public static final String PARAMETER_CASCADE_DELETE_MAX_ROUNDS = "_maxRounds";
public static final String HEADER_CASCADE = "X-Cascade";
public static final String HEADER_CASCADE_MAX_ROUNDS = "max-rounds";
public static final String CASCADE_DELETE = "delete";
public static final int MAX_RESOURCE_NAME_LENGTH = 100;
public static final String CACHE_CONTROL_PRIVATE = "private";

View File

@ -0,0 +1,6 @@
---
type: add
issue: 4931
title: "The `$delete-expunge` operation has a new parameter `cascade` that can be used to
request that resources with indexed references to other resources being deleted should also
be deleted."

View File

@ -1123,7 +1123,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
JobInstanceStartRequest request = new JobInstanceStartRequest();
request.setJobDefinitionId(ReindexAppCtx.JOB_REINDEX);
request.setParameters(params);
myJobCoordinator.startInstance(request);
myJobCoordinator.startInstance(theRequestDetails, request);
ourLog.debug("Started reindex job with parameters {}", params);

View File

@ -34,7 +34,9 @@ import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class DeleteExpungeSqlBuilder {
@ -53,10 +55,10 @@ public class DeleteExpungeSqlBuilder {
@Nonnull
List<String> convertPidsToDeleteExpungeSql(List<JpaPid> theJpaPids) {
List<Long> pids = JpaPid.toLongList(theJpaPids);
DeleteExpungeSqlResult convertPidsToDeleteExpungeSql(List<JpaPid> theJpaPids, boolean theCascade, Integer theCascadeMaxRounds) {
validateOkToDeleteAndExpunge(pids);
Set<Long> pids = JpaPid.toLongSet(theJpaPids);
validateOkToDeleteAndExpunge(pids, theCascade, theCascadeMaxRounds);
List<String> rawSql = new ArrayList<>();
@ -70,10 +72,10 @@ public class DeleteExpungeSqlBuilder {
// Lastly we need to delete records from the resource table all of these other tables link to:
ResourceForeignKey resourceTablePk = new ResourceForeignKey("HFJ_RESOURCE", "RES_ID");
rawSql.add(deleteRecordsByColumnSql(pidListString, resourceTablePk));
return rawSql;
return new DeleteExpungeSqlResult(rawSql, pids.size());
}
public void validateOkToDeleteAndExpunge(List<Long> thePids) {
public void validateOkToDeleteAndExpunge(Set<Long> thePids, boolean theCascade, Integer theCascadeMaxRounds) {
if (!myStorageSettings.isEnforceReferentialIntegrityOnDelete()) {
ourLog.info("Referential integrity on delete disabled. Skipping referential integrity check.");
return;
@ -87,6 +89,40 @@ public class DeleteExpungeSqlBuilder {
return;
}
if (theCascade) {
int cascadeMaxRounds = Integer.MAX_VALUE;
if (theCascadeMaxRounds != null) {
cascadeMaxRounds = theCascadeMaxRounds;
}
if (myStorageSettings.getMaximumDeleteConflictQueryCount() != null) {
if (myStorageSettings.getMaximumDeleteConflictQueryCount() < cascadeMaxRounds) {
cascadeMaxRounds = myStorageSettings.getMaximumDeleteConflictQueryCount();
}
}
while (true) {
List<JpaPid> addedThisRound = new ArrayList<>();
for (ResourceLink next : conflictResourceLinks) {
Long nextPid = next.getSourceResourcePid();
if (thePids.add(nextPid)) {
addedThisRound.add(JpaPid.fromId(nextPid));
}
}
if (addedThisRound.isEmpty()) {
return;
}
if (--cascadeMaxRounds > 0) {
conflictResourceLinks = Collections.synchronizedList(new ArrayList<>());
findResourceLinksWithTargetPidIn(addedThisRound, addedThisRound, conflictResourceLinks);
} else {
// We'll proceed to below where we throw an exception
break;
}
}
}
ResourceLink firstConflict = conflictResourceLinks.get(0);
//NB-GGG: We previously instantiated these ID values from firstConflict.getSourceResource().getIdDt(), but in a situation where we
@ -119,4 +155,26 @@ public class DeleteExpungeSqlBuilder {
private String deleteRecordsByColumnSql(String thePidListString, ResourceForeignKey theResourceForeignKey) {
return "DELETE FROM " + theResourceForeignKey.table + " WHERE " + theResourceForeignKey.key + " IN " + thePidListString;
}
public static class DeleteExpungeSqlResult {
private final List<String> mySqlStatements;
private final int myRecordCount;
public DeleteExpungeSqlResult(List<String> theSqlStatments, int theRecordCount) {
mySqlStatements = theSqlStatments;
myRecordCount = theRecordCount;
}
public List<String> getSqlStatements() {
return mySqlStatements;
}
public int getRecordCount() {
return myRecordCount;
}
}
}

View File

@ -26,14 +26,11 @@ import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.stream.Collectors;
@Transactional(propagation = Propagation.MANDATORY)
public class DeleteExpungeSvcImpl implements IDeleteExpungeSvc<JpaPid> {
private static final Logger ourLog = LoggerFactory.getLogger(DeleteExpungeSvcImpl.class);
@ -48,8 +45,9 @@ public class DeleteExpungeSvcImpl implements IDeleteExpungeSvc<JpaPid> {
}
@Override
public void deleteExpunge(List<JpaPid> theJpaPids) {
List<String> sqlList = myDeleteExpungeSqlBuilder.convertPidsToDeleteExpungeSql(theJpaPids);
public int deleteExpunge(List<JpaPid> theJpaPids, boolean theCascade, Integer theCascadeMaxRounds) {
DeleteExpungeSqlBuilder.DeleteExpungeSqlResult sqlResult = myDeleteExpungeSqlBuilder.convertPidsToDeleteExpungeSql(theJpaPids, theCascade, theCascadeMaxRounds);
List<String> sqlList = sqlResult.getSqlStatements();
ourLog.debug("Executing {} delete expunge sql commands", sqlList.size());
long totalDeleted = 0;
@ -62,6 +60,12 @@ public class DeleteExpungeSvcImpl implements IDeleteExpungeSvc<JpaPid> {
clearHibernateSearchIndex(theJpaPids);
// TODO KHS instead of logging progress, produce result chunks that get aggregated into a delete expunge report
return sqlResult.getRecordCount();
}
@Override
public boolean isCascadeSupported() {
return true;
}
/**

View File

@ -177,6 +177,6 @@ public class CascadingDeleteInterceptor {
*/
@Nonnull
protected DeleteCascadeModeEnum shouldCascade(@Nullable RequestDetails theRequest) {
return RestfulServerUtils.extractDeleteCascadeParameter(theRequest);
return RestfulServerUtils.extractDeleteCascadeParameter(theRequest).getMode();
}
}

View File

@ -30,6 +30,7 @@ import ca.uhn.fhir.jpa.api.pid.IResourcePidList;
import ca.uhn.fhir.jpa.api.pid.MixedResourcePidList;
import ca.uhn.fhir.jpa.api.svc.IBatch2DaoSvc;
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
@ -66,20 +67,26 @@ public class Batch2DaoSvcImpl implements IBatch2DaoSvc {
@Autowired
private FhirContext myFhirContext;
@Autowired
private IHapiTransactionService myTransactionService;
@Override
public boolean isAllResourceTypeSupported() {
return true;
}
@Override
@Transactional
public IResourcePidList fetchResourceIdsPage(Date theStart, Date theEnd, @Nonnull Integer thePageSize, @Nullable RequestPartitionId theRequestPartitionId, @Nullable String theUrl) {
if (theUrl == null) {
return fetchResourceIdsPageNoUrl(theStart, theEnd, thePageSize, theRequestPartitionId);
} else {
return fetchResourceIdsPageWithUrl(theStart, theEnd, thePageSize, theUrl, theRequestPartitionId);
}
return myTransactionService
.withSystemRequest()
.withRequestPartitionId(theRequestPartitionId)
.execute(()->{
if (theUrl == null) {
return fetchResourceIdsPageNoUrl(theStart, theEnd, thePageSize, theRequestPartitionId);
} else {
return fetchResourceIdsPageWithUrl(theStart, theEnd, thePageSize, theUrl, theRequestPartitionId);
}
});
}
private IResourcePidList fetchResourceIdsPageWithUrl(Date theStart, Date theEnd, int thePageSize, String theUrl, RequestPartitionId theRequestPartitionId) {
@ -104,7 +111,7 @@ public class Batch2DaoSvcImpl implements IBatch2DaoSvc {
lastDate = dao.readByPid(lastResourcePersistentId, true).getMeta().getLastUpdated();
}
return new HomogeneousResourcePidList(resourceType, ids, lastDate);
return new HomogeneousResourcePidList(resourceType, ids, lastDate, theRequestPartitionId);
}
@Nonnull
@ -136,6 +143,6 @@ public class Batch2DaoSvcImpl implements IBatch2DaoSvc {
Date lastDate = (Date) content.get(content.size() - 1)[2];
return new MixedResourcePidList(types, ids, lastDate);
return new MixedResourcePidList(types, ids, lastDate, theRequestPartitionId);
}
}

View File

@ -41,6 +41,7 @@ import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc;
import ca.uhn.fhir.jpa.term.api.ITermVersionAdapterSvc;
import ca.uhn.fhir.jpa.term.models.TermCodeSystemDeleteJobParameters;
import ca.uhn.fhir.jpa.term.models.TermCodeSystemDeleteVersionJobParameters;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.TimeoutManager;
import com.google.common.annotations.VisibleForTesting;
@ -372,7 +373,7 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc, IHas
parameters.setCodeSystemVersionPid(theCodeSystemVersionPid);
request.setParameters(parameters);
Batch2JobStartResponse response = myJobCoordinator.startInstance(request);
Batch2JobStartResponse response = myJobCoordinator.startInstance(new SystemRequestDetails(), request);
myJobExecutions.add(response.getInstanceId());
}
@ -382,7 +383,7 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc, IHas
JobInstanceStartRequest request = new JobInstanceStartRequest();
request.setParameters(parameters);
request.setJobDefinitionId(TERM_CODE_SYSTEM_DELETE_JOB_NAME);
Batch2JobStartResponse response = myJobCoordinator.startInstance(request);
Batch2JobStartResponse response = myJobCoordinator.startInstance(new SystemRequestDetails(), request);
myJobExecutions.add(response.getInstanceId());
}

View File

@ -212,16 +212,6 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<!--<useManifestOnlyJar>false</useManifestOnlyJar>-->
<forkCount>1</forkCount>
<reuseForks>false</reuseForks>
<runOrder>alphabetical</runOrder>
<includes>
<include>**/*IT.java</include>
</includes>
<useModulePath>false</useModulePath>
</configuration>
<executions>
<execution>
<goals>
@ -242,11 +232,7 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<runOrder>alphabetical</runOrder>
<argLine>@{argLine} ${surefire_jvm_args}</argLine>
<forkCount>0.6C</forkCount>
<excludes>*StressTest*</excludes>
<skip>${skipFailsafe}</skip>
</configuration>
</plugin>
<plugin>
@ -283,9 +269,6 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<runOrder>alphabetical</runOrder>
</configuration>
</plugin>
</plugins>
</build>

View File

@ -83,7 +83,7 @@ public class BulkGroupExportWithIndexedSearchParametersTest extends BaseJpaTest
}
private BulkExportJobResults getBulkExportJobResults(BulkDataExportOptions theOptions) {
Batch2JobStartResponse startResponse = myJobRunner.startNewJob(BulkExportUtils.createBulkExportJobParametersFromExportOptions(theOptions));
Batch2JobStartResponse startResponse = myJobRunner.startNewJob(mySrd, BulkExportUtils.createBulkExportJobParametersFromExportOptions(theOptions));
assertNotNull(startResponse);

View File

@ -95,15 +95,6 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<forkCount>1</forkCount>
<reuseForks>false</reuseForks>
<runOrder>alphabetical</runOrder>
<includes>
<include>**/*IT.java</include>
</includes>
<useModulePath>false</useModulePath>
</configuration>
<executions>
<execution>
<goals>

View File

@ -83,6 +83,6 @@ public class GoldenResourceSearchSvcImpl implements IGoldenResourceSearchSvc {
lastDate = dao.readByPid(ids.get(ids.size() - 1)).getMeta().getLastUpdated();
}
return new HomogeneousResourcePidList(theResourceType, ids, lastDate);
return new HomogeneousResourcePidList(theResourceType, ids, lastDate, theRequestPartitionId);
}
}

View File

@ -220,7 +220,7 @@ public class MdmControllerSvcImpl implements IMdmControllerSvc {
JobInstanceStartRequest request = new JobInstanceStartRequest();
request.setJobDefinitionId(MdmClearAppCtx.JOB_MDM_CLEAR);
request.setParameters(params);
Batch2JobStartResponse response = myJobCoordinator.startInstance(request);
Batch2JobStartResponse response = myJobCoordinator.startInstance(theRequestDetails, request);
String id = response.getInstanceId();
IBaseParameters retVal = ParametersUtil.newInstance(myFhirContext);
@ -244,7 +244,7 @@ public class MdmControllerSvcImpl implements IMdmControllerSvc {
request.setParameters(params);
request.setJobDefinitionId(MdmSubmitAppCtx.MDM_SUBMIT_JOB);
Batch2JobStartResponse batch2JobStartResponse = myJobCoordinator.startInstance(request);
Batch2JobStartResponse batch2JobStartResponse = myJobCoordinator.startInstance(theRequestDetails, request);
String id = batch2JobStartResponse.getInstanceId();
IBaseParameters retVal = ParametersUtil.newInstance(myFhirContext);

View File

@ -23,8 +23,10 @@ import ca.uhn.fhir.rest.api.server.storage.BaseResourcePersistentId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* JPA implementation of IResourcePersistentId. JPA uses a Long as the primary key. This class should be used in any
@ -61,7 +63,15 @@ public class JpaPid extends BaseResourcePersistentId<Long> {
return retVal;
}
public static List<JpaPid> fromLongList(List<Long> theResultList) {
public static Set<Long> toLongSet(Collection<JpaPid> thePids) {
Set<Long> retVal = new HashSet<>(thePids.size());
for (JpaPid next : thePids) {
retVal.add(next.getId());
}
return retVal;
}
public static List<JpaPid> fromLongList(Collection<Long> theResultList) {
List<JpaPid> retVal = new ArrayList<>(theResultList.size());
for (Long next : theResultList) {
retVal.add(fromId(next));

View File

@ -185,10 +185,7 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<runOrder>alphabetical</runOrder>
<argLine>@{argLine} ${surefire_jvm_args}</argLine>
<forkCount>0.6C</forkCount>
<excludes>*StressTest*</excludes>
</configuration>
</plugin>
</plugins>

View File

@ -35,16 +35,6 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<!--<useManifestOnlyJar>false</useManifestOnlyJar>-->
<forkCount>1</forkCount>
<reuseForks>false</reuseForks>
<runOrder>alphabetical</runOrder>
<includes>
<include>**/*IT.java</include>
</includes>
<useModulePath>false</useModulePath>
</configuration>
<executions>
<execution>
<goals>
@ -65,11 +55,7 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<runOrder>alphabetical</runOrder>
<argLine>@{argLine} ${surefire_jvm_args}</argLine>
<forkCount>0.6C</forkCount>
<excludes>*StressTest*</excludes>
<skip>${skipFailsafe}</skip>
</configuration>
</plugin>
<plugin>
@ -106,9 +92,6 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<runOrder>alphabetical</runOrder>
</configuration>
</plugin>
</plugins>
</build>

View File

@ -122,28 +122,6 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test {
myStorageSettings.setHistoryCountMode(JpaStorageSettings.DEFAULT_HISTORY_COUNT_MODE);
}
private void assertGone(IIdType theId) {
try {
assertNotGone(theId);
fail();
} catch (ResourceGoneException e) {
// good
}
}
/**
* This gets called from assertGone too! Careful about exceptions...
*/
private void assertNotGone(IIdType theId) {
if ("Patient".equals(theId.getResourceType())) {
myPatientDao.read(theId, mySrd);
} else if ("Organization".equals(theId.getResourceType())) {
myOrganizationDao.read(theId, mySrd);
} else {
fail("No type");
}
}
@BeforeEach
public void beforeDisableResultReuse() {
myStorageSettings.setReuseCachedSearchResultsForMillis(null);

View File

@ -41,17 +41,8 @@ public class ResourceProviderExpungeDstu2Test extends BaseResourceProviderDstu2T
}
}
private void assertGone(IIdType theId) {
try {
getDao(theId).read(theId);
fail();
} catch (ResourceGoneException e) {
// good
}
}
private void assertStillThere(IIdType theId) {
getDao(theId).read(theId);
assertNotGone(theId);
}
@Override

View File

@ -35,16 +35,6 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<!--<useManifestOnlyJar>false</useManifestOnlyJar>-->
<forkCount>1</forkCount>
<reuseForks>false</reuseForks>
<runOrder>alphabetical</runOrder>
<includes>
<include>**/*IT.java</include>
</includes>
<useModulePath>false</useModulePath>
</configuration>
<executions>
<execution>
<goals>
@ -65,11 +55,7 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<runOrder>alphabetical</runOrder>
<argLine>@{argLine} ${surefire_jvm_args}</argLine>
<forkCount>0.6C</forkCount>
<excludes>*StressTest*</excludes>
<skip>${skipFailsafe}</skip>
</configuration>
</plugin>
<plugin>
@ -106,9 +92,6 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<runOrder>alphabetical</runOrder>
</configuration>
</plugin>
</plugins>
</build>

View File

@ -140,31 +140,6 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test {
myStorageSettings.setHistoryCountMode(JpaStorageSettings.DEFAULT_HISTORY_COUNT_MODE);
}
private void assertGone(IIdType theId) {
try {
assertNotGone(theId);
fail();
} catch (ResourceGoneException e) {
// good
}
}
/**
* This gets called from assertGone too! Careful about exceptions...
*/
private void assertNotGone(IIdType theId) {
if ("Patient".equals(theId.getResourceType())) {
myPatientDao.read(theId, mySrd);
} else if ("Organization".equals(theId.getResourceType())) {
myOrganizationDao.read(theId, mySrd);
} else if ("CodeSystem".equals(theId.getResourceType())) {
myCodeSystemDao.read(theId, mySrd);
} else {
fail("Can't handle type: " + theId.getResourceType());
}
}
@BeforeEach
public void beforeDisableResultReuse() {
myStorageSettings.setReuseCachedSearchResultsForMillis(null);

View File

@ -50,17 +50,8 @@ public class ResourceProviderExpungeDstu3Test extends BaseResourceProviderDstu3T
}
}
private void assertGone(IIdType theId) {
try {
getDao(theId).read(theId);
fail();
} catch (ResourceGoneException e) {
// good
}
}
private void assertStillThere(IIdType theId) {
getDao(theId).read(theId);
assertNotGone(theId);
}
@Override

View File

@ -62,17 +62,6 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<!--<useManifestOnlyJar>false</useManifestOnlyJar>-->
<forkCount>1</forkCount>
<redirectTestOutputToFile>true</redirectTestOutputToFile>
<reuseForks>false</reuseForks>
<runOrder>alphabetical</runOrder>
<includes>
<include>**/*IT.java</include>
</includes>
<useModulePath>false</useModulePath>
</configuration>
<executions>
<execution>
<goals>
@ -93,11 +82,7 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<runOrder>alphabetical</runOrder>
<argLine>@{argLine} ${surefire_jvm_args} -XX:+HeapDumpOnOutOfMemoryError</argLine>
<forkCount>0.6C</forkCount>
<excludes>*StressTest*</excludes>
<skip>${skipFailsafe}</skip>
<argLine>@{argLine} ${surefire_jvm_args}</argLine>
</configuration>
</plugin>
<plugin>
@ -128,20 +113,6 @@
</plugins>
</build>
</profile>
<profile>
<id>CI</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<runOrder>alphabetical</runOrder>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@ -240,7 +240,7 @@ public class BulkDataErrorAbuseTest extends BaseResourceProviderR4Test {
private String startJob(BulkDataExportOptions theOptions) {
BulkExportParameters startRequest = BulkExportUtils.createBulkExportJobParametersFromExportOptions(theOptions);
startRequest.setUseExistingJobsFirst(false);
Batch2JobStartResponse startResponse = myJobRunner.startNewJob(startRequest);
Batch2JobStartResponse startResponse = myJobRunner.startNewJob(null, startRequest);
assertNotNull(startResponse);
return startResponse.getInstanceId();
}

View File

@ -79,6 +79,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.isNotNull;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
@ -160,7 +161,7 @@ public class BulkDataExportProviderTest {
private BulkExportParameters verifyJobStart() {
ArgumentCaptor<Batch2BaseJobParameters> startJobCaptor = ArgumentCaptor.forClass(Batch2BaseJobParameters.class);
verify(myJobRunner).startNewJob(startJobCaptor.capture());
verify(myJobRunner).startNewJob(isNotNull(), startJobCaptor.capture());
Batch2BaseJobParameters sp = startJobCaptor.getValue();
assertTrue(sp instanceof BulkExportParameters);
return (BulkExportParameters) sp;
@ -197,7 +198,7 @@ public class BulkDataExportProviderTest {
String practitionerResource = "Practitioner";
String filter = "Patient?identifier=foo";
String postFetchFilter = "Patient?_tag=foo";
when(myJobRunner.startNewJob(any()))
when(myJobRunner.startNewJob(isNotNull(), any()))
.thenReturn(createJobStartResponse());
InstantType now = InstantType.now();
@ -249,7 +250,7 @@ public class BulkDataExportProviderTest {
@Test
public void testOmittingOutputFormatDefaultsToNdjson() throws IOException {
when(myJobRunner.startNewJob(any()))
when(myJobRunner.startNewJob(isNotNull(), any()))
.thenReturn(createJobStartResponse());
Parameters input = new Parameters();
@ -270,7 +271,7 @@ public class BulkDataExportProviderTest {
@ParameterizedTest
@MethodSource("paramsProvider")
public void testSuccessfulInitiateBulkRequest_GetWithPartitioning(boolean partitioningEnabled) throws IOException {
when(myJobRunner.startNewJob(any())).thenReturn(createJobStartResponse());
when(myJobRunner.startNewJob(isNotNull(), any())).thenReturn(createJobStartResponse());
InstantType now = InstantType.now();
@ -307,7 +308,7 @@ public class BulkDataExportProviderTest {
@Test
public void testSuccessfulInitiateBulkRequest_Get_MultipleTypeFilters() throws IOException {
when(myJobRunner.startNewJob(any()))
when(myJobRunner.startNewJob(isNotNull(), any()))
.thenReturn(createJobStartResponse());
String url = myServer.getBaseUrl() + "/" + JpaConstants.OPERATION_EXPORT
@ -583,7 +584,7 @@ public class BulkDataExportProviderTest {
@Test
public void testSuccessfulInitiateGroupBulkRequest_Post() throws IOException {
// when
when(myJobRunner.startNewJob(any()))
when(myJobRunner.startNewJob(isNotNull(), any()))
.thenReturn(createJobStartResponse(G_JOB_ID));
InstantType now = InstantType.now();
@ -624,7 +625,7 @@ public class BulkDataExportProviderTest {
@Test
public void testSuccessfulInitiateGroupBulkRequest_Get() throws IOException {
// when
when(myJobRunner.startNewJob(any())).thenReturn(createJobStartResponse(G_JOB_ID));
when(myJobRunner.startNewJob(isNotNull(), any())).thenReturn(createJobStartResponse(G_JOB_ID));
InstantType now = InstantType.now();
@ -713,7 +714,7 @@ public class BulkDataExportProviderTest {
@Test
public void testInitiateGroupExportWithNoResourceTypes() throws IOException {
// when
when(myJobRunner.startNewJob(any(Batch2BaseJobParameters.class)))
when(myJobRunner.startNewJob(isNotNull(), any(Batch2BaseJobParameters.class)))
.thenReturn(createJobStartResponse());
// test
@ -739,7 +740,7 @@ public class BulkDataExportProviderTest {
@Test
public void testInitiateWithPostAndMultipleTypeFilters() throws IOException {
// when
when(myJobRunner.startNewJob(any())).thenReturn(createJobStartResponse());
when(myJobRunner.startNewJob(isNotNull(), any())).thenReturn(createJobStartResponse());
Parameters input = new Parameters();
input.addParameter(JpaConstants.PARAM_EXPORT_OUTPUT_FORMAT, new StringType(Constants.CT_FHIR_NDJSON));
@ -771,7 +772,7 @@ public class BulkDataExportProviderTest {
@Test
public void testInitiateBulkExportOnPatient_noTypeParam_addsTypeBeforeBulkExport() throws IOException {
// when
when(myJobRunner.startNewJob(any()))
when(myJobRunner.startNewJob(isNotNull(), any()))
.thenReturn(createJobStartResponse());
Parameters input = new Parameters();
@ -797,7 +798,7 @@ public class BulkDataExportProviderTest {
@Test
public void testInitiatePatientExportRequest() throws IOException {
// when
when(myJobRunner.startNewJob(any()))
when(myJobRunner.startNewJob(isNotNull(), any()))
.thenReturn(createJobStartResponse());
InstantType now = InstantType.now();
@ -836,7 +837,7 @@ public class BulkDataExportProviderTest {
startResponse.setUsesCachedResult(true);
// when
when(myJobRunner.startNewJob(any(Batch2BaseJobParameters.class)))
when(myJobRunner.startNewJob(isNotNull(), any(Batch2BaseJobParameters.class)))
.thenReturn(startResponse);
Parameters input = new Parameters();
@ -870,7 +871,7 @@ public class BulkDataExportProviderTest {
myStorageSettings.setEnableBulkExportJobReuse(false);
// when
when(myJobRunner.startNewJob(any(Batch2BaseJobParameters.class)))
when(myJobRunner.startNewJob(isNotNull(), any(Batch2BaseJobParameters.class)))
.thenReturn(startResponse);
Parameters input = new Parameters();
@ -900,7 +901,7 @@ public class BulkDataExportProviderTest {
Batch2JobStartResponse startResponse = createJobStartResponse();
startResponse.setUsesCachedResult(true);
startResponse.setInstanceId(A_JOB_ID);
when(myJobRunner.startNewJob(any(Batch2BaseJobParameters.class)))
when(myJobRunner.startNewJob(isNotNull(), any(Batch2BaseJobParameters.class)))
.thenReturn(startResponse);
// when
@ -994,7 +995,7 @@ public class BulkDataExportProviderTest {
@Test
public void testGetBulkExport_outputFormat_FhirNdJson_inHeader() throws IOException {
// when
when(myJobRunner.startNewJob(any()))
when(myJobRunner.startNewJob(isNotNull(), any()))
.thenReturn(createJobStartResponse());
// call
@ -1017,7 +1018,7 @@ public class BulkDataExportProviderTest {
@Test
public void testGetBulkExport_outputFormat_FhirNdJson_inUrl() throws IOException {
// when
when(myJobRunner.startNewJob(any()))
when(myJobRunner.startNewJob(isNotNull(), any()))
.thenReturn(createJobStartResponse());
// call

View File

@ -711,7 +711,7 @@ public class BulkDataExportTest extends BaseResourceProviderR4Test {
}
private JobInstance verifyBulkExportResults(BulkDataExportOptions theOptions, List<String> theContainedList, List<String> theExcludedList) {
Batch2JobStartResponse startResponse = myJobRunner.startNewJob(BulkExportUtils.createBulkExportJobParametersFromExportOptions(theOptions));
Batch2JobStartResponse startResponse = myJobRunner.startNewJob(mySrd, BulkExportUtils.createBulkExportJobParametersFromExportOptions(theOptions));
assertNotNull(startResponse);
assertFalse(startResponse.isUsesCachedResult());
@ -781,7 +781,7 @@ public class BulkDataExportTest extends BaseResourceProviderR4Test {
// Test
try {
myJobRunner.startNewJob(BulkExportUtils.createBulkExportJobParametersFromExportOptions(options));
myJobRunner.startNewJob(mySrd, BulkExportUtils.createBulkExportJobParametersFromExportOptions(options));
fail();
} catch (InvalidRequestException e) {
@ -800,7 +800,7 @@ public class BulkDataExportTest extends BaseResourceProviderR4Test {
// Test
try {
myJobRunner.startNewJob(BulkExportUtils.createBulkExportJobParametersFromExportOptions(options));
myJobRunner.startNewJob(mySrd, BulkExportUtils.createBulkExportJobParametersFromExportOptions(options));
fail();
} catch (InvalidRequestException e) {
@ -819,7 +819,7 @@ public class BulkDataExportTest extends BaseResourceProviderR4Test {
// Test
try {
myJobRunner.startNewJob(BulkExportUtils.createBulkExportJobParametersFromExportOptions(options));
myJobRunner.startNewJob(mySrd, BulkExportUtils.createBulkExportJobParametersFromExportOptions(options));
fail();
} catch (InvalidRequestException e) {
@ -838,7 +838,7 @@ public class BulkDataExportTest extends BaseResourceProviderR4Test {
// Test
try {
myJobRunner.startNewJob(BulkExportUtils.createBulkExportJobParametersFromExportOptions(options));
myJobRunner.startNewJob(mySrd, BulkExportUtils.createBulkExportJobParametersFromExportOptions(options));
fail();
} catch (InvalidRequestException e) {
@ -857,7 +857,7 @@ public class BulkDataExportTest extends BaseResourceProviderR4Test {
// Test
try {
myJobRunner.startNewJob(BulkExportUtils.createBulkExportJobParametersFromExportOptions(options));
myJobRunner.startNewJob(mySrd, BulkExportUtils.createBulkExportJobParametersFromExportOptions(options));
fail();
} catch (InvalidRequestException e) {

View File

@ -447,7 +447,7 @@ public class BulkExportUseCaseTest extends BaseResourceProviderR4Test {
myCaptureQueriesListener.clear();
Batch2JobStartResponse startResponse = myJobRunner.startNewJob(BulkExportUtils.createBulkExportJobParametersFromExportOptions(options));
Batch2JobStartResponse startResponse = myJobRunner.startNewJob(mySrd, BulkExportUtils.createBulkExportJobParametersFromExportOptions(options));
assertNotNull(startResponse);
@ -567,7 +567,7 @@ public class BulkExportUseCaseTest extends BaseResourceProviderR4Test {
options.setExportStyle(BulkDataExportOptions.ExportStyle.PATIENT);
options.setOutputFormat(Constants.CT_FHIR_NDJSON);
Batch2JobStartResponse job = myJobRunner.startNewJob(BulkExportUtils.createBulkExportJobParametersFromExportOptions(options));
Batch2JobStartResponse job = myJobRunner.startNewJob(mySrd, BulkExportUtils.createBulkExportJobParametersFromExportOptions(options));
myBatch2JobHelper.awaitJobCompletion(job.getInstanceId(), 60);
ourLog.debug("Job status after awaiting - {}", myJobRunner.getJobInfo(job.getInstanceId()).getStatus());
await()
@ -1468,7 +1468,7 @@ public class BulkExportUseCaseTest extends BaseResourceProviderR4Test {
options.setExportStyle(BulkDataExportOptions.ExportStyle.GROUP);
options.setOutputFormat(Constants.CT_FHIR_NDJSON);
Batch2JobStartResponse startResponse = myJobRunner.startNewJob(BulkExportUtils.createBulkExportJobParametersFromExportOptions(options));
Batch2JobStartResponse startResponse = myJobRunner.startNewJob(mySrd, BulkExportUtils.createBulkExportJobParametersFromExportOptions(options));
assertNotNull(startResponse);

View File

@ -44,6 +44,7 @@ import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.isNotNull;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@ -207,7 +208,7 @@ class BaseHapiFhirResourceDaoTest {
mySvc.requestReindexForRelatedResources(false, base, new ServletRequestDetails());
ArgumentCaptor<JobInstanceStartRequest> requestCaptor = ArgumentCaptor.forClass(JobInstanceStartRequest.class);
Mockito.verify(myJobCoordinator).startInstance(requestCaptor.capture());
Mockito.verify(myJobCoordinator).startInstance(isNotNull(), requestCaptor.capture());
JobInstanceStartRequest actualRequest = requestCaptor.getValue();
assertNotNull(actualRequest);
@ -228,7 +229,7 @@ class BaseHapiFhirResourceDaoTest {
mySvc.requestReindexForRelatedResources(false, base, new ServletRequestDetails());
ArgumentCaptor<JobInstanceStartRequest> requestCaptor = ArgumentCaptor.forClass(JobInstanceStartRequest.class);
Mockito.verify(myJobCoordinator).startInstance(requestCaptor.capture());
Mockito.verify(myJobCoordinator).startInstance(isNotNull(), requestCaptor.capture());
JobInstanceStartRequest actualRequest = requestCaptor.getValue();
assertNotNull(actualRequest);

View File

@ -6,9 +6,10 @@ import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.util.BundleBuilder;
import org.hl7.fhir.instance.model.api.IIdType;
@ -20,9 +21,9 @@ import org.hl7.fhir.r4.model.Reference;
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 java.util.List;
import java.util.Map;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
@ -52,39 +53,73 @@ class DeleteExpungeDaoTest extends BaseJpaR4Test {
}
@Test
public void testDeleteCascadeExpungeReturns400() {
// Create new organization
Organization organization = new Organization();
organization.setName("FOO");
IIdType organizationId = myOrganizationDao.create(organization).getId().toUnqualifiedVersionless();
public void testCascade_MultiLevel_Success() {
// Setup
Patient patient = new Patient();
patient.setManagingOrganization(new Reference(organizationId));
IIdType patientId = myPatientDao.create(patient).getId().toUnqualifiedVersionless();
// Create a chain of dependent references
IIdType p1 = createPatient(withActiveTrue());
IIdType o1 = createObservation(withSubject(p1));
IIdType o1b = createObservation(withReference("hasMember", o1));
IIdType o1c = createObservation(withReference("hasMember", o1b));
// Try to delete _cascade and _expunge on the organization
BaseServerResponseException e = assertThrows(BaseServerResponseException.class, () -> {
myOrganizationDao
.deleteByUrl("Organization?" + "_cascade=delete&" + JpaConstants.PARAM_DELETE_EXPUNGE + "=true", mySrd);
});
// validate precondition
assertEquals(1, myPatientDao.search(SearchParameterMap.newSynchronous()).size());
assertEquals(3, myObservationDao.search(SearchParameterMap.newSynchronous()).size());
// Get not implemented HTTP 400 error
assertEquals(Constants.STATUS_HTTP_400_BAD_REQUEST, e.getStatusCode());
assertEquals(Msg.code(964) + "_expunge cannot be used with _cascade", e.getMessage());
// execute
String url = "Patient?" +
JpaConstants.PARAM_DELETE_EXPUNGE + "=true";
when(mySrd.getParameters()).thenReturn(Map.of(
Constants.PARAMETER_CASCADE_DELETE, new String[]{Constants.CASCADE_DELETE},
JpaConstants.PARAM_DELETE_EXPUNGE, new String[]{"true"},
Constants.PARAMETER_CASCADE_DELETE_MAX_ROUNDS, new String[]{"10"}
));
DeleteMethodOutcome outcome = myOrganizationDao.deleteByUrl(url, mySrd);
String jobId = jobExecutionIdFromOutcome(outcome);
JobInstance job = myBatch2JobHelper.awaitJobCompletion(jobId);
// Try to delete with header 'X-Cascade' = delete
when(mySrd.getHeader(Constants.HEADER_CASCADE)).thenReturn(Constants.CASCADE_DELETE);
e = assertThrows(BaseServerResponseException.class, () -> {
myOrganizationDao
.deleteByUrl("Organization?" + JpaConstants.PARAM_DELETE_EXPUNGE + "=true", mySrd);
});
// Get not implemented HTTP 400 error
assertEquals(Constants.STATUS_HTTP_400_BAD_REQUEST, e.getStatusCode());
assertEquals(Msg.code(964) + "_expunge cannot be used with _cascade", e.getMessage());
// Validate
assertEquals(4, job.getCombinedRecordsProcessed());
assertDoesntExist(p1);
assertDoesntExist(o1);
assertDoesntExist(o1b);
assertDoesntExist(o1c);
}
@Test
public void testCascade_MultiLevel_NotEnoughRounds() {
// Setup
// Create a chain of dependent references
IIdType p1 = createPatient(withActiveTrue());
IIdType o1 = createObservation(withSubject(p1));
IIdType o1b = createObservation(withReference("hasMember", o1));
IIdType o1c = createObservation(withReference("hasMember", o1b));
// validate precondition
assertEquals(1, myPatientDao.search(SearchParameterMap.newSynchronous()).size());
assertEquals(3, myObservationDao.search(SearchParameterMap.newSynchronous()).size());
String url = "Patient?" +
JpaConstants.PARAM_DELETE_EXPUNGE + "=true";
when(mySrd.getParameters()).thenReturn(Map.of(
Constants.PARAMETER_CASCADE_DELETE, new String[]{Constants.CASCADE_DELETE},
JpaConstants.PARAM_DELETE_EXPUNGE, new String[]{"true"},
Constants.PARAMETER_CASCADE_DELETE_MAX_ROUNDS, new String[]{"2"}
));
DeleteMethodOutcome outcome = myOrganizationDao.deleteByUrl(url, mySrd);
String jobId = jobExecutionIdFromOutcome(outcome);
JobInstance job = myBatch2JobHelper.awaitJobFailure(jobId);
// Validate
assertThat(job.getErrorMessage(), containsString("Unable to delete"));
assertNotGone(p1);
assertNotGone(o1);
assertNotGone(o1b);
assertNotGone(o1c);
}
@Test
public void testDeleteExpungeThrowExceptionIfForeignKeyLinksExists() {
// setup

View File

@ -86,7 +86,6 @@ import org.springframework.data.domain.Slice;
import org.springframework.util.comparator.ComparableComparator;
import javax.annotation.Nonnull;
import javax.persistence.Id;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
@ -841,7 +840,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
// Test
myCaptureQueriesListener.clear();
RunOutcome outcome = myDeleteExpungeStep.doDeleteExpunge(new ResourceIdListWorkChunkJson(pids), sink, "instance-id", "chunk-id");
RunOutcome outcome = myDeleteExpungeStep.doDeleteExpunge(new ResourceIdListWorkChunkJson(pids, null), sink, "instance-id", "chunk-id", false, null);
// Verify
assertEquals(1, myCaptureQueriesListener.countSelectQueriesForCurrentThread());

View File

@ -178,30 +178,6 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test {
myStorageSettings.setMassIngestionMode(false);
}
private void assertGone(IIdType theId) {
try {
assertNotGone(theId);
fail();
} catch (ResourceGoneException e) {
// good
}
}
/**
* This gets called from assertGone too! Careful about exceptions...
*/
private void assertNotGone(IIdType theId) {
if ("Patient".equals(theId.getResourceType())) {
myPatientDao.read(theId, mySrd);
} else if ("Organization".equals(theId.getResourceType())) {
myOrganizationDao.read(theId, mySrd);
} else if ("CodeSystem".equals(theId.getResourceType())) {
myCodeSystemDao.read(theId, mySrd);
} else {
fail("Can't handle type: " + theId.getResourceType());
}
}
@BeforeEach
public void beforeDisableResultReuse() {
myStorageSettings.setReuseCachedSearchResultsForMillis(null);

View File

@ -67,9 +67,17 @@ public class JpaHistoryR4Test extends BaseJpaR4SystemTest {
@Test
public void testTypeHistory_CountAccurate() {
runInTransaction(()->{
assertEquals(0, myResourceHistoryTableDao.count());
});
myStorageSettings.setHistoryCountMode(HistoryCountModeEnum.COUNT_ACCURATE);
create20Patients();
runInTransaction(()->{
assertEquals(20, myResourceHistoryTableDao.count());
});
/*
* Perform initial history
*/

View File

@ -1,5 +1,10 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.batch2.api.IJobCoordinator;
import ca.uhn.fhir.batch2.jobs.expunge.DeleteExpungeAppCtx;
import ca.uhn.fhir.batch2.jobs.expunge.DeleteExpungeJobParameters;
import ca.uhn.fhir.batch2.model.JobInstance;
import ca.uhn.fhir.batch2.model.JobInstanceStartRequest;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.api.HookParams;
@ -7,6 +12,7 @@ import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.ForcedId;
@ -67,6 +73,7 @@ import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException;
import java.time.LocalDate;
@ -104,6 +111,9 @@ import static org.mockito.Mockito.verify;
public class PartitioningSqlR4Test extends BasePartitioningR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(PartitioningSqlR4Test.class);
@Autowired
private IJobCoordinator myJobCoordinator;
@BeforeEach
public void disableAdvanceIndexing() {
myStorageSettings.setAdvancedHSearchIndexing(false);
@ -679,6 +689,56 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
assertPersistedPartitionIdMatches(patientId);
}
@Test
public void testDeleteExpunge_Cascade() {
myPartitionSettings.setPartitioningEnabled(true);
addCreatePartition(myPartitionId, myPartitionDate);
addCreatePartition(myPartitionId, myPartitionDate);
IIdType p1 = createPatient(withActiveTrue());
IIdType o1 = createObservation(withSubject(p1));
addCreatePartition(myPartitionId2, myPartitionDate);
addCreatePartition(myPartitionId2, myPartitionDate);
IIdType p2 = createPatient(withActiveTrue());
IIdType o2 = createObservation(withSubject(p2));
// validate precondition
addReadAllPartitions();
addReadAllPartitions();
assertEquals(2, myPatientDao.search(SearchParameterMap.newSynchronous(), mySrd).size());
assertEquals(2, myObservationDao.search(SearchParameterMap.newSynchronous(), mySrd).size());
addReadPartition(myPartitionId);
addReadPartition(myPartitionId);
assertEquals(1, myPatientDao.search(SearchParameterMap.newSynchronous(), mySrd).size());
assertEquals(1, myObservationDao.search(SearchParameterMap.newSynchronous(), mySrd).size());
DeleteExpungeJobParameters jobParameters = new DeleteExpungeJobParameters();
jobParameters.addUrl("Patient?_id=" + p1.getIdPart() + "," + p2.getIdPart());
jobParameters.setRequestPartitionId(RequestPartitionId.fromPartitionId(myPartitionId));
jobParameters.setCascade(true);
JobInstanceStartRequest startRequest = new JobInstanceStartRequest();
startRequest.setParameters(jobParameters);
startRequest.setJobDefinitionId(DeleteExpungeAppCtx.JOB_DELETE_EXPUNGE);
// execute
Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(startRequest);
// Validate
JobInstance outcome = myBatch2JobHelper.awaitJobCompletion(startResponse);
assertEquals(2, outcome.getCombinedRecordsProcessed());
addReadAllPartitions();
assertDoesntExist(p1);
addReadAllPartitions();
assertDoesntExist(o1);
addReadAllPartitions();
assertNotGone(p2);
addReadAllPartitions();
assertNotGone(o2);
}
private void assertPersistedPartitionIdMatches(Long patientId) {
runInTransaction(() -> {
// HFJ_RESOURCE

View File

@ -10,6 +10,7 @@ import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
import ca.uhn.fhir.jpa.test.Batch2JobHelper;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.DiagnosticReport;
import org.hl7.fhir.r4.model.Observation;
@ -18,7 +19,10 @@ import org.hl7.fhir.r4.model.Reference;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
public class DeleteExpungeJobTest extends BaseJpaR4Test {
@Autowired
@ -27,7 +31,7 @@ public class DeleteExpungeJobTest extends BaseJpaR4Test {
private Batch2JobHelper myBatch2JobHelper;
@Test
public void testDeleteExpunge() throws Exception {
public void testDeleteExpunge() {
// setup
Patient patientActive = new Patient();
patientActive.setActive(true);
@ -80,6 +84,159 @@ public class DeleteExpungeJobTest extends BaseJpaR4Test {
assertDocumentCountMatchesResourceCount(myPatientDao);
}
@Test
public void testCascade_FailIfNotEnabled() {
IIdType p1 = createPatient(withActiveTrue());
IIdType o1 = createObservation(withSubject(p1));
IIdType p2 = createPatient(withActiveTrue());
IIdType o2 = createObservation(withSubject(p2));
// validate precondition
assertEquals(2, myPatientDao.search(SearchParameterMap.newSynchronous()).size());
assertEquals(2, myObservationDao.search(SearchParameterMap.newSynchronous()).size());
DeleteExpungeJobParameters jobParameters = new DeleteExpungeJobParameters();
jobParameters.addUrl("Patient?_id=" + p1.getIdPart());
JobInstanceStartRequest startRequest = new JobInstanceStartRequest();
startRequest.setParameters(jobParameters);
startRequest.setJobDefinitionId(DeleteExpungeAppCtx.JOB_DELETE_EXPUNGE);
// execute
Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(startRequest);
// Validate
JobInstance failure = myBatch2JobHelper.awaitJobFailure(startResponse);
assertThat(failure.getErrorMessage(), containsString("Unable to delete " + p1.getValue() + " because " + o1.getValue() + " refers to it"));
}
@Test
public void testCascade() {
// Setup
IIdType p1 = createPatient(withActiveTrue());
IIdType o1 = createObservation(withSubject(p1));
IIdType p2 = createPatient(withActiveTrue());
IIdType o2 = createObservation(withSubject(p2));
// validate precondition
assertEquals(2, myPatientDao.search(SearchParameterMap.newSynchronous()).size());
assertEquals(2, myObservationDao.search(SearchParameterMap.newSynchronous()).size());
DeleteExpungeJobParameters jobParameters = new DeleteExpungeJobParameters();
jobParameters.addUrl("Patient?_id=" + p1.getIdPart());
jobParameters.setCascade(true);
JobInstanceStartRequest startRequest = new JobInstanceStartRequest();
startRequest.setParameters(jobParameters);
startRequest.setJobDefinitionId(DeleteExpungeAppCtx.JOB_DELETE_EXPUNGE);
// execute
Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(startRequest);
// Validate
JobInstance outcome = myBatch2JobHelper.awaitJobCompletion(startResponse);
assertEquals(2, outcome.getCombinedRecordsProcessed());
assertDoesntExist(p1);
assertDoesntExist(o1);
assertNotGone(p2);
assertNotGone(o2);
}
@Test
public void testCascade_MultiLevel_Success() {
// Setup
// Create a chain of dependent references
IIdType p1 = createPatient(withActiveTrue());
IIdType o1 = createObservation(withSubject(p1));
IIdType o1b = createObservation(withReference("hasMember", o1));
IIdType o1c = createObservation(withReference("hasMember", o1b));
// validate precondition
assertEquals(1, myPatientDao.search(SearchParameterMap.newSynchronous()).size());
assertEquals(3, myObservationDao.search(SearchParameterMap.newSynchronous()).size());
DeleteExpungeJobParameters jobParameters = new DeleteExpungeJobParameters();
jobParameters.addUrl("Patient?_id=" + p1.getIdPart());
jobParameters.setCascade(true);
jobParameters.setCascadeMaxRounds(4);
JobInstanceStartRequest startRequest = new JobInstanceStartRequest();
startRequest.setParameters(jobParameters);
startRequest.setJobDefinitionId(DeleteExpungeAppCtx.JOB_DELETE_EXPUNGE);
// execute
Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(startRequest);
// Validate
JobInstance outcome = myBatch2JobHelper.awaitJobCompletion(startResponse);
assertEquals(4, outcome.getCombinedRecordsProcessed());
assertDoesntExist(p1);
assertDoesntExist(o1);
assertDoesntExist(o1b);
assertDoesntExist(o1c);
}
@Test
public void testCascade_MultiLevel_NotEnoughRounds() {
// Setup
// Create a chain of dependent references
IIdType p1 = createPatient(withActiveTrue());
IIdType o1 = createObservation(withSubject(p1));
IIdType o1b = createObservation(withReference("hasMember", o1));
IIdType o1c = createObservation(withReference("hasMember", o1b));
// validate precondition
assertEquals(1, myPatientDao.search(SearchParameterMap.newSynchronous()).size());
assertEquals(3, myObservationDao.search(SearchParameterMap.newSynchronous()).size());
DeleteExpungeJobParameters jobParameters = new DeleteExpungeJobParameters();
jobParameters.addUrl("Patient?_id=" + p1.getIdPart());
jobParameters.setCascade(true);
jobParameters.setCascadeMaxRounds(1);
JobInstanceStartRequest startRequest = new JobInstanceStartRequest();
startRequest.setParameters(jobParameters);
startRequest.setJobDefinitionId(DeleteExpungeAppCtx.JOB_DELETE_EXPUNGE);
// execute
Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(startRequest);
// Validate
JobInstance outcome = myBatch2JobHelper.awaitJobFailure(startResponse);
assertThat(outcome.getErrorMessage(), containsString("refers to it via the path"));
assertNotGone(p1);
assertNotGone(o1);
assertNotGone(o1b);
assertNotGone(o1c);
}
@Test
public void testInvalidParams_NoSearchParams() {
// Setup
DeleteExpungeJobParameters jobParameters = new DeleteExpungeJobParameters();
jobParameters.addUrl("Patient/123");
JobInstanceStartRequest startRequest = new JobInstanceStartRequest();
startRequest.setParameters(jobParameters);
startRequest.setJobDefinitionId(DeleteExpungeAppCtx.JOB_DELETE_EXPUNGE);
// execute
try {
myJobCoordinator.startInstance(startRequest);
fail();
} catch (InvalidRequestException e) {
// validate
assertThat(e.getMessage(), containsString("Delete expunge URLs must be in the format"));
}
}
public void assertDocumentCountMatchesResourceCount(IFhirResourceDao dao) {
String resourceType = myFhirContext.getResourceType(dao.getResourceType());
long resourceCount = dao.search(new SearchParameterMap().setLoadSynchronous(true)).size();

View File

@ -204,7 +204,7 @@ public class ResponseTerminologyTranslationInterceptorTest extends BaseResourceP
options.setExportStyle(BulkDataExportOptions.ExportStyle.SYSTEM);
options.setOutputFormat(Constants.CT_FHIR_NDJSON);
Batch2JobStartResponse startResponse = myJobRunner.startNewJob(BulkExportUtils.createBulkExportJobParametersFromExportOptions(options));
Batch2JobStartResponse startResponse = myJobRunner.startNewJob(mySrd, BulkExportUtils.createBulkExportJobParametersFromExportOptions(options));
assertNotNull(startResponse);

View File

@ -117,7 +117,9 @@ public class NpmR4Test extends BaseJpaR4Test {
int port = JettyUtil.getPortForStartedServer(myServer);
jpaPackageCache.getPackageServers().clear();
jpaPackageCache.addPackageServer(new PackageServer("http://localhost:" + port));
String url = "http://localhost:" + port;
ourLog.info("Package server is at base: {}", url);
jpaPackageCache.addPackageServer(new PackageServer(url));
myFakeNpmServlet.responses.clear();
}

View File

@ -119,15 +119,6 @@ public class ExpungeR4Test extends BaseResourceProviderR4Test {
}
}
private void assertGone(IIdType theId) {
try {
getDao(theId).read(theId);
fail();
} catch (ResourceGoneException e) {
// good
}
}
private void assertStillThere(IIdType theId) {
getDao(theId).read(theId);
}

View File

@ -108,7 +108,7 @@ public class MultitenantBatchOperationR4Test extends BaseMultitenantResourceProv
String jobId = BatchHelperR4.jobIdFromBatch2Parameters(response);
myBatch2JobHelper.awaitJobCompletion(jobId);
assertThat(interceptor.requestPartitionIds, hasSize(3));
assertThat(interceptor.requestPartitionIds, hasSize(5));
RequestPartitionId partitionId = interceptor.requestPartitionIds.get(0);
assertEquals(TENANT_B_ID, partitionId.getFirstPartitionIdOrNull());
assertEquals(TENANT_B, partitionId.getFirstPartitionNameOrNull());

View File

@ -69,6 +69,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.isNotNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
@ -672,7 +673,7 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
Batch2JobStartResponse startResponse = new Batch2JobStartResponse();
startResponse.setInstanceId(jobId);
when(myJobRunner.startNewJob(any()))
when(myJobRunner.startNewJob(isNotNull(), any()))
.thenReturn(startResponse);
when(myJobRunner.getJobInfo(anyString()))
.thenReturn(jobInfo);

View File

@ -48,17 +48,8 @@ public class ResourceProviderExpungeR4Test extends BaseResourceProviderR4Test {
}
}
private void assertGone(IIdType theId) {
try {
getDao(theId).read(theId);
fail();
} catch (ResourceGoneException e) {
// good
}
}
private void assertStillThere(IIdType theId) {
getDao(theId).read(theId);
assertNotGone(theId);
}
@Override

View File

@ -72,6 +72,7 @@ import org.hl7.fhir.r4.model.DecimalType;
import org.hl7.fhir.r4.model.DiagnosticReport;
import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.IntegerType;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.OperationDefinition;
import org.hl7.fhir.r4.model.OperationOutcome;
@ -956,7 +957,7 @@ public class SystemProviderR4Test extends BaseJpaR4Test {
input.addParameter(ProviderConstants.OPERATION_DELETE_EXPUNGE_URL, "Observation?subject.active=false");
input.addParameter(ProviderConstants.OPERATION_DELETE_EXPUNGE_URL, "DiagnosticReport?subject.active=false");
int batchSize = 2;
input.addParameter(ProviderConstants.OPERATION_DELETE_BATCH_SIZE, new DecimalType(batchSize));
input.addParameter(ProviderConstants.OPERATION_DELETE_BATCH_SIZE, new IntegerType(batchSize));
// execute

View File

@ -36,16 +36,6 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<!--<useManifestOnlyJar>false</useManifestOnlyJar>-->
<forkCount>1</forkCount>
<reuseForks>false</reuseForks>
<runOrder>alphabetical</runOrder>
<includes>
<include>**/*IT.java</include>
</includes>
<useModulePath>false</useModulePath>
</configuration>
<executions>
<execution>
<goals>
@ -66,11 +56,7 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<runOrder>alphabetical</runOrder>
<argLine>@{argLine} ${surefire_jvm_args}</argLine>
<forkCount>0.6C</forkCount>
<excludes>*StressTest*</excludes>
<skip>${skipFailsafe}</skip>
</configuration>
</plugin>
<plugin>
@ -100,20 +86,6 @@
</plugins>
</build>
</profile>
<profile>
<id>CI</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<runOrder>alphabetical</runOrder>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@ -41,16 +41,6 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<!--<useManifestOnlyJar>false</useManifestOnlyJar>-->
<forkCount>1</forkCount>
<reuseForks>false</reuseForks>
<runOrder>alphabetical</runOrder>
<includes>
<include>**/*IT.java</include>
</includes>
<useModulePath>false</useModulePath>
</configuration>
<executions>
<execution>
<goals>
@ -71,11 +61,7 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<runOrder>alphabetical</runOrder>
<argLine>@{argLine} ${surefire_jvm_args}</argLine>
<forkCount>0.6C</forkCount>
<excludes>*StressTest*</excludes>
<skip>${skipFailsafe}</skip>
</configuration>
</plugin>
<plugin>
@ -105,20 +91,6 @@
</plugins>
</build>
</profile>
<profile>
<id>CI</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<runOrder>alphabetical</runOrder>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@ -262,16 +262,6 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<!--<useManifestOnlyJar>false</useManifestOnlyJar>-->
<forkCount>1</forkCount>
<reuseForks>false</reuseForks>
<runOrder>alphabetical</runOrder>
<includes>
<include>**/*IT.java</include>
</includes>
<useModulePath>false</useModulePath>
</configuration>
<executions>
<execution>
<goals>
@ -326,20 +316,6 @@
</plugins>
</build>
</profile>
<profile>
<id>CI</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<runOrder>alphabetical</runOrder>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@ -26,6 +26,8 @@ import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.interceptor.executor.InterceptorService;
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.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc;
@ -34,41 +36,9 @@ import ca.uhn.fhir.jpa.config.JpaConfig;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc;
import ca.uhn.fhir.jpa.dao.JpaPersistedResourceValidationSupport;
import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedComboTokensNonUniqueDao;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamCoordsDao;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamDateDao;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamNumberDao;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamStringDao;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamTokenDao;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamUriDao;
import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao;
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
import ca.uhn.fhir.jpa.dao.data.IResourceTagDao;
import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemDao;
import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemVersionDao;
import ca.uhn.fhir.jpa.dao.data.ITermConceptDao;
import ca.uhn.fhir.jpa.dao.data.ITermConceptDesignationDao;
import ca.uhn.fhir.jpa.dao.data.ITermConceptPropertyDao;
import ca.uhn.fhir.jpa.dao.data.ITermValueSetConceptDao;
import ca.uhn.fhir.jpa.dao.data.ITermValueSetDao;
import ca.uhn.fhir.jpa.entity.TermConcept;
import ca.uhn.fhir.jpa.entity.TermConceptDesignation;
import ca.uhn.fhir.jpa.entity.TermConceptProperty;
import ca.uhn.fhir.jpa.entity.TermValueSet;
import ca.uhn.fhir.jpa.entity.TermValueSetConcept;
import ca.uhn.fhir.jpa.entity.TermValueSetConceptDesignation;
import ca.uhn.fhir.jpa.model.entity.ForcedId;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboTokenNonUnique;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamCoords;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri;
import ca.uhn.fhir.jpa.model.entity.ResourceLink;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.dao.data.*;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.jpa.model.entity.*;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
@ -82,6 +52,7 @@ import ca.uhn.fhir.jpa.util.MemoryCacheService;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
@ -91,11 +62,7 @@ import ca.uhn.fhir.test.utilities.LoggingExtension;
import ca.uhn.fhir.test.utilities.ProxyUtil;
import ca.uhn.fhir.test.utilities.UnregisterScheduledProcessor;
import ca.uhn.fhir.test.utilities.server.SpringContextGrabbingTestExecutionListener;
import ca.uhn.fhir.util.BundleUtil;
import ca.uhn.fhir.util.ClasspathUtil;
import ca.uhn.fhir.util.FhirVersionIndependentConcept;
import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.util.*;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
@ -131,13 +98,7 @@ import javax.persistence.EntityManager;
import java.io.IOException;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
@ -147,9 +108,7 @@ import java.util.stream.Stream;
import static ca.uhn.fhir.util.TestUtil.doRandomizeLocaleAndTimezone;
import static java.util.stream.Collectors.joining;
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
@ -253,8 +212,104 @@ public abstract class BaseJpaTest extends BaseTest {
private IResourceHistoryTableDao myResourceHistoryTableDao;
@Autowired
private IForcedIdDao myForcedIdDao;
@Autowired
private DaoRegistry myDaoRegistry;
private List<Object> myRegisteredInterceptors = new ArrayList<>(1);
@SuppressWarnings("BusyWait")
public static void waitForSize(int theTarget, List<?> theList) {
StopWatch sw = new StopWatch();
while (theList.size() != theTarget && sw.getMillis() <= 16000) {
try {
Thread.sleep(50);
} catch (InterruptedException theE) {
throw new Error(theE);
}
}
if (sw.getMillis() >= 16000 || theList.size() > theTarget) {
String describeResults = theList
.stream()
.map(t -> {
if (t == null) {
return "null";
}
if (t instanceof IBaseResource) {
return ((IBaseResource) t).getIdElement().getValue();
}
return t.toString();
})
.collect(Collectors.joining(", "));
fail("Size " + theList.size() + " is != target " + theTarget + " - Got: " + describeResults);
}
}
@BeforeAll
public static void beforeClassRandomizeLocale() {
doRandomizeLocaleAndTimezone();
}
@SuppressWarnings("BusyWait")
protected static void purgeDatabase(JpaStorageSettings theStorageSettings, IFhirSystemDao<?, ?> theSystemDao, IResourceReindexingSvc theResourceReindexingSvc, ISearchCoordinatorSvc theSearchCoordinatorSvc, ISearchParamRegistry theSearchParamRegistry, IBulkDataExportJobSchedulingHelper theBulkDataJobActivator) {
theSearchCoordinatorSvc.cancelAllActiveSearches();
theResourceReindexingSvc.cancelAndPurgeAllJobs();
theBulkDataJobActivator.cancelAndPurgeAllJobs();
boolean expungeEnabled = theStorageSettings.isExpungeEnabled();
boolean multiDeleteEnabled = theStorageSettings.isAllowMultipleDelete();
theStorageSettings.setExpungeEnabled(true);
theStorageSettings.setAllowMultipleDelete(true);
for (int count = 0; ; count++) {
try {
theSystemDao.expunge(new ExpungeOptions().setExpungeEverything(true), new SystemRequestDetails());
break;
} catch (Exception e) {
if (count >= 3) {
ourLog.error("Failed during expunge", e);
fail(e.toString());
} else {
try {
Thread.sleep(1000);
} catch (InterruptedException e2) {
fail(e2.toString());
}
}
}
}
theStorageSettings.setExpungeEnabled(expungeEnabled);
theStorageSettings.setAllowMultipleDelete(multiDeleteEnabled);
theSearchParamRegistry.forceRefresh();
}
protected static Set<String> toCodes(Set<TermConcept> theConcepts) {
HashSet<String> retVal = new HashSet<>();
for (TermConcept next : theConcepts) {
retVal.add(next.getCode());
}
return retVal;
}
protected static Set<String> toCodes(List<FhirVersionIndependentConcept> theConcepts) {
HashSet<String> retVal = new HashSet<>();
for (FhirVersionIndependentConcept next : theConcepts) {
retVal.add(next.getCode());
}
return retVal;
}
public static void waitForSize(int theTarget, Callable<Number> theCallable, Callable<String> theFailureMessage) throws Exception {
waitForSize(theTarget, 10000, theCallable, theFailureMessage);
}
@SuppressWarnings("BusyWait")
public static void waitForSize(int theTarget, int theTimeoutMillis, Callable<Number> theCallable, Callable<String> theFailureMessage) throws Exception {
await()
.alias("Waiting for size " + theTarget + ". Current size is " + theCallable.call().intValue() + ": " + theFailureMessage.call())
.atMost(Duration.of(theTimeoutMillis, ChronoUnit.MILLIS))
.until(() -> theCallable.call().intValue() == theTarget);
}
protected <T extends IBaseResource> T loadResourceFromClasspath(Class<T> type, String resourceName) throws IOException {
return ClasspathUtil.loadResource(myFhirContext, type, resourceName);
}
@ -360,7 +415,6 @@ public abstract class BaseJpaTest extends BaseTest {
});
}
protected void logAllResourceLinks() {
runInTransaction(() -> {
ourLog.info("Resource Links:\n * {}", myResourceLinkDao.findAll().stream().map(ResourceLink::toString).collect(Collectors.joining("\n * ")));
@ -751,97 +805,35 @@ public abstract class BaseJpaTest extends BaseTest {
myRegisteredInterceptors.clear();
}
@SuppressWarnings("BusyWait")
public static void waitForSize(int theTarget, List<?> theList) {
StopWatch sw = new StopWatch();
while (theList.size() != theTarget && sw.getMillis() <= 16000) {
try {
Thread.sleep(50);
} catch (InterruptedException theE) {
throw new Error(theE);
}
/**
* Asserts that the resource with {@literal theId} is deleted
*/
protected void assertGone(IIdType theId) {
IFhirResourceDao dao = myDaoRegistry.getResourceDao(theId.getResourceType());
IBaseResource result = dao.read(theId, mySrd, true);
assertTrue(result.isDeleted());
}
/**
* Asserts that the resource with {@literal theId} exists and is not deleted
*/
protected void assertNotGone(IIdType theId) {
IFhirResourceDao dao = myDaoRegistry.getResourceDao(theId.getResourceType());
assertNotNull(dao.read(theId, mySrd));
}
/**
* Asserts that the resource with {@literal theId} does not exist (i.e. not that
* it exists but that it was deleted, but rather that the ID doesn't exist at all).
* This can be used to test that a resource was expunged.
*/
protected void assertDoesntExist(IIdType theId) {
IFhirResourceDao dao = myDaoRegistry.getResourceDao(theId.getResourceType());
try {
dao.read(theId, mySrd);
fail();
} catch (ResourceNotFoundException e) {
// good
}
if (sw.getMillis() >= 16000 || theList.size() > theTarget) {
String describeResults = theList
.stream()
.map(t -> {
if (t == null) {
return "null";
}
if (t instanceof IBaseResource) {
return ((IBaseResource) t).getIdElement().getValue();
}
return t.toString();
})
.collect(Collectors.joining(", "));
fail("Size " + theList.size() + " is != target " + theTarget + " - Got: " + describeResults);
}
}
@BeforeAll
public static void beforeClassRandomizeLocale() {
doRandomizeLocaleAndTimezone();
}
@SuppressWarnings("BusyWait")
protected static void purgeDatabase(JpaStorageSettings theStorageSettings, IFhirSystemDao<?, ?> theSystemDao, IResourceReindexingSvc theResourceReindexingSvc, ISearchCoordinatorSvc theSearchCoordinatorSvc, ISearchParamRegistry theSearchParamRegistry, IBulkDataExportJobSchedulingHelper theBulkDataJobActivator) {
theSearchCoordinatorSvc.cancelAllActiveSearches();
theResourceReindexingSvc.cancelAndPurgeAllJobs();
theBulkDataJobActivator.cancelAndPurgeAllJobs();
boolean expungeEnabled = theStorageSettings.isExpungeEnabled();
boolean multiDeleteEnabled = theStorageSettings.isAllowMultipleDelete();
theStorageSettings.setExpungeEnabled(true);
theStorageSettings.setAllowMultipleDelete(true);
for (int count = 0; ; count++) {
try {
theSystemDao.expunge(new ExpungeOptions().setExpungeEverything(true), new SystemRequestDetails());
break;
} catch (Exception e) {
if (count >= 3) {
ourLog.error("Failed during expunge", e);
fail(e.toString());
} else {
try {
Thread.sleep(1000);
} catch (InterruptedException e2) {
fail(e2.toString());
}
}
}
}
theStorageSettings.setExpungeEnabled(expungeEnabled);
theStorageSettings.setAllowMultipleDelete(multiDeleteEnabled);
theSearchParamRegistry.forceRefresh();
}
protected static Set<String> toCodes(Set<TermConcept> theConcepts) {
HashSet<String> retVal = new HashSet<>();
for (TermConcept next : theConcepts) {
retVal.add(next.getCode());
}
return retVal;
}
protected static Set<String> toCodes(List<FhirVersionIndependentConcept> theConcepts) {
HashSet<String> retVal = new HashSet<>();
for (FhirVersionIndependentConcept next : theConcepts) {
retVal.add(next.getCode());
}
return retVal;
}
public static void waitForSize(int theTarget, Callable<Number> theCallable, Callable<String> theFailureMessage) throws Exception {
waitForSize(theTarget, 10000, theCallable, theFailureMessage);
}
@SuppressWarnings("BusyWait")
public static void waitForSize(int theTarget, int theTimeoutMillis, Callable<Number> theCallable, Callable<String> theFailureMessage) throws Exception {
await()
.alias("Waiting for size " + theTarget + ". Current size is " + theCallable.call().intValue() + ": " + theFailureMessage.call())
.atMost(Duration.of(theTimeoutMillis, ChronoUnit.MILLIS))
.until(() -> theCallable.call().intValue() == theTarget);
}
}

View File

@ -92,7 +92,21 @@ public class Batch2JobHelper {
try {
await()
.atMost(theSecondsToWait, TimeUnit.SECONDS)
.until(() -> checkStatusWithMaintenancePass(theBatchJobId, theExpectedStatus));
.until(() -> {
boolean inFinalStatus = false;
if (ArrayUtils.contains(theExpectedStatus, StatusEnum.COMPLETED) && !ArrayUtils.contains(theExpectedStatus, StatusEnum.FAILED)) {
inFinalStatus = hasStatus(theBatchJobId, StatusEnum.FAILED);
}
if (ArrayUtils.contains(theExpectedStatus, StatusEnum.FAILED) && !ArrayUtils.contains(theExpectedStatus, StatusEnum.COMPLETED)) {
inFinalStatus = hasStatus(theBatchJobId, StatusEnum.COMPLETED);
}
boolean retVal = checkStatusWithMaintenancePass(theBatchJobId, theExpectedStatus);
if (!retVal && inFinalStatus) {
// Fail fast - If we hit one of these statuses and it's not the one we want, abort
throw new ConditionTimeoutException("Already in failed/completed status");
}
return retVal;
});
} catch (ConditionTimeoutException e) {
String statuses = myJobPersistence.fetchInstances(100, 0)
.stream()
@ -130,7 +144,7 @@ public class Batch2JobHelper {
return hasStatus(theBatchJobId, theExpectedStatuses);
}
private boolean hasStatus(String theBatchJobId, StatusEnum[] theExpectedStatuses) {
private boolean hasStatus(String theBatchJobId, StatusEnum... theExpectedStatuses) {
StatusEnum status = getStatus(theBatchJobId);
ourLog.debug("Checking status of {} in {}: is {}", theBatchJobId, theExpectedStatuses, status);
return ArrayUtils.contains(theExpectedStatuses, status);

View File

@ -1,76 +0,0 @@
package ca.uhn.fhir.jpa.delete.provider;
import ca.uhn.fhir.batch2.jobs.expunge.DeleteExpungeProvider;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.api.server.storage.IDeleteExpungeJobSubmitter;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import ca.uhn.fhir.test.utilities.JettyUtil;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
@ExtendWith(MockitoExtension.class)
class DeleteExpungeProviderTest {
@Mock
private IDeleteExpungeJobSubmitter myJobSubmitter;
private Server myServer;
private FhirContext myCtx;
private int myPort;
private CloseableHttpClient myClient;
@BeforeEach
public void start() throws Exception {
myCtx = FhirContext.forR4Cached();
myServer = new Server(0);
DeleteExpungeProvider provider = new DeleteExpungeProvider(myCtx, myJobSubmitter);
ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer(myCtx);
servlet.registerProvider(provider);
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
myServer.setHandler(proxyHandler);
JettyUtil.startServer(myServer);
myPort = JettyUtil.getPortForStartedServer(myServer);
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
HttpClientBuilder builder = HttpClientBuilder.create();
builder.setConnectionManager(connectionManager);
myClient = builder.build();
}
@Test
public void testSupplyingNoUrlsProvidesValidErrorMessage() throws IOException {
HttpPost post = new HttpPost("http://localhost:" + myPort + "/" + ProviderConstants.OPERATION_DELETE_EXPUNGE);
try(CloseableHttpResponse execute = myClient.execute(post)) {
String body = IOUtils.toString(execute.getEntity().getContent(), Charset.defaultCharset());
assertThat(execute.getStatusLine().getStatusCode(), is(equalTo(400)));
assertThat(body, is(containsString("At least one `url` parameter to $delete-expunge must be provided.")));
}
}
}

View File

@ -40,7 +40,7 @@ class Batch2JobHelperTest {
@Test
void awaitJobCompletion_inProgress_callsMaintenance() {
when(myJobCoordinator.getInstance(JOB_ID)).thenReturn(ourIncompleteInstance, ourCompleteInstance);
when(myJobCoordinator.getInstance(JOB_ID)).thenReturn(ourIncompleteInstance, ourIncompleteInstance, ourIncompleteInstance, ourCompleteInstance);
myBatch2JobHelper.awaitJobCompletion(JOB_ID);
verify(myJobMaintenanceService, times(1)).runMaintenancePass();

View File

@ -0,0 +1,14 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%file:%line] - %msg%n
</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>

View File

@ -15,7 +15,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import javax.annotation.PostConstruct;
import java.util.Date;
import java.util.List;
@ -48,7 +47,7 @@ public class OldAuditEventPurgeService {
ourLog.info("Submitting an AuditEvent purge job with URL: {}", url);
myDeleteExpungeSubmitter.submitJob(1000, List.of(url), new SystemRequestDetails());
myDeleteExpungeSubmitter.submitJob(1000, List.of(url), false, null, new SystemRequestDetails());
}
public static class OldAuditEventPurgeServiceJob implements HapiJob {

View File

@ -126,15 +126,6 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<forkCount>1</forkCount>
<reuseForks>false</reuseForks>
<runOrder>alphabetical</runOrder>
<includes>
<include>**/*IT.java</include>
</includes>
<useModulePath>false</useModulePath>
</configuration>
<executions>
<execution>
<goals>

View File

@ -30,5 +30,5 @@ public interface IDeleteExpungeJobSubmitter {
* @param theUrlsToProcess A list of strings of the form "/Patient?active=true"
* @return The Batch2 JobId that was started to run this batch job
*/
String submitJob(Integer theBatchSize, List<String> theUrlsToProcess, RequestDetails theRequest);
String submitJob(Integer theBatchSize, List<String> theUrlsToProcess, boolean theCascade, Integer theCascadeMaxRounds, RequestDetails theRequest);
}

View File

@ -29,16 +29,7 @@ import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.BundleLinks;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.DeleteCascadeModeEnum;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.PreferHandlingEnum;
import ca.uhn.fhir.rest.api.PreferHeader;
import ca.uhn.fhir.rest.api.PreferReturnEnum;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.SummaryEnum;
import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.api.server.IRestfulResponse;
import ca.uhn.fhir.rest.api.server.IRestfulServer;
import ca.uhn.fhir.rest.api.server.RequestDetails;
@ -49,17 +40,11 @@ import ca.uhn.fhir.rest.server.method.SummaryEnumParameter;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.BinaryUtil;
import ca.uhn.fhir.util.DateUtils;
import ca.uhn.fhir.util.IoUtil;
import ca.uhn.fhir.util.UrlUtil;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseBinary;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IDomainResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.apache.commons.lang3.math.NumberUtils;
import org.hl7.fhir.instance.model.api.*;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@ -67,96 +52,23 @@ import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeSet;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.replace;
import static org.apache.commons.lang3.StringUtils.trim;
import static org.apache.commons.lang3.StringUtils.*;
public class RestfulServerUtils {
static final Pattern ACCEPT_HEADER_PATTERN = Pattern.compile("\\s*([a-zA-Z0-9+.*/-]+)\\s*(;\\s*([a-zA-Z]+)\\s*=\\s*([a-zA-Z0-9.]+)\\s*)?(,?)" );
static final Pattern ACCEPT_HEADER_PATTERN = Pattern.compile("\\s*([a-zA-Z0-9+.*/-]+)\\s*(;\\s*([a-zA-Z]+)\\s*=\\s*([a-zA-Z0-9.]+)\\s*)?(,?)");
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestfulServerUtils.class);
private static final HashSet<String> TEXT_ENCODE_ELEMENTS = new HashSet<>(Arrays.asList("*.text", "*.id", "*.meta", "*.(mandatory)" ));
private static final HashSet<String> TEXT_ENCODE_ELEMENTS = new HashSet<>(Arrays.asList("*.text", "*.id", "*.meta", "*.(mandatory)"));
private static Map<FhirVersionEnum, FhirContext> myFhirContextMap = Collections.synchronizedMap(new HashMap<>());
private static EnumSet<RestOperationTypeEnum> ourOperationsWhichAllowPreferHeader = EnumSet.of(RestOperationTypeEnum.CREATE, RestOperationTypeEnum.UPDATE, RestOperationTypeEnum.PATCH);
private enum NarrativeModeEnum {
NORMAL, ONLY, SUPPRESS;
public static NarrativeModeEnum valueOfCaseInsensitive(String theCode) {
return valueOf(NarrativeModeEnum.class, theCode.toUpperCase());
}
}
/**
* Return type for {@link RestfulServerUtils#determineRequestEncodingNoDefault(RequestDetails)}
*/
public static class ResponseEncoding {
private final String myContentType;
private final EncodingEnum myEncoding;
private final Boolean myNonLegacy;
public ResponseEncoding(FhirContext theCtx, EncodingEnum theEncoding, String theContentType) {
super();
myEncoding = theEncoding;
myContentType = theContentType;
if (theContentType != null) {
FhirVersionEnum ctxtEnum = theCtx.getVersion().getVersion();
if (theContentType.equals(EncodingEnum.JSON_PLAIN_STRING) || theContentType.equals(EncodingEnum.XML_PLAIN_STRING)) {
myNonLegacy = ctxtEnum.isNewerThan(FhirVersionEnum.DSTU2_1);
} else {
myNonLegacy = ctxtEnum.isNewerThan(FhirVersionEnum.DSTU2_1) && !EncodingEnum.isLegacy(theContentType);
}
} else {
FhirVersionEnum ctxtEnum = theCtx.getVersion().getVersion();
if (ctxtEnum.isOlderThan(FhirVersionEnum.DSTU3)) {
myNonLegacy = null;
} else {
myNonLegacy = Boolean.TRUE;
}
}
}
public String getContentType() {
return myContentType;
}
public EncodingEnum getEncoding() {
return myEncoding;
}
public String getResourceContentType() {
if (Boolean.TRUE.equals(isNonLegacy())) {
return getEncoding().getResourceContentTypeNonLegacy();
}
return getEncoding().getResourceContentType();
}
Boolean isNonLegacy() {
return myNonLegacy;
}
}
@SuppressWarnings("EnumSwitchStatementWhichMissesCases" )
@SuppressWarnings("EnumSwitchStatementWhichMissesCases")
public static void configureResponseParser(RequestDetails theRequestDetails, IParser parser) {
// Pretty print
boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theRequestDetails.getServer(), theRequestDetails);
@ -170,7 +82,7 @@ public class RestfulServerUtils {
// _elements
Set<String> elements = ElementsParameter.getElementsValueOrNull(theRequestDetails, false);
if (elements != null && !summaryMode.equals(Collections.singleton(SummaryEnum.FALSE))) {
throw new InvalidRequestException(Msg.code(304) + "Cannot combine the " + Constants.PARAM_SUMMARY + " and " + Constants.PARAM_ELEMENTS + " parameters" );
throw new InvalidRequestException(Msg.code(304) + "Cannot combine the " + Constants.PARAM_SUMMARY + " and " + Constants.PARAM_ELEMENTS + " parameters");
}
// _elements:exclude
@ -188,7 +100,7 @@ public class RestfulServerUtils {
}
if (summaryModeCount) {
parser.setEncodeElements(Sets.newHashSet("Bundle.total", "Bundle.type" ));
parser.setEncodeElements(Sets.newHashSet("Bundle.total", "Bundle.type"));
} else if (summaryMode.contains(SummaryEnum.TEXT) && summaryMode.size() == 1) {
parser.setEncodeElements(TEXT_ENCODE_ELEMENTS);
parser.setEncodeElementsAppliesToChildResourcesOnly(true);
@ -224,7 +136,7 @@ public class RestfulServerUtils {
*/
boolean haveExplicitBundleElement = false;
for (String next : newElements) {
if (next.startsWith("Bundle." )) {
if (next.startsWith("Bundle.")) {
haveExplicitBundleElement = true;
break;
}
@ -251,7 +163,6 @@ public class RestfulServerUtils {
}
}
public static String createLinkSelf(String theServerBase, RequestDetails theRequest) {
return createLinkSelfWithoutGivenParameters(theServerBase, theRequest, null);
}
@ -265,7 +176,7 @@ public class RestfulServerUtils {
if (isNotBlank(theRequest.getRequestPath())) {
b.append('/');
if (isNotBlank(theRequest.getTenantId()) && theRequest.getRequestPath().startsWith(theRequest.getTenantId() + "/" )) {
if (isNotBlank(theRequest.getTenantId()) && theRequest.getRequestPath().startsWith(theRequest.getTenantId() + "/")) {
b.append(theRequest.getRequestPath().substring(theRequest.getTenantId().length() + 1));
} else {
b.append(theRequest.getRequestPath());
@ -302,7 +213,7 @@ public class RestfulServerUtils {
if (isNotBlank(requestPath)) {
b.append('/');
if (isNotBlank(tenantId) && requestPath.startsWith(tenantId + "/" )) {
if (isNotBlank(tenantId) && requestPath.startsWith(tenantId + "/")) {
b.append(requestPath.substring(tenantId.length() + 1));
} else {
b.append(requestPath);
@ -376,7 +287,7 @@ public class RestfulServerUtils {
b.append(Constants.PARAM_FORMAT);
b.append('=');
String format = strings[0];
format = replace(format, " ", "+" );
format = replace(format, " ", "+");
b.append(UrlUtil.escapeUrlParam(format));
}
if (theBundleLinks.prettyPrint) {
@ -414,7 +325,7 @@ public class RestfulServerUtils {
.stream()
.sorted()
.map(UrlUtil::escapeUrlParam)
.collect(Collectors.joining("," ));
.collect(Collectors.joining(","));
b.append(nextValue);
}
@ -429,7 +340,7 @@ public class RestfulServerUtils {
.stream()
.sorted()
.map(UrlUtil::escapeUrlParam)
.collect(Collectors.joining("," ));
.collect(Collectors.joining(","));
b.append(nextValue);
}
}
@ -460,7 +371,7 @@ public class RestfulServerUtils {
while (acceptValues.hasNext() && retVal == null) {
String nextAcceptHeaderValue = acceptValues.next();
if (nextAcceptHeaderValue != null && isNotBlank(nextAcceptHeaderValue)) {
for (String nextPart : nextAcceptHeaderValue.split("," )) {
for (String nextPart : nextAcceptHeaderValue.split(",")) {
int scIdx = nextPart.indexOf(';');
if (scIdx == 0) {
continue;
@ -535,7 +446,7 @@ public class RestfulServerUtils {
ResponseEncoding retVal = null;
if (acceptValues != null) {
for (String nextAcceptHeaderValue : acceptValues) {
StringTokenizer tok = new StringTokenizer(nextAcceptHeaderValue, "," );
StringTokenizer tok = new StringTokenizer(nextAcceptHeaderValue, ",");
while (tok.hasMoreTokens()) {
String nextToken = tok.nextToken();
int startSpaceIndex = -1;
@ -569,14 +480,14 @@ public class RestfulServerUtils {
} else {
encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken.substring(startSpaceIndex, endSpaceIndex), thePreferContentType);
String remaining = nextToken.substring(endSpaceIndex + 1);
StringTokenizer qualifierTok = new StringTokenizer(remaining, ";" );
StringTokenizer qualifierTok = new StringTokenizer(remaining, ";");
while (qualifierTok.hasMoreTokens()) {
String nextQualifier = qualifierTok.nextToken();
int equalsIndex = nextQualifier.indexOf('=');
if (equalsIndex != -1) {
String nextQualifierKey = nextQualifier.substring(0, equalsIndex).trim();
String nextQualifierValue = nextQualifier.substring(equalsIndex + 1, nextQualifier.length()).trim();
if (nextQualifierKey.equals("q" )) {
if (nextQualifierKey.equals("q")) {
try {
q = Float.parseFloat(nextQualifierValue);
q = Math.max(q, 0.0f);
@ -816,7 +727,7 @@ public class RestfulServerUtils {
PreferHeader retVal = new PreferHeader();
if (isNotBlank(theValue)) {
StringTokenizer tok = new StringTokenizer(theValue, ";," );
StringTokenizer tok = new StringTokenizer(theValue, ";,");
while (tok.hasMoreTokens()) {
String next = trim(tok.nextToken());
int eqIndex = next.indexOf('=');
@ -866,7 +777,6 @@ public class RestfulServerUtils {
return value;
}
public static boolean prettyPrintResponse(IRestfulServerDefaults theServer, RequestDetails theRequest) {
Map<String, String[]> requestParams = theRequest.getParameters();
String[] pretty = requestParams.get(Constants.PARAM_PRETTY);
@ -878,7 +788,7 @@ public class RestfulServerUtils {
List<String> acceptValues = theRequest.getHeaders(Constants.HEADER_ACCEPT);
if (acceptValues != null) {
for (String nextAcceptHeaderValue : acceptValues) {
if (nextAcceptHeaderValue.contains("pretty=true" )) {
if (nextAcceptHeaderValue.contains("pretty=true")) {
prettyPrint = true;
}
}
@ -956,7 +866,7 @@ public class RestfulServerUtils {
// Force binary resources to download - This is a security measure to prevent
// malicious images or HTML blocks being served up as content.
contentType = getBinaryContentTypeOrDefault(bin);
response.addHeader(Constants.HEADER_CONTENT_DISPOSITION, "Attachment;" );
response.addHeader(Constants.HEADER_CONTENT_DISPOSITION, "Attachment;");
Integer contentLength = null;
if (bin.hasData()) {
@ -1065,7 +975,7 @@ public class RestfulServerUtils {
* - If the binary was externalized and has not been reinflated upstream, return false.
* - If they request octet-stream, return true;
* - If the content-type happens to be a match, return true.
*
* <p>
* - Construct an EncodingEnum out of the contentType. If this matches the responseEncoding, return true.
* - Otherwise, return false.
*
@ -1102,34 +1012,147 @@ public class RestfulServerUtils {
try {
return Integer.parseInt(retVal[0]);
} catch (NumberFormatException e) {
ourLog.debug("Failed to parse {} value '{}': {}", new Object[]{theParamName, retVal[0], e});
ourLog.debug("Failed to parse {} value '{}': {}", theParamName, retVal[0], e.toString());
return null;
}
}
public static void validateResourceListNotNull(List<? extends IBaseResource> theResourceList) {
if (theResourceList == null) {
throw new InternalErrorException(Msg.code(306) + "IBundleProvider returned a null list of resources - This is not allowed" );
throw new InternalErrorException(Msg.code(306) + "IBundleProvider returned a null list of resources - This is not allowed");
}
}
/**
* @since 5.0.0
*/
public static DeleteCascadeModeEnum extractDeleteCascadeParameter(RequestDetails theRequest) {
public static DeleteCascadeDetails extractDeleteCascadeParameter(RequestDetails theRequest) {
DeleteCascadeModeEnum mode = null;
Integer maxRounds = null;
if (theRequest != null) {
String[] cascadeParameters = theRequest.getParameters().get(Constants.PARAMETER_CASCADE_DELETE);
if (cascadeParameters != null && Arrays.asList(cascadeParameters).contains(Constants.CASCADE_DELETE)) {
return DeleteCascadeModeEnum.DELETE;
mode = DeleteCascadeModeEnum.DELETE;
String[] maxRoundsValues = theRequest.getParameters().get(Constants.PARAMETER_CASCADE_DELETE_MAX_ROUNDS);
if (maxRoundsValues != null && maxRoundsValues.length > 0) {
String maxRoundsString = maxRoundsValues[0];
maxRounds = parseMaxRoundsString(maxRoundsString);
}
}
String cascadeHeader = theRequest.getHeader(Constants.HEADER_CASCADE);
if (Constants.CASCADE_DELETE.equals(cascadeHeader)) {
return DeleteCascadeModeEnum.DELETE;
if (mode == null) {
String cascadeHeader = theRequest.getHeader(Constants.HEADER_CASCADE);
if (isNotBlank(cascadeHeader)) {
if (Constants.CASCADE_DELETE.equals(cascadeHeader) || cascadeHeader.startsWith(Constants.CASCADE_DELETE + ";") || cascadeHeader.startsWith(Constants.CASCADE_DELETE + " ")) {
mode = DeleteCascadeModeEnum.DELETE;
if (cascadeHeader.contains(";")) {
String remainder = cascadeHeader.substring(cascadeHeader.indexOf(';') + 1);
remainder = trim(remainder);
if (remainder.startsWith(Constants.HEADER_CASCADE_MAX_ROUNDS + "=")) {
String maxRoundsString = remainder.substring(Constants.HEADER_CASCADE_MAX_ROUNDS.length() + 1);
maxRounds = parseMaxRoundsString(maxRoundsString);
}
}
}
}
}
}
return DeleteCascadeModeEnum.NONE;
if (mode == null) {
mode = DeleteCascadeModeEnum.NONE;
}
return new DeleteCascadeDetails(mode, maxRounds);
}
@Nullable
private static Integer parseMaxRoundsString(String theMaxRoundsString) {
Integer maxRounds;
if (isBlank(theMaxRoundsString)) {
maxRounds = null;
} else if (NumberUtils.isDigits(theMaxRoundsString)) {
maxRounds = Integer.parseInt(theMaxRoundsString);
} else {
throw new InvalidRequestException(Msg.code(2349) + "Invalid value for " + Constants.PARAMETER_CASCADE_DELETE_MAX_ROUNDS + " parameter");
}
return maxRounds;
}
private enum NarrativeModeEnum {
NORMAL, ONLY, SUPPRESS;
public static NarrativeModeEnum valueOfCaseInsensitive(String theCode) {
return valueOf(NarrativeModeEnum.class, theCode.toUpperCase());
}
}
/**
* Return type for {@link RestfulServerUtils#determineRequestEncodingNoDefault(RequestDetails)}
*/
public static class ResponseEncoding {
private final String myContentType;
private final EncodingEnum myEncoding;
private final Boolean myNonLegacy;
public ResponseEncoding(FhirContext theCtx, EncodingEnum theEncoding, String theContentType) {
super();
myEncoding = theEncoding;
myContentType = theContentType;
if (theContentType != null) {
FhirVersionEnum ctxtEnum = theCtx.getVersion().getVersion();
if (theContentType.equals(EncodingEnum.JSON_PLAIN_STRING) || theContentType.equals(EncodingEnum.XML_PLAIN_STRING)) {
myNonLegacy = ctxtEnum.isNewerThan(FhirVersionEnum.DSTU2_1);
} else {
myNonLegacy = ctxtEnum.isNewerThan(FhirVersionEnum.DSTU2_1) && !EncodingEnum.isLegacy(theContentType);
}
} else {
FhirVersionEnum ctxtEnum = theCtx.getVersion().getVersion();
if (ctxtEnum.isOlderThan(FhirVersionEnum.DSTU3)) {
myNonLegacy = null;
} else {
myNonLegacy = Boolean.TRUE;
}
}
}
public String getContentType() {
return myContentType;
}
public EncodingEnum getEncoding() {
return myEncoding;
}
public String getResourceContentType() {
if (Boolean.TRUE.equals(isNonLegacy())) {
return getEncoding().getResourceContentTypeNonLegacy();
}
return getEncoding().getResourceContentType();
}
Boolean isNonLegacy() {
return myNonLegacy;
}
}
public static class DeleteCascadeDetails {
private final DeleteCascadeModeEnum myMode;
private final Integer myMaxRounds;
public DeleteCascadeDetails(DeleteCascadeModeEnum theMode, Integer theMaxRounds) {
myMode = theMode;
myMaxRounds = theMaxRounds;
}
public DeleteCascadeModeEnum getMode() {
return myMode;
}
public Integer getMaxRounds() {
return myMaxRounds;
}
}
}

View File

@ -251,7 +251,7 @@ public class OperationParameter implements IParameter {
@SuppressWarnings("unchecked")
@Override
public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException {
List<Object> matchingParamValues = new ArrayList<Object>();
List<Object> matchingParamValues = new ArrayList<>();
OperationMethodBinding method = (OperationMethodBinding) theMethodBinding;

View File

@ -161,6 +161,14 @@ public class ProviderConstants {
* Number of resources to delete at a time for the $delete-expunge operation
*/
public static final String OPERATION_DELETE_BATCH_SIZE = "batchSize";
/**
* Should we cascade the $delete-expunge operation
*/
public static final String OPERATION_DELETE_CASCADE = "cascade";
/**
* How many rounds for the $delete-expunge operation
*/
public static final String OPERATION_DELETE_CASCADE_MAX_ROUNDS = "cascadeMaxRounds";
/**
* The Spring Batch job id of the delete expunge job created by a $delete-expunge operation

View File

@ -1,10 +1,15 @@
package ca.uhn.fhir.rest.server;
import ca.uhn.fhir.rest.api.PreferHandlingEnum;
import ca.uhn.fhir.rest.api.PreferHeader;
import ca.uhn.fhir.rest.api.PreferReturnEnum;
import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
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.CsvSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Arrays;
import java.util.HashMap;
@ -12,39 +17,43 @@ import java.util.List;
import java.util.Map;
import static ca.uhn.fhir.rest.api.RequestTypeEnum.GET;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
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.api.Assertions.*;
import static org.mockito.Mockito.when;
public class RestfulServerUtilsTest{
@ExtendWith(MockitoExtension.class)
public class RestfulServerUtilsTest {
@Mock
private RequestDetails myRequestDetails;
@Test
public void testParsePreferReturn() {
PreferHeader header = RestfulServerUtils.parsePreferHeader(null,"return=representation");
PreferHeader header = RestfulServerUtils.parsePreferHeader(null, "return=representation");
assertEquals(PreferReturnEnum.REPRESENTATION, header.getReturn());
assertFalse(header.getRespondAsync());
}
@Test
public void testParsePreferReturnAndAsync() {
PreferHeader header = RestfulServerUtils.parsePreferHeader(null,"return=OperationOutcome; respond-async");
PreferHeader header = RestfulServerUtils.parsePreferHeader(null, "return=OperationOutcome; respond-async");
assertEquals(PreferReturnEnum.OPERATION_OUTCOME, header.getReturn());
assertTrue(header.getRespondAsync());
}
@Test
public void testParsePreferAsync() {
PreferHeader header = RestfulServerUtils.parsePreferHeader(null,"respond-async");
PreferHeader header = RestfulServerUtils.parsePreferHeader(null, "respond-async");
assertEquals(null, header.getReturn());
assertTrue(header.getRespondAsync());
}
@Test
public void testParseHandlingLenient() {
PreferHeader header = RestfulServerUtils.parsePreferHeader(null,"handling=lenient");
PreferHeader header = RestfulServerUtils.parsePreferHeader(null, "handling=lenient");
assertEquals(null, header.getReturn());
assertFalse(header.getRespondAsync());
assertEquals(PreferHandlingEnum.LENIENT, header.getHanding());
@ -52,7 +61,7 @@ public class RestfulServerUtilsTest{
@Test
public void testParseHandlingLenientAndReturnRepresentation_CommaSeparatd() {
PreferHeader header = RestfulServerUtils.parsePreferHeader(null,"handling=lenient, return=representation");
PreferHeader header = RestfulServerUtils.parsePreferHeader(null, "handling=lenient, return=representation");
assertEquals(PreferReturnEnum.REPRESENTATION, header.getReturn());
assertFalse(header.getRespondAsync());
assertEquals(PreferHandlingEnum.LENIENT, header.getHanding());
@ -60,12 +69,55 @@ public class RestfulServerUtilsTest{
@Test
public void testParseHandlingLenientAndReturnRepresentation_SemicolonSeparatd() {
PreferHeader header = RestfulServerUtils.parsePreferHeader(null,"handling=lenient; return=representation");
PreferHeader header = RestfulServerUtils.parsePreferHeader(null, "handling=lenient; return=representation");
assertEquals(PreferReturnEnum.REPRESENTATION, header.getReturn());
assertFalse(header.getRespondAsync());
assertEquals(PreferHandlingEnum.LENIENT, header.getHanding());
}
@ParameterizedTest
@CsvSource({
" , , , NONE ,",
"foo , , , NONE , ",
"delete , , , DELETE ,",
"delete , 10 , , DELETE , 10",
"delete , abc , , DELETE , -1", // -1 means exception
" , , delete , DELETE ,",
" , , delete; , DELETE ,",
" , , delete; max-rounds= , DELETE , ",
" , , delete; max-rounds , DELETE , ",
" , , delete; max-rounds=10 , DELETE , 10",
" , , delete; max-rounds=10 , DELETE , 10",
})
public void testParseCascade(String theCascadeParam, String theCascadeMaxRoundsParam, String theCascadeHeader, DeleteCascadeModeEnum theExpectedMode, Integer theExpectedMaxRounds) {
HashMap<String, String[]> params = new HashMap<>();
when(myRequestDetails.getParameters()).thenReturn(params);
if (isNotBlank(theCascadeParam)) {
params.put(Constants.PARAMETER_CASCADE_DELETE, new String[]{theCascadeParam.trim()});
}
if (isNotBlank(theCascadeMaxRoundsParam)) {
params.put(Constants.PARAMETER_CASCADE_DELETE_MAX_ROUNDS, new String[]{theCascadeMaxRoundsParam.trim()});
}
if (isNotBlank(theCascadeHeader)) {
when(myRequestDetails.getHeader(Constants.HEADER_CASCADE)).thenReturn(theCascadeHeader);
}
if (theExpectedMaxRounds != null && theExpectedMaxRounds == -1) {
try {
RestfulServerUtils.extractDeleteCascadeParameter(myRequestDetails);
fail();
} catch (InvalidRequestException e) {
// good
}
} else {
RestfulServerUtils.DeleteCascadeDetails outcome = RestfulServerUtils.extractDeleteCascadeParameter(myRequestDetails);
assertEquals(theExpectedMode, outcome.getMode());
assertEquals(theExpectedMaxRounds, outcome.getMaxRounds());
}
}
@Test
public void testCreateSelfLinks() {

View File

@ -135,9 +135,6 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<redirectTestOutputToFile>true</redirectTestOutputToFile>
</configuration>
<executions>
<execution>
<goals>

View File

@ -27,6 +27,7 @@ import ca.uhn.fhir.jpa.bulk.export.provider.BulkDataExportProvider;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.bulk.BulkDataExportOptions;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.apache.commons.lang3.StringUtils;
@ -54,7 +55,7 @@ public class BulkExportJobParametersValidator implements IJobParametersValidator
@Nullable
@Override
public List<String> validate(@Nonnull BulkExportJobParameters theParameters) {
public List<String> validate(RequestDetails theRequestDetails, @Nonnull BulkExportJobParameters theParameters) {
List<String> errorMsgs = new ArrayList<>();
// initial validation

View File

@ -30,6 +30,7 @@ import ca.uhn.fhir.jpa.api.svc.IBatch2DaoSvc;
import ca.uhn.fhir.jpa.api.svc.IDeleteExpungeSvc;
import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
import ca.uhn.fhir.rest.api.server.storage.IDeleteExpungeJobSubmitter;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import org.springframework.context.annotation.Bean;
@ -46,14 +47,15 @@ public class DeleteExpungeAppCtx {
IBatch2DaoSvc theBatch2DaoSvc,
HapiTransactionService theHapiTransactionService,
IDeleteExpungeSvc theDeleteExpungeSvc,
IIdHelperService theIdHelperService) {
IIdHelperService theIdHelperService,
IRequestPartitionHelperSvc theRequestPartitionHelperSvc) {
return JobDefinition
.newBuilder()
.setJobDefinitionId(JOB_DELETE_EXPUNGE)
.setJobDescription("Expunge resources")
.setJobDefinitionVersion(1)
.setParametersType(DeleteExpungeJobParameters.class)
.setParametersValidator(expungeJobParametersValidator(theBatch2DaoSvc))
.setParametersValidator(expungeJobParametersValidator(theBatch2DaoSvc, theDeleteExpungeSvc, theRequestPartitionHelperSvc))
.gatedExecution()
.addFirstStep(
"generate-ranges",
@ -73,8 +75,8 @@ public class DeleteExpungeAppCtx {
}
@Bean
public DeleteExpungeJobParametersValidator expungeJobParametersValidator(IBatch2DaoSvc theBatch2DaoSvc) {
return new DeleteExpungeJobParametersValidator(new UrlListValidator(ProviderConstants.OPERATION_EXPUNGE, theBatch2DaoSvc));
public DeleteExpungeJobParametersValidator expungeJobParametersValidator(IBatch2DaoSvc theBatch2DaoSvc, IDeleteExpungeSvc theDeleteExpungeSvc, IRequestPartitionHelperSvc theRequestPartitionHelperSvc) {
return new DeleteExpungeJobParametersValidator(new UrlListValidator(ProviderConstants.OPERATION_EXPUNGE, theBatch2DaoSvc), theDeleteExpungeSvc, theRequestPartitionHelperSvc);
}
@Bean

View File

@ -20,6 +20,34 @@
package ca.uhn.fhir.batch2.jobs.expunge;
import ca.uhn.fhir.batch2.jobs.parameters.PartitionedUrlListJobParameters;
import com.fasterxml.jackson.annotation.JsonProperty;
public class DeleteExpungeJobParameters extends PartitionedUrlListJobParameters {
@JsonProperty("cascade")
private boolean myCascade;
@JsonProperty("cascadeMaxRounds")
private Integer myCascadeMaxRounds;
/**
* Constructor
*/
public DeleteExpungeJobParameters() {
super();
}
public Integer getCascadeMaxRounds() {
return myCascadeMaxRounds;
}
public void setCascadeMaxRounds(Integer theCascadeMaxRounds) {
myCascadeMaxRounds = theCascadeMaxRounds;
}
public boolean isCascade() {
return myCascade;
}
public void setCascade(boolean theCascade) {
myCascade = theCascade;
}
}

View File

@ -20,22 +20,47 @@
package ca.uhn.fhir.batch2.jobs.expunge;
import ca.uhn.fhir.batch2.api.IJobParametersValidator;
import ca.uhn.fhir.batch2.jobs.parameters.UrlListValidator;
import ca.uhn.fhir.batch2.jobs.parameters.IUrlListValidator;
import ca.uhn.fhir.batch2.jobs.parameters.PartitionedUrl;
import ca.uhn.fhir.jpa.api.svc.IDeleteExpungeSvc;
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.util.ValidateUtil;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.List;
public class DeleteExpungeJobParametersValidator implements IJobParametersValidator<DeleteExpungeJobParameters> {
private final UrlListValidator myUrlListValidator;
private final IUrlListValidator myUrlListValidator;
private final IDeleteExpungeSvc<?> myDeleteExpungeSvc;
private final IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
public DeleteExpungeJobParametersValidator(UrlListValidator theUrlListValidator) {
public DeleteExpungeJobParametersValidator(IUrlListValidator theUrlListValidator, IDeleteExpungeSvc<?> theDeleteExpungeSvc, IRequestPartitionHelperSvc theRequestPartitionHelperSvc) {
myUrlListValidator = theUrlListValidator;
myDeleteExpungeSvc = theDeleteExpungeSvc;
myRequestPartitionHelperSvc = theRequestPartitionHelperSvc;
}
@Nullable
@Override
public List<String> validate(@Nonnull DeleteExpungeJobParameters theParameters) {
public List<String> validate(RequestDetails theRequestDetails, @Nonnull DeleteExpungeJobParameters theParameters) {
// Make sure cascade is supported if requested
if (theParameters.isCascade() && !myDeleteExpungeSvc.isCascadeSupported()) {
return List.of("Cascading delete is not supported on this server");
}
// Verify that the user has access to all requested partitions
myRequestPartitionHelperSvc.validateHasPartitionPermissions(theRequestDetails, null, theParameters.getRequestPartitionId());
for (PartitionedUrl partitionedUrl : theParameters.getPartitionedUrls()) {
String url = partitionedUrl.getUrl();
ValidateUtil.isTrueOrThrowInvalidRequest(url.matches("[a-zA-Z]+\\?.*"), "Delete expunge URLs must be in the format [resourceType]?[parameters]");
if (partitionedUrl.getRequestPartitionId() != null) {
myRequestPartitionHelperSvc.validateHasPartitionPermissions(theRequestDetails, null, partitionedUrl.getRequestPartitionId());
}
}
return myUrlListValidator.validatePartitionedUrls(theParameters.getPartitionedUrls());
}
}

View File

@ -66,7 +66,7 @@ public class DeleteExpungeJobSubmitterImpl implements IDeleteExpungeJobSubmitter
@Override
@Transactional(propagation = Propagation.NEVER)
public String submitJob(Integer theBatchSize, List<String> theUrlsToDeleteExpunge, RequestDetails theRequestDetails) {
public String submitJob(Integer theBatchSize, List<String> theUrlsToDeleteExpunge, boolean theCascade, Integer theCascadeMaxRounds, RequestDetails theRequestDetails) {
if (theBatchSize == null) {
theBatchSize = myStorageSettings.getExpungeBatchSize();
}
@ -94,11 +94,13 @@ public class DeleteExpungeJobSubmitterImpl implements IDeleteExpungeJobSubmitter
// Also set toplevel partition in case there are no urls
RequestPartitionId requestPartition = myRequestPartitionHelperSvc.determineReadPartitionForRequest(theRequestDetails, details);
deleteExpungeJobParameters.setRequestPartitionId(requestPartition);
deleteExpungeJobParameters.setCascade(theCascade);
deleteExpungeJobParameters.setCascadeMaxRounds(theCascadeMaxRounds);
JobInstanceStartRequest startRequest = new JobInstanceStartRequest();
startRequest.setJobDefinitionId(JOB_DELETE_EXPUNGE);
startRequest.setParameters(deleteExpungeJobParameters);
Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(startRequest);
Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(theRequestDetails, startRequest);
return startResponse.getInstanceId();
}
}

View File

@ -48,7 +48,9 @@ public class DeleteExpungeProvider {
@Operation(name = ProviderConstants.OPERATION_DELETE_EXPUNGE, idempotent = false)
public IBaseParameters deleteExpunge(
@OperationParam(name = ProviderConstants.OPERATION_DELETE_EXPUNGE_URL, typeName = "string", min = 1) List<IPrimitiveType<String>> theUrlsToDeleteExpunge,
@OperationParam(name = ProviderConstants.OPERATION_DELETE_BATCH_SIZE, typeName = "decimal", min = 0, max = 1) IPrimitiveType<BigDecimal> theBatchSize,
@OperationParam(name = ProviderConstants.OPERATION_DELETE_BATCH_SIZE, typeName = "integer", min = 0, max = 1) IPrimitiveType<Integer> theBatchSize,
@OperationParam(name = ProviderConstants.OPERATION_DELETE_CASCADE, typeName = "boolean", min = 0, max = 1) IPrimitiveType<Boolean> theCascade,
@OperationParam(name = ProviderConstants.OPERATION_DELETE_CASCADE_MAX_ROUNDS, typeName = "integer", min = 0, max = 1) IPrimitiveType<Integer> theCascadeMaxRounds,
RequestDetails theRequestDetails
) {
if (theUrlsToDeleteExpunge == null) {
@ -60,10 +62,21 @@ public class DeleteExpungeProvider {
.collect(Collectors.toList());
Integer batchSize = null;
if (theBatchSize != null && theBatchSize.getValue() !=null && theBatchSize.getValue().intValue() > 0) {
batchSize = theBatchSize.getValue().intValue();
if (theBatchSize != null && theBatchSize.getValue() !=null && theBatchSize.getValue() > 0) {
batchSize = theBatchSize.getValue();
}
String jobId = myDeleteExpungeJobSubmitter.submitJob(batchSize, urls, theRequestDetails);
boolean cascase = false;
if (theCascade != null && theCascade.hasValue()) {
cascase = theCascade.getValue();
}
Integer cascadeMaxRounds = null;
if (theCascadeMaxRounds != null) {
cascadeMaxRounds = theCascadeMaxRounds.getValue();
}
String jobId = myDeleteExpungeJobSubmitter.submitJob(batchSize, urls, cascase, cascadeMaxRounds, theRequestDetails);
IBaseParameters retval = ParametersUtil.newInstance(myFhirContext);
ParametersUtil.addParameterToParametersString(myFhirContext, retval, ProviderConstants.OPERATION_BATCH_RESPONSE_JOB_ID, jobId);

View File

@ -19,20 +19,14 @@
*/
package ca.uhn.fhir.batch2.jobs.expunge;
import ca.uhn.fhir.batch2.api.IJobDataSink;
import ca.uhn.fhir.batch2.api.IJobStepWorker;
import ca.uhn.fhir.batch2.api.JobExecutionFailedException;
import ca.uhn.fhir.batch2.api.RunOutcome;
import ca.uhn.fhir.batch2.api.StepExecutionDetails;
import ca.uhn.fhir.batch2.api.VoidModel;
import ca.uhn.fhir.batch2.api.*;
import ca.uhn.fhir.batch2.jobs.chunk.ResourceIdListWorkChunkJson;
import ca.uhn.fhir.batch2.jobs.reindex.ReindexJobParameters;
import ca.uhn.fhir.jpa.api.svc.IDeleteExpungeSvc;
import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -42,7 +36,7 @@ import org.springframework.transaction.support.TransactionCallback;
import javax.annotation.Nonnull;
import java.util.List;
public class DeleteExpungeStep implements IJobStepWorker<ReindexJobParameters, ResourceIdListWorkChunkJson, VoidModel> {
public class DeleteExpungeStep implements IJobStepWorker<DeleteExpungeJobParameters, ResourceIdListWorkChunkJson, VoidModel> {
private static final Logger ourLog = LoggerFactory.getLogger(DeleteExpungeStep.class);
private final HapiTransactionService myHapiTransactionService;
@ -57,20 +51,27 @@ public class DeleteExpungeStep implements IJobStepWorker<ReindexJobParameters, R
@Nonnull
@Override
public RunOutcome run(@Nonnull StepExecutionDetails<ReindexJobParameters, ResourceIdListWorkChunkJson> theStepExecutionDetails, @Nonnull IJobDataSink<VoidModel> theDataSink) throws JobExecutionFailedException {
public RunOutcome run(@Nonnull StepExecutionDetails<DeleteExpungeJobParameters, ResourceIdListWorkChunkJson> theStepExecutionDetails, @Nonnull IJobDataSink<VoidModel> theDataSink) throws JobExecutionFailedException {
ResourceIdListWorkChunkJson data = theStepExecutionDetails.getData();
return doDeleteExpunge(data, theDataSink, theStepExecutionDetails.getInstance().getInstanceId(), theStepExecutionDetails.getChunkId());
boolean cascade = theStepExecutionDetails.getParameters().isCascade();
Integer cascadeMaxRounds = theStepExecutionDetails.getParameters().getCascadeMaxRounds();
return doDeleteExpunge(data, theDataSink, theStepExecutionDetails.getInstance().getInstanceId(), theStepExecutionDetails.getChunkId(), cascade, cascadeMaxRounds);
}
@Nonnull
public RunOutcome doDeleteExpunge(ResourceIdListWorkChunkJson data, IJobDataSink<VoidModel> theDataSink, String theInstanceId, String theChunkId) {
public RunOutcome doDeleteExpunge(ResourceIdListWorkChunkJson theData, IJobDataSink<VoidModel> theDataSink, String theInstanceId, String theChunkId, boolean theCascade, Integer theCascadeMaxRounds) {
RequestDetails requestDetails = new SystemRequestDetails();
TransactionDetails transactionDetails = new TransactionDetails();
myHapiTransactionService.execute(requestDetails, transactionDetails, new DeleteExpungeJob(data, requestDetails, transactionDetails, theDataSink, theInstanceId, theChunkId));
DeleteExpungeJob job = new DeleteExpungeJob(theData, requestDetails, transactionDetails, theDataSink, theInstanceId, theChunkId, theCascade, theCascadeMaxRounds);
myHapiTransactionService
.withRequest(requestDetails)
.withTransactionDetails(transactionDetails)
.withRequestPartitionId(theData.getRequestPartitionId())
.execute(job);
return new RunOutcome(data.size());
return new RunOutcome(job.getRecordCount());
}
private class DeleteExpungeJob implements TransactionCallback<Void> {
@ -80,14 +81,23 @@ public class DeleteExpungeStep implements IJobStepWorker<ReindexJobParameters, R
private final IJobDataSink<VoidModel> myDataSink;
private final String myChunkId;
private final String myInstanceId;
private final boolean myCascade;
private final Integer myCascadeMaxRounds;
private int myRecordCount;
public DeleteExpungeJob(ResourceIdListWorkChunkJson theData, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails, IJobDataSink<VoidModel> theDataSink, String theInstanceId, String theChunkId) {
public DeleteExpungeJob(ResourceIdListWorkChunkJson theData, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails, IJobDataSink<VoidModel> theDataSink, String theInstanceId, String theChunkId, boolean theCascade, Integer theCascadeMaxRounds) {
myData = theData;
myRequestDetails = theRequestDetails;
myTransactionDetails = theTransactionDetails;
myDataSink = theDataSink;
myInstanceId = theInstanceId;
myChunkId = theChunkId;
myCascade = theCascade;
myCascadeMaxRounds = theCascadeMaxRounds;
}
public int getRecordCount() {
return myRecordCount;
}
@Override
@ -100,15 +110,13 @@ public class DeleteExpungeStep implements IJobStepWorker<ReindexJobParameters, R
return null;
}
ourLog.info("Starting delete expunge work chunk with {} resources - Instance[{}] Chunk[{}]", persistentIds.size(), myInstanceId, myChunkId);
myDeleteExpungeSvc.deleteExpunge(persistentIds);
myRecordCount = myDeleteExpungeSvc.deleteExpunge(persistentIds, myCascade, myCascadeMaxRounds);
return null;
}
}
}

View File

@ -23,6 +23,7 @@ import ca.uhn.fhir.batch2.api.IJobParametersValidator;
import ca.uhn.fhir.batch2.importpull.models.Batch2BulkImportPullJobParameters;
import ca.uhn.fhir.jpa.bulk.imprt.api.IBulkDataImportSvc;
import ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobJson;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
@ -44,7 +45,7 @@ public class BulkImportParameterValidator implements IJobParametersValidator<Bat
@Nullable
@Override
public List<String> validate(@Nonnull Batch2BulkImportPullJobParameters theParameters) {
public List<String> validate(RequestDetails theRequestDetails, @Nonnull Batch2BulkImportPullJobParameters theParameters) {
ourLog.info("BulkImportPull parameter validation begin");
ArrayList<String> errors = new ArrayList<>();

View File

@ -179,7 +179,7 @@ public class BulkDataImportProvider {
ourLog.info("Requesting Bulk Import Job ($import by Manifest) with {} urls", typeAndUrls.size());
Batch2JobStartResponse jobStartResponse = myJobCoordinator.startInstance(request);
Batch2JobStartResponse jobStartResponse = myJobCoordinator.startInstance(theRequestDetails, request);
String jobId = jobStartResponse.getInstanceId();
IBaseOperationOutcome response = OperationOutcomeUtil.newInstance(myFhirCtx);

View File

@ -22,6 +22,7 @@ package ca.uhn.fhir.batch2.jobs.reindex;
import ca.uhn.fhir.batch2.api.IJobParametersValidator;
import ca.uhn.fhir.batch2.jobs.parameters.PartitionedUrl;
import ca.uhn.fhir.batch2.jobs.parameters.UrlListValidator;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@ -38,7 +39,7 @@ public class ReindexJobParametersValidator implements IJobParametersValidator<Re
@Nullable
@Override
public List<String> validate(@Nonnull ReindexJobParameters theParameters) {
public List<String> validate(RequestDetails theRequestDetails, @Nonnull ReindexJobParameters theParameters) {
List<String> errors = myUrlListValidator.validatePartitionedUrls(theParameters.getPartitionedUrls());
if (errors == null || errors.isEmpty()) {

View File

@ -118,7 +118,7 @@ public class ReindexProvider {
JobInstanceStartRequest request = new JobInstanceStartRequest();
request.setJobDefinitionId(ReindexAppCtx.JOB_REINDEX);
request.setParameters(params);
Batch2JobStartResponse response = myJobCoordinator.startInstance(request);
Batch2JobStartResponse response = myJobCoordinator.startInstance(theRequestDetails, request);
IBaseParameters retVal = ParametersUtil.newInstance(myFhirContext);
ParametersUtil.addParameterToParametersString(myFhirContext, retVal, ProviderConstants.OPERATION_BATCH_RESPONSE_JOB_ID, response.getInstanceId());

View File

@ -33,6 +33,7 @@ import ca.uhn.fhir.jpa.api.model.BulkExportParameters;
import ca.uhn.fhir.jpa.api.svc.IBatch2JobRunner;
import ca.uhn.fhir.jpa.batch.models.Batch2BaseJobParameters;
import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.util.Batch2JobDefinitionConstants;
import org.slf4j.Logger;
@ -54,11 +55,11 @@ public class Batch2JobRunnerImpl implements IBatch2JobRunner {
}
@Override
public Batch2JobStartResponse startNewJob(Batch2BaseJobParameters theParameters) {
public Batch2JobStartResponse startNewJob(RequestDetails theRequestDetails, Batch2BaseJobParameters theParameters) {
switch (theParameters.getJobDefinitionId()) {
case Batch2JobDefinitionConstants.BULK_EXPORT:
if (theParameters instanceof BulkExportParameters) {
return startBatch2BulkExportJob((BulkExportParameters) theParameters);
return startBatch2BulkExportJob(theRequestDetails, (BulkExportParameters) theParameters);
}
else {
ourLog.error("Invalid parameters for " + Batch2JobDefinitionConstants.BULK_EXPORT);
@ -119,11 +120,11 @@ public class Batch2JobRunnerImpl implements IBatch2JobRunner {
return info;
}
private Batch2JobStartResponse startBatch2BulkExportJob(BulkExportParameters theParameters) {
private Batch2JobStartResponse startBatch2BulkExportJob(RequestDetails theRequestDetails, BulkExportParameters theParameters) {
JobInstanceStartRequest request = createStartRequest(theParameters);
BulkExportJobParameters parameters = BulkExportJobParameters.createFromExportJobParameters(theParameters);
request.setParameters(parameters);
return myJobCoordinator.startInstance(request);
return myJobCoordinator.startInstance(theRequestDetails, request);
}
private JobInstanceStartRequest createStartRequest(Batch2BaseJobParameters theParameters) {

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.batch2.jobs.termcodesystem.codesystemdelete;
import ca.uhn.fhir.batch2.api.IJobParametersValidator;
import ca.uhn.fhir.jpa.term.models.TermCodeSystemDeleteJobParameters;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@ -31,7 +32,7 @@ public class TermCodeSystemDeleteJobParametersValidator implements IJobParameter
@Nullable
@Override
public List<String> validate(@Nonnull TermCodeSystemDeleteJobParameters theParameters) {
public List<String> validate(RequestDetails theRequestDetails, @Nonnull TermCodeSystemDeleteJobParameters theParameters) {
List<String> errors = new ArrayList<>();
if (theParameters.getTermPid() <= 0) {
errors.add("Invalid Term Code System PID " + theParameters.getTermPid());

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.batch2.jobs.termcodesystem.codesystemversiondelete;
import ca.uhn.fhir.batch2.api.IJobParametersValidator;
import ca.uhn.fhir.jpa.term.models.TermCodeSystemDeleteVersionJobParameters;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@ -31,7 +32,7 @@ public class DeleteCodeSystemVersionParameterValidator implements IJobParameters
@Nullable
@Override
public List<String> validate(@Nonnull TermCodeSystemDeleteVersionJobParameters theParameters) {
public List<String> validate(RequestDetails theRequestDetails, @Nonnull TermCodeSystemDeleteVersionJobParameters theParameters) {
ArrayList<String> errors = new ArrayList<>();
long versionPID = theParameters.getCodeSystemVersionPid();

View File

@ -53,7 +53,7 @@ public class BulkExportJobParametersValidatorTest {
.thenReturn(true);
// test
List<String> result = myValidator.validate(parameters);
List<String> result = myValidator.validate(null, parameters);
// verify
assertNotNull(result);
@ -69,7 +69,7 @@ public class BulkExportJobParametersValidatorTest {
when(myDaoRegistry.isResourceTypeSupported(anyString()))
.thenReturn(true);
when(myIBinaryStorageSvc.isValidBlobId(any())).thenReturn(false);
List<String> errors = myValidator.validate(parameters);
List<String> errors = myValidator.validate(null, parameters);
// verify
assertNotNull(errors);
@ -86,7 +86,7 @@ public class BulkExportJobParametersValidatorTest {
.thenReturn(true);
when(myIBinaryStorageSvc.isValidBlobId(any())).thenReturn(true);
List<String> errors = myValidator.validate(parameters);
List<String> errors = myValidator.validate(null, parameters);
// verify
assertNotNull(errors);
@ -103,7 +103,7 @@ public class BulkExportJobParametersValidatorTest {
.thenReturn(true);
// test
List<String> result = myValidator.validate(parameters);
List<String> result = myValidator.validate(null, parameters);
// verify
assertNotNull(result);
@ -119,7 +119,7 @@ public class BulkExportJobParametersValidatorTest {
parameters.setResourceTypes(Collections.singletonList(resourceType));
// test
List<String> result = myValidator.validate(parameters);
List<String> result = myValidator.validate(null, parameters);
// verify
assertNotNull(result);
@ -140,7 +140,7 @@ public class BulkExportJobParametersValidatorTest {
.thenReturn(true);
// test
List<String> result = myValidator.validate(parameters);
List<String> result = myValidator.validate(null, parameters);
// verify
assertNotNull(result);
@ -154,7 +154,7 @@ public class BulkExportJobParametersValidatorTest {
parameters.setExportStyle(BulkDataExportOptions.ExportStyle.GROUP);
// test
List<String> result = myValidator.validate(parameters);
List<String> result = myValidator.validate(null, parameters);
// verify
assertNotNull(result);
@ -169,7 +169,7 @@ public class BulkExportJobParametersValidatorTest {
parameters.setResourceTypes(null);
// test
List<String> results = myValidator.validate(parameters);
List<String> results = myValidator.validate(null, parameters);
// verify
assertNotNull(results);
@ -185,7 +185,7 @@ public class BulkExportJobParametersValidatorTest {
parameters.setOutputFormat(Constants.CT_FHIR_NDJSON);
// test
List<String> errors = myValidator.validate(parameters);
List<String> errors = myValidator.validate(null, parameters);
// validate
assertNotNull(errors);
@ -201,7 +201,7 @@ public class BulkExportJobParametersValidatorTest {
parameters.setOutputFormat("json");
// test
List<String> errors = myValidator.validate(parameters);
List<String> errors = myValidator.validate(null, parameters);
// validate
assertNotNull(errors);

View File

@ -0,0 +1,64 @@
package ca.uhn.fhir.batch2.jobs.expunge;
import ca.uhn.fhir.batch2.jobs.parameters.IUrlListValidator;
import ca.uhn.fhir.jpa.api.svc.IDeleteExpungeSvc;
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.empty;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class DeleteExpungeJobParametersValidatorTest {
@Mock
private IDeleteExpungeSvc<?> myDeleteExpungeSvc;
@Mock
private IUrlListValidator myUrlListValidator;
@Mock
private IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
@InjectMocks
private DeleteExpungeJobParametersValidator mySvc;
@Test
public void testRejectCascadeIfNotSupported() {
// Setup
when(myDeleteExpungeSvc.isCascadeSupported()).thenReturn(false);
DeleteExpungeJobParameters parameters = new DeleteExpungeJobParameters();
parameters.addUrl("Patient?active=true");
parameters.setCascade(true);
// Test
List<String> outcome = mySvc.validate(new SystemRequestDetails(), parameters);
// Verify
assertThat(outcome.toString(), outcome, contains("Cascading delete is not supported on this server"));
}
@Test
public void testValidateSuccess() {
// Setup
when(myDeleteExpungeSvc.isCascadeSupported()).thenReturn(true);
DeleteExpungeJobParameters parameters = new DeleteExpungeJobParameters();
parameters.addUrl("Patient?active=true");
parameters.setCascade(true);
// Test
List<String> outcome = mySvc.validate(new SystemRequestDetails(), parameters);
// Verify
assertThat(outcome, empty());
}
}

View File

@ -1,80 +1,112 @@
package ca.uhn.fhir.batch2.jobs.expunge;
import ca.uhn.fhir.batch2.jobs.BaseR4ServerTest;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.api.server.storage.IDeleteExpungeJobSubmitter;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import ca.uhn.fhir.test.utilities.HttpClientExtension;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.hl7.fhir.r4.hapi.rest.server.helper.BatchHelperR4;
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.DecimalType;
import org.hl7.fhir.r4.model.IntegerType;
import org.hl7.fhir.r4.model.Parameters;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
public class DeleteExpungeProviderTest extends BaseR4ServerTest {
@ExtendWith(MockitoExtension.class)
public class DeleteExpungeProviderTest {
public static final String TEST_JOB_ID = "test-job-id";
private static final Logger ourLog = LoggerFactory.getLogger(DeleteExpungeProviderTest.class);
private Parameters myReturnParameters;
private MyDeleteExpungeJobSubmitter myDeleteExpungeJobSubmitter = new MyDeleteExpungeJobSubmitter();
private static final FhirContext ourCtx = FhirContext.forR4Cached();
@RegisterExtension
public static RestfulServerExtension myServer = new RestfulServerExtension(ourCtx);
@RegisterExtension
private final HttpClientExtension myClient = new HttpClientExtension();
@Mock
private IDeleteExpungeJobSubmitter myDeleteExpungeJobSubmitter;
private DeleteExpungeProvider myProvider;
@BeforeEach
public void reset() {
myReturnParameters = new Parameters();
myReturnParameters.addParameter("success", true);
public void beforeEach() {
myProvider = new DeleteExpungeProvider(ourCtx, myDeleteExpungeJobSubmitter);
myServer.registerProvider(myProvider);
}
@AfterEach
public void afterEach() {
myServer.unregisterProvider(myProvider);
}
@Test
public void testDeleteExpunge() throws Exception {
public void testSupplyingNoUrlsProvidesValidErrorMessage() throws IOException {
HttpPost post = new HttpPost(myServer.getBaseUrl() + "/" + ProviderConstants.OPERATION_DELETE_EXPUNGE);
try(CloseableHttpResponse execute = myClient.execute(post)) {
String body = IOUtils.toString(execute.getEntity().getContent(), Charset.defaultCharset());
assertThat(execute.getStatusLine().getStatusCode(), is(equalTo(400)));
assertThat(body, is(containsString("At least one `url` parameter to $delete-expunge must be provided.")));
}
}
@Test
public void testDeleteExpunge() {
// setup
Parameters input = new Parameters();
String url1 = "Observation?status=active";
String url2 = "Patient?active=false";
Integer batchSize = 2401;
int batchSize = 2401;
input.addParameter(ProviderConstants.OPERATION_DELETE_EXPUNGE_URL, url1);
input.addParameter(ProviderConstants.OPERATION_DELETE_EXPUNGE_URL, url2);
input.addParameter(ProviderConstants.OPERATION_DELETE_BATCH_SIZE, new DecimalType(batchSize));
input.addParameter(ProviderConstants.OPERATION_DELETE_CASCADE, new BooleanType(true));
input.addParameter(ProviderConstants.OPERATION_DELETE_CASCADE_MAX_ROUNDS, new IntegerType(44));
input.addParameter(ProviderConstants.OPERATION_DELETE_BATCH_SIZE, new IntegerType(batchSize));
ourLog.debug(myCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(input));
when(myDeleteExpungeJobSubmitter.submitJob(any(), any(), anyBoolean(), any(), any())).thenReturn(TEST_JOB_ID);
DeleteExpungeProvider provider = new DeleteExpungeProvider(myCtx, myDeleteExpungeJobSubmitter);
startServer(provider);
Parameters response = myClient
// Test
Parameters response = myServer
.getFhirClient()
.operation()
.onServer()
.named(ProviderConstants.OPERATION_DELETE_EXPUNGE)
.withParameters(input)
.execute();
ourLog.debug(myCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(response));
// Verify
ourLog.debug(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(response));
assertEquals(TEST_JOB_ID, BatchHelperR4.jobIdFromBatch2Parameters(response));
assertThat(myDeleteExpungeJobSubmitter.calledWithUrls, hasSize(2));
assertEquals(url1, myDeleteExpungeJobSubmitter.calledWithUrls.get(0));
assertEquals(url2, myDeleteExpungeJobSubmitter.calledWithUrls.get(1));
assertEquals(batchSize, myDeleteExpungeJobSubmitter.calledWithBatchSize);
assertNotNull(myDeleteExpungeJobSubmitter.calledWithRequestDetails);
verify(myDeleteExpungeJobSubmitter, times(1)).submitJob(
eq(2401),
eq(List.of(url1, url2)),
eq(true),
eq(44),
any()
);
}
private class MyDeleteExpungeJobSubmitter implements IDeleteExpungeJobSubmitter {
Integer calledWithBatchSize;
List<String> calledWithUrls;
RequestDetails calledWithRequestDetails;
@Override
public String submitJob(Integer theBatchSize, List<String> theUrlsToProcess, RequestDetails theRequest) {
calledWithBatchSize = theBatchSize;
calledWithUrls = theUrlsToProcess;
calledWithRequestDetails = theRequest;
return TEST_JOB_ID;
}
}
}

View File

@ -64,6 +64,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNotNull;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ -118,7 +119,7 @@ public class BulkDataImportProviderTest {
String jobId = UUID.randomUUID().toString();
Batch2JobStartResponse startResponse = new Batch2JobStartResponse();
startResponse.setInstanceId(jobId);
when(myJobCoordinator.startInstance(any()))
when(myJobCoordinator.startInstance(isNotNull(), any()))
.thenReturn(startResponse);
String requestUrl;
@ -149,7 +150,7 @@ public class BulkDataImportProviderTest {
assertEquals("Use the following URL to poll for job status: " + requestUrl + "$import-poll-status?_jobId=" + jobId, oo.getIssue().get(1).getDiagnostics());
}
verify(myJobCoordinator, times(1)).startInstance(myStartRequestCaptor.capture());
verify(myJobCoordinator, times(1)).startInstance(isNotNull(), myStartRequestCaptor.capture());
JobInstanceStartRequest startRequest = myStartRequestCaptor.getValue();
ourLog.info("Parameters: {}", startRequest.getParameters());
@ -407,6 +408,7 @@ public class BulkDataImportProviderTest {
}
}
@Override
public void validateHasPartitionPermissions(RequestDetails theRequest, String theResourceType, RequestPartitionId theRequestPartitionId) {
if (!myPartitionName.equals(theRequest.getTenantId()) && theRequest.getTenantId() != null) {
throw new ForbiddenOperationException("User does not have access to resources on the requested partition");

View File

@ -1,14 +1,12 @@
package ca.uhn.fhir.batch2.jobs.reindex;
import ca.uhn.fhir.batch2.jobs.parameters.UrlListValidator;
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.ValueSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Value;
import java.util.List;
@ -40,7 +38,7 @@ public class ReindexJobParametersValidatorTest {
parameters.addUrl(theUrl);
// test
List<String> errors = myValidator.validate(parameters);
List<String> errors = myValidator.validate(null, parameters);
return errors;
}

View File

@ -38,6 +38,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.isNotNull;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ -71,7 +72,7 @@ public class ReindexProviderTest {
public void beforeEach() {
myServerExtension.registerProvider(mySvc);
when(myJobCoordinator.startInstance(any()))
when(myJobCoordinator.startInstance(isNotNull(), any()))
.thenReturn(createJobStartResponse());
when(myRequestPartitionHelperSvc.determineReadPartitionForRequest(any(), any())).thenReturn(RequestPartitionId.allPartitions());
}
@ -119,7 +120,7 @@ public class ReindexProviderTest {
StringType jobId = (StringType) response.getParameterValue(ProviderConstants.OPERATION_REINDEX_RESPONSE_JOB_ID);
assertEquals(TEST_JOB_ID, jobId.getValue());
verify(myJobCoordinator, times(1)).startInstance(myStartRequestCaptor.capture());
verify(myJobCoordinator, times(1)).startInstance(isNotNull(), myStartRequestCaptor.capture());
ReindexJobParameters params = myStartRequestCaptor.getValue().getParameters(ReindexJobParameters.class);
assertThat(params.getPartitionedUrls(), hasSize(1));
assertEquals(url, params.getPartitionedUrls().get(0).getUrl());
@ -155,7 +156,7 @@ public class ReindexProviderTest {
StringType jobId = (StringType) response.getParameterValue(ProviderConstants.OPERATION_REINDEX_RESPONSE_JOB_ID);
assertEquals(TEST_JOB_ID, jobId.getValue());
verify(myJobCoordinator, times(1)).startInstance(myStartRequestCaptor.capture());
verify(myJobCoordinator, times(1)).startInstance(isNotNull(), myStartRequestCaptor.capture());
ReindexJobParameters params = myStartRequestCaptor.getValue().getParameters(ReindexJobParameters.class);
assertThat(params.getPartitionedUrls(), empty());
// Non-default values

View File

@ -11,6 +11,7 @@ import ca.uhn.fhir.jpa.api.model.BulkExportParameters;
import ca.uhn.fhir.jpa.api.svc.IBatch2JobRunner;
import ca.uhn.fhir.jpa.batch.models.Batch2BaseJobParameters;
import ca.uhn.fhir.jpa.bulk.export.model.BulkExportJobStatusEnum;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.util.Batch2JobDefinitionConstants;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
@ -35,6 +36,7 @@ import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNotNull;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ -70,7 +72,7 @@ public class Batch2JobRunnerImplTest {
ourLog.setLevel(Level.ERROR);
// test
myJobRunner.startNewJob(new Batch2BaseJobParameters(jobId));
myJobRunner.startNewJob(new SystemRequestDetails(), new Batch2BaseJobParameters(jobId));
// verify
ArgumentCaptor<ILoggingEvent> captor = ArgumentCaptor.forClass(ILoggingEvent.class);
@ -87,7 +89,7 @@ public class Batch2JobRunnerImplTest {
ourLog.setLevel(Level.ERROR);
// test
myJobRunner.startNewJob(new Batch2BaseJobParameters(Batch2JobDefinitionConstants.BULK_EXPORT));
myJobRunner.startNewJob(new SystemRequestDetails(), new Batch2BaseJobParameters(Batch2JobDefinitionConstants.BULK_EXPORT));
// verify
ArgumentCaptor<ILoggingEvent> captor = ArgumentCaptor.forClass(ILoggingEvent.class);
@ -115,12 +117,12 @@ public class Batch2JobRunnerImplTest {
when(myJobCoordinator.getInstance(eq(jobInstanceId))).thenReturn(mockJobInstance);
// test
myJobRunner.startNewJob(parameters);
myJobRunner.startNewJob(new SystemRequestDetails(), parameters);
// verify
ArgumentCaptor<JobInstanceStartRequest> captor = ArgumentCaptor.forClass(JobInstanceStartRequest.class);
verify(myJobCoordinator)
.startInstance(captor.capture());
.startInstance(isNotNull(), captor.capture());
JobInstanceStartRequest val = captor.getValue();
// we need to verify something in the parameters
ourLog.info(val.getParameters());
@ -175,12 +177,12 @@ public class Batch2JobRunnerImplTest {
when(myJobCoordinator.getInstance(eq(jobInstanceId))).thenReturn(mockJobInstance);
// test
myJobRunner.startNewJob(parameters);
myJobRunner.startNewJob(new SystemRequestDetails(), parameters);
// verify
ArgumentCaptor<JobInstanceStartRequest> captor = ArgumentCaptor.forClass(JobInstanceStartRequest.class);
verify(myJobCoordinator)
.startInstance(captor.capture());
.startInstance(isNotNull(), captor.capture());
JobInstanceStartRequest val = captor.getValue();
// we need to verify something in the parameters
ourLog.info(val.getParameters());

View File

@ -24,6 +24,7 @@ import ca.uhn.fhir.batch2.model.JobInstanceStartRequest;
import ca.uhn.fhir.batch2.model.StatusEnum;
import ca.uhn.fhir.batch2.models.JobInstanceFetchRequest;
import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.springframework.data.domain.Page;
@ -40,8 +41,24 @@ public interface IJobCoordinator {
* @param theStartRequest The request, containing the job type and parameters
* @return Returns a unique ID for this job execution
* @throws InvalidRequestException If the request is invalid (incorrect/missing parameters, etc)
* @deprecated Use {@link #startInstance(RequestDetails, JobInstanceStartRequest)}
*/
Batch2JobStartResponse startInstance(JobInstanceStartRequest theStartRequest) throws InvalidRequestException;
@Deprecated(since = "6.8.0", forRemoval = true)
default Batch2JobStartResponse startInstance(JobInstanceStartRequest theStartRequest) throws InvalidRequestException {
return startInstance(null, theStartRequest);
}
/**
* Starts a new job instance
*
* @param theRequestDetails The request details associated with the request. This will get used to validate that the
* request is appropriate for the given user, so if at all possible it should be the
* original RequestDetails from the server request.
* @param theStartRequest The request, containing the job type and parameters
* @return Returns a unique ID for this job execution
* @throws InvalidRequestException If the request is invalid (incorrect/missing parameters, etc)
*/
Batch2JobStartResponse startInstance(RequestDetails theRequestDetails, JobInstanceStartRequest theStartRequest) throws InvalidRequestException;
/**
* Fetch details about a job instance

View File

@ -20,6 +20,7 @@
package ca.uhn.fhir.batch2.api;
import ca.uhn.fhir.model.api.IModelJson;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@ -44,10 +45,11 @@ public interface IJobParametersValidator<T extends IModelJson> {
/**
* Validate the given job parameters.
*
* @param theParameters The parameters object to validate
* @param theRequestDetails The request details associated with the start request
* @param theParameters The parameters object to validate
* @return Any strings returned by this method are treated as validation failures and returned to the client initiating the job. Return <code>null</code> or an empty list to indicate that no validation failures occurred.
*/
@Nullable
List<String> validate(@Nonnull T theParameters);
List<String> validate(RequestDetails theRequestDetails, @Nonnull T theParameters);
}

View File

@ -35,6 +35,7 @@ import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse;
import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService;
import ca.uhn.fhir.jpa.subscription.channel.api.IChannelReceiver;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.util.Logs;
@ -90,7 +91,7 @@ public class JobCoordinatorImpl implements IJobCoordinator {
}
@Override
public Batch2JobStartResponse startInstance(JobInstanceStartRequest theStartRequest) {
public Batch2JobStartResponse startInstance(RequestDetails theRequestDetails, JobInstanceStartRequest theStartRequest) {
String paramsString = theStartRequest.getParameters();
if (isBlank(paramsString)) {
throw new InvalidRequestException(Msg.code(2065) + "No parameters supplied");
@ -119,7 +120,7 @@ public class JobCoordinatorImpl implements IJobCoordinator {
JobDefinition<?> jobDefinition = myJobDefinitionRegistry
.getLatestJobDefinition(theStartRequest.getJobDefinitionId()).orElseThrow(() -> new IllegalArgumentException(Msg.code(2063) + "Unknown job definition ID: " + theStartRequest.getJobDefinitionId()));
myJobParameterJsonValidator.validateJobParameters(theStartRequest, jobDefinition);
myJobParameterJsonValidator.validateJobParameters(theRequestDetails, theStartRequest, jobDefinition);
IJobPersistence.CreateResult instanceAndFirstChunk =
myTransactionService.withSystemRequest().execute(() ->

View File

@ -24,6 +24,7 @@ import ca.uhn.fhir.batch2.model.JobDefinition;
import ca.uhn.fhir.batch2.model.JobInstanceStartRequest;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.model.api.IModelJson;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import javax.annotation.Nonnull;
@ -41,7 +42,7 @@ import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
class JobParameterJsonValidator {
private final ValidatorFactory myValidatorFactory = Validation.buildDefaultValidatorFactory();
<PT extends IModelJson> void validateJobParameters(@Nonnull JobInstanceStartRequest theStartRequest, @Nonnull JobDefinition<PT> theJobDefinition) {
<PT extends IModelJson> void validateJobParameters(RequestDetails theRequestDetails, @Nonnull JobInstanceStartRequest theStartRequest, @Nonnull JobDefinition<PT> theJobDefinition) {
// JSR 380
Validator validator = myValidatorFactory.getValidator();
@ -52,7 +53,7 @@ class JobParameterJsonValidator {
// Programmatic Validator
IJobParametersValidator<PT> parametersValidator = theJobDefinition.getParametersValidator();
if (parametersValidator != null) {
List<String> outcome = parametersValidator.validate(parameters);
List<String> outcome = parametersValidator.validate(theRequestDetails, parameters);
outcome = defaultIfNull(outcome, Collections.emptyList());
errorStrings.addAll(outcome);
}

View File

@ -19,6 +19,7 @@
*/
package ca.uhn.fhir.batch2.jobs.chunk;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
import ca.uhn.fhir.model.api.IModelJson;
import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
@ -34,13 +35,29 @@ import java.util.stream.Collectors;
public class ResourceIdListWorkChunkJson implements IModelJson {
@JsonProperty("requestPartitionId")
private RequestPartitionId myRequestPartitionId;
@JsonProperty("ids")
private List<TypedPidJson> myTypedPids;
public ResourceIdListWorkChunkJson() {}
/**
* Constructor
*/
public ResourceIdListWorkChunkJson() {
super();
}
public ResourceIdListWorkChunkJson(Collection<TypedPidJson> theTypedPids) {
/**
* Constructor
*/
public ResourceIdListWorkChunkJson(Collection<TypedPidJson> theTypedPids, RequestPartitionId theRequestPartitionId) {
this();
getTypedPids().addAll(theTypedPids);
myRequestPartitionId = theRequestPartitionId;
}
public RequestPartitionId getRequestPartitionId() {
return myRequestPartitionId;
}
private List<TypedPidJson> getTypedPids() {

View File

@ -0,0 +1,13 @@
package ca.uhn.fhir.batch2.jobs.parameters;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.List;
public interface IUrlListValidator {
@Nullable
List<String> validateUrls(@Nonnull List<String> theUrls);
@Nullable
List<String> validatePartitionedUrls(@Nonnull List<PartitionedUrl> thePartitionedUrls);
}

View File

@ -49,7 +49,6 @@ public class PartitionedUrlListJobParameters extends PartitionedJobParameters {
public PartitionedUrlListJobParameters addUrl(@Nonnull String theUrl) {
PartitionedUrl partitionedUrl = new PartitionedUrl();
partitionedUrl.setUrl(theUrl);
partitionedUrl.setRequestPartitionId(RequestPartitionId.defaultPartition());
return addPartitionedUrl(partitionedUrl);
}
}

View File

@ -27,7 +27,7 @@ import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class UrlListValidator {
public class UrlListValidator implements IUrlListValidator {
private final String myOperationName;
private final IBatch2DaoSvc myBatch2DaoSvc;
@ -38,6 +38,7 @@ public class UrlListValidator {
@Nullable
@Override
public List<String> validateUrls(@Nonnull List<String> theUrls) {
if (theUrls.isEmpty()) {
if (!myBatch2DaoSvc.isAllResourceTypeSupported()) {
@ -48,6 +49,7 @@ public class UrlListValidator {
}
@Nullable
@Override
public List<String> validatePartitionedUrls(@Nonnull List<PartitionedUrl> thePartitionedUrls) {
List<String> urls = thePartitionedUrls.stream().map(PartitionedUrl::getUrl).collect(Collectors.toList());
return validateUrls(urls);

View File

@ -48,7 +48,11 @@ public class PartitionedUrlListIdChunkProducer implements IIdChunkProducer<Parti
return myBatch2DaoSvc.fetchResourceIdsPage(theNextStart, theEnd, thePageSize, theRequestPartitionId, null);
} else {
ourLog.info("Fetching resource ID chunk for URL {} - Range {} - {}", partitionedUrl.getUrl(), theNextStart, theEnd);
return myBatch2DaoSvc.fetchResourceIdsPage(theNextStart, theEnd, thePageSize, partitionedUrl.getRequestPartitionId(), partitionedUrl.getUrl());
RequestPartitionId requestPartitionId = partitionedUrl.getRequestPartitionId();
if (requestPartitionId == null) {
requestPartitionId = theRequestPartitionId;
}
return myBatch2DaoSvc.fetchResourceIdsPage(theNextStart, theEnd, thePageSize, requestPartitionId, partitionedUrl.getUrl());
}
}
}

View File

@ -117,24 +117,24 @@ public class ResourceIdListStep<PT extends PartitionedJobParameters, IT extends
totalIdsFound += submissionIds.size();
chunkCount++;
submitWorkChunk(submissionIds, theDataSink);
submitWorkChunk(submissionIds, nextChunk.getRequestPartitionId(), theDataSink);
}
}
totalIdsFound += idBuffer.size();
chunkCount++;
submitWorkChunk(idBuffer, theDataSink);
submitWorkChunk(idBuffer, requestPartitionId, theDataSink);
ourLog.info("Submitted {} chunks with {} resource IDs", chunkCount, totalIdsFound);
return RunOutcome.SUCCESS;
}
private void submitWorkChunk(Collection<TypedPidJson> theTypedPids, IJobDataSink<ResourceIdListWorkChunkJson> theDataSink) {
private void submitWorkChunk(Collection<TypedPidJson> theTypedPids, RequestPartitionId theRequestPartitionId, IJobDataSink<ResourceIdListWorkChunkJson> theDataSink) {
if (theTypedPids.isEmpty()) {
return;
}
ourLog.info("Submitting work chunk with {} IDs", theTypedPids.size());
ResourceIdListWorkChunkJson data = new ResourceIdListWorkChunkJson(theTypedPids);
ResourceIdListWorkChunkJson data = new ResourceIdListWorkChunkJson(theTypedPids, theRequestPartitionId);
ourLog.debug("IDs are: {}", data);
theDataSink.accept(data);
}

View File

@ -521,7 +521,7 @@ public class JobCoordinatorImplTest extends BaseBatch2Test {
// Setup
IJobParametersValidator<TestJobParameters> v = p -> {
IJobParametersValidator<TestJobParameters> v = (theRequestDetails, p) -> {
if (p.getParam1().equals("bad")) {
return Lists.newArrayList("Bad Parameter Value", "Bad Parameter Value 2");
}

View File

@ -109,7 +109,7 @@ public class LoadIdsStepTest {
for (long i = idLow; i < idHigh; i++) {
ids.add(JpaPid.fromId(i));
}
IResourcePidList chunk = new HomogeneousResourcePidList("Patient", ids, lastDate);
IResourcePidList chunk = new HomogeneousResourcePidList("Patient", ids, lastDate, null);
return chunk;
}

View File

@ -22,6 +22,7 @@ package ca.uhn.fhir.mdm.batch2.clear;
import ca.uhn.fhir.batch2.api.IJobParametersValidator;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@ -41,7 +42,7 @@ public class MdmClearJobParametersValidator implements IJobParametersValidator<M
@Nullable
@Override
public List<String> validate(@Nonnull MdmClearJobParameters theParameters) {
public List<String> validate(RequestDetails theRequestDetails, @Nonnull MdmClearJobParameters theParameters) {
if (myMdmSettings == null || !myMdmSettings.isEnabled()) {
return Collections.singletonList("Mdm is not enabled on this server");
}

View File

@ -23,14 +23,12 @@ import ca.uhn.fhir.batch2.api.IJobParametersValidator;
import ca.uhn.fhir.batch2.jobs.parameters.PartitionedUrl;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
@ -49,7 +47,7 @@ public class MdmSubmitJobParametersValidator implements IJobParametersValidator<
@Nonnull
@Override
public List<String> validate(@Nonnull MdmSubmitJobParameters theParameters) {
public List<String> validate(RequestDetails theRequestDetails, @Nonnull MdmSubmitJobParameters theParameters) {
List<String> errorMsgs = new ArrayList<>();
for (PartitionedUrl partitionedUrl : theParameters.getPartitionedUrls()) {
String url = partitionedUrl.getUrl();

View File

@ -38,7 +38,7 @@ class MdmClearJobParametersValidatorTest {
MdmClearJobParameters parameters = new MdmClearJobParameters();
// execute
List<String> result = myMdmClearJobParametersValidator.validate(parameters);
List<String> result = myMdmClearJobParametersValidator.validate(null, parameters);
// verify
assertThat(result, hasSize(1));
@ -52,7 +52,7 @@ class MdmClearJobParametersValidatorTest {
when(myMdmSettings.isEnabled()).thenReturn(true);
// execute
List<String> result = myMdmClearJobParametersValidator.validate(parameters);
List<String> result = myMdmClearJobParametersValidator.validate(null, parameters);
// verify
assertThat(result, hasSize(1));
@ -69,7 +69,7 @@ class MdmClearJobParametersValidatorTest {
when(myMdmSettings.getMdmRules()).thenReturn(rules);
// execute
List<String> result = myMdmClearJobParametersValidator.validate(parameters);
List<String> result = myMdmClearJobParametersValidator.validate(null, parameters);
// verify
assertThat(result, hasSize(2));
@ -88,7 +88,7 @@ class MdmClearJobParametersValidatorTest {
when(myDaoRegistry.isResourceTypeSupported("Patient")).thenReturn(true);
// execute
List<String> result = myMdmClearJobParametersValidator.validate(parameters);
List<String> result = myMdmClearJobParametersValidator.validate(null, parameters);
// verify
assertThat(result, hasSize(0));

View File

@ -1,10 +1,8 @@
package ca.uhn.fhir.mdm.batch2.submit;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.rules.json.MdmRulesJson;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -48,7 +46,7 @@ class MdmSubmitJobParametersValidatorTest {
MdmSubmitJobParameters parameters = new MdmSubmitJobParameters();
parameters.addUrl("Practitioner?name=foo");
List<String> errors = myValidator.validate(parameters);
List<String> errors = myValidator.validate(null, parameters);
assertThat(errors, hasSize(1));
assertThat(errors.get(0), is(equalTo("Resource type Practitioner is not supported by MDM. Check your MDM settings")));
}
@ -59,7 +57,7 @@ class MdmSubmitJobParametersValidatorTest {
when(myMatchUrlService.translateMatchUrl(anyString(), any())).thenThrow(new InvalidRequestException("Can't find death-date!"));
MdmSubmitJobParameters parameters = new MdmSubmitJobParameters();
parameters.addUrl("Practitioner?death-date=foo");
List<String> errors = myValidator.validate(parameters);
List<String> errors = myValidator.validate(null, parameters);
assertThat(errors, hasSize(1));
assertThat(errors.get(0), is(equalTo("Invalid request detected: Can't find death-date!")));
}

View File

@ -1823,6 +1823,7 @@ public class JpaStorageSettings extends StorageSettings {
/**
* <p>
* This determines the maximum number of conflicts that should be fetched and handled while retrying a delete of a resource.
* This can also be thought of as the maximum number of rounds of cascading deletion.
* </p>
* <p>
* The default value for this setting is {@code 60}.
@ -1837,6 +1838,7 @@ public class JpaStorageSettings extends StorageSettings {
/**
* <p>
* This determines the maximum number of conflicts that should be fetched and handled while retrying a delete of a resource.
* This can also be thought of as the maximum number of rounds of cascading deletion.
* </p>
* <p>
* The default value for this setting is {@code 60}.

View File

@ -19,15 +19,12 @@
*/
package ca.uhn.fhir.jpa.api.pid;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.*;
abstract public class BaseResourcePidList implements IResourcePidList {
@ -35,10 +32,17 @@ abstract public class BaseResourcePidList implements IResourcePidList {
@Nullable
final Date myLastDate;
private final RequestPartitionId myRequestPartitionId;
BaseResourcePidList(Collection<IResourcePersistentId> theIds, Date theLastDate) {
BaseResourcePidList(Collection<IResourcePersistentId> theIds, Date theLastDate, RequestPartitionId theRequestPartitionId) {
myIds.addAll(theIds);
myLastDate = theLastDate;
myRequestPartitionId = theRequestPartitionId;
}
@Override
public RequestPartitionId getRequestPartitionId() {
return myRequestPartitionId;
}
@Override

View File

@ -20,6 +20,7 @@
package ca.uhn.fhir.jpa.api.pid;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
import org.apache.commons.lang3.builder.ToStringBuilder;
@ -32,6 +33,11 @@ import java.util.List;
* An empty resource pid list
*/
public class EmptyResourcePidList implements IResourcePidList {
@Override
public RequestPartitionId getRequestPartitionId() {
return null;
}
@Override
public Date getLastDate() {
return null;

View File

@ -19,6 +19,7 @@
*/
package ca.uhn.fhir.jpa.api.pid;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
import javax.annotation.Nonnull;
@ -32,8 +33,8 @@ public class HomogeneousResourcePidList extends BaseResourcePidList {
@Nonnull
final String myResourceType;
public HomogeneousResourcePidList(String theResourceType, Collection<IResourcePersistentId> theIds, Date theLastDate) {
super(theIds, theLastDate);
public HomogeneousResourcePidList(String theResourceType, Collection<IResourcePersistentId> theIds, Date theLastDate, RequestPartitionId theRequestPartitionId) {
super(theIds, theLastDate, theRequestPartitionId);
myResourceType = theResourceType;
}

View File

@ -19,6 +19,7 @@
*/
package ca.uhn.fhir.jpa.api.pid;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
import javax.annotation.Nonnull;
@ -30,6 +31,8 @@ import java.util.List;
*/
public interface IResourcePidList {
RequestPartitionId getRequestPartitionId();
Date getLastDate();
int size();

Some files were not shown because too many files have changed in this diff Show More